mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 15:44:42 -05:00
Compare commits
157 Commits
2182
...
remove-any
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0375df5431 | ||
|
|
6206073c5e | ||
|
|
3d2cdc21a1 | ||
|
|
93d939aef8 | ||
|
|
fb04750607 | ||
|
|
a080496e7e | ||
|
|
9fc1002167 | ||
|
|
bc5c766530 | ||
|
|
17821f863a | ||
|
|
1ca4f34ef3 | ||
|
|
8f0a1554b1 | ||
|
|
38d4f26d03 | ||
|
|
2b04c2710d | ||
|
|
a4937a1236 | ||
|
|
f6f2c39686 | ||
|
|
d7eacf1ab5 | ||
|
|
d1a4bbe28e | ||
|
|
412ecd6b1b | ||
|
|
9bc0152121 | ||
|
|
4b05cada8f | ||
|
|
a818862704 | ||
|
|
173487debc | ||
|
|
449d96cc9a | ||
|
|
f9bf6a95ed | ||
|
|
5bf6c94bb2 | ||
|
|
e1ce94a28d | ||
|
|
2a62dcf27e | ||
|
|
3094766c5c | ||
|
|
a52804595d | ||
|
|
e72f12d32b | ||
|
|
e70083708a | ||
|
|
cbc4caef19 | ||
|
|
fbeee4dbf5 | ||
|
|
d13f7e5438 | ||
|
|
7b543bd31c | ||
|
|
1743724420 | ||
|
|
73e0add670 | ||
|
|
4f5eb444bc | ||
|
|
7de98823fb | ||
|
|
6d930573fc | ||
|
|
3317002ff5 | ||
|
|
99403d0167 | ||
|
|
23ce022c60 | ||
|
|
96e1fd0fb8 | ||
|
|
f28dac1093 | ||
|
|
ff28544fb2 | ||
|
|
27765b417c | ||
|
|
b0d8d4ee26 | ||
|
|
c4b1176a6a | ||
|
|
fd133dd79a | ||
|
|
9c2477a4cf | ||
|
|
f3b6d1f351 | ||
|
|
5af7b54c9c | ||
|
|
ba9604101d | ||
|
|
e136c1fc44 | ||
|
|
c581b3293e | ||
|
|
cc7f861637 | ||
|
|
642d6fc72b | ||
|
|
e69c7f4ae0 | ||
|
|
9ca36d4763 | ||
|
|
8dc600ca02 | ||
|
|
b621ead607 | ||
|
|
66cf21f650 | ||
|
|
f3dcdc057d | ||
|
|
2bdacf636e | ||
|
|
fc06980c60 | ||
|
|
550a3a4e6d | ||
|
|
3310e7766b | ||
|
|
5ab865e89d | ||
|
|
f0c60f6ef6 | ||
|
|
f3f685c923 | ||
|
|
3646bf31b0 | ||
|
|
b39895fa2d | ||
|
|
1fce8931ab | ||
|
|
6166f6edbd | ||
|
|
dc9fbb0585 | ||
|
|
d7b2f9d05b | ||
|
|
69c4090d32 | ||
|
|
fff5fa3459 | ||
|
|
e92b80c71e | ||
|
|
8bb04ef248 | ||
|
|
d7881ccfb5 | ||
|
|
96a1f80daf | ||
|
|
a083b57260 | ||
|
|
4fa6660a3f | ||
|
|
43f2ad7043 | ||
|
|
2bf04072ea | ||
|
|
efc6fc017d | ||
|
|
6cb10401df | ||
|
|
346efd66f5 | ||
|
|
7c0889e873 | ||
|
|
bb40576bd5 | ||
|
|
6baf20275f | ||
|
|
5a57d48913 | ||
|
|
73f0207a7d | ||
|
|
4e4fb8ab10 | ||
|
|
b9cccc6b91 | ||
|
|
d42163d888 | ||
|
|
2db3e4f4d8 | ||
|
|
45380a258a | ||
|
|
40292d0896 | ||
|
|
e8be9e31ff | ||
|
|
3d0fdb1ab0 | ||
|
|
4dea1195e2 | ||
|
|
92ea39ddac | ||
|
|
05e08166c4 | ||
|
|
827cc0bdfa | ||
|
|
57bd343f4a | ||
|
|
4a76aead68 | ||
|
|
48c2148589 | ||
|
|
32bea69c28 | ||
|
|
f3c57f8bce | ||
|
|
000896b2f7 | ||
|
|
88004e5042 | ||
|
|
6001a93475 | ||
|
|
4784b2ddab | ||
|
|
32b4cd008f | ||
|
|
823f8b51be | ||
|
|
209743d6bc | ||
|
|
b93a88accc | ||
|
|
dc2314d5e2 | ||
|
|
33aa676854 | ||
|
|
4a3b3ffb8a | ||
|
|
ee5cbf1891 | ||
|
|
8fcf3544a8 | ||
|
|
2b8e987cb8 | ||
|
|
998165148b | ||
|
|
c80eff1098 | ||
|
|
cd8f2c2153 | ||
|
|
cb0abff2d5 | ||
|
|
3b1b2e2dcc | ||
|
|
7831e4ad05 | ||
|
|
e7bb859cd9 | ||
|
|
9fc26e609c | ||
|
|
4f1ee65e6c | ||
|
|
ceff827a77 | ||
|
|
a7db918775 | ||
|
|
be20ecd366 | ||
|
|
5790d8ad12 | ||
|
|
7dc58e248c | ||
|
|
7b03e63b23 | ||
|
|
55fd7c6421 | ||
|
|
ba1ea4c2bb | ||
|
|
6a4fc96835 | ||
|
|
58476bb98e | ||
|
|
88d4f14541 | ||
|
|
6bba233ba7 | ||
|
|
1d99764740 | ||
|
|
53cc479c14 | ||
|
|
d3707d9b88 | ||
|
|
1df4076fd2 | ||
|
|
28337bb6c9 | ||
|
|
c5dea52e69 | ||
|
|
d5096ff2e6 | ||
|
|
84734f1110 | ||
|
|
7094308287 | ||
|
|
3b88c8ccd2 |
6
.github/workflows/ci-changed-examples.yml
vendored
6
.github/workflows/ci-changed-examples.yml
vendored
@@ -1,23 +1,21 @@
|
||||
name: CI Changed Examples
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- leptos_0.6
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
- leptos_0.6
|
||||
jobs:
|
||||
get-example-changed:
|
||||
uses: ./.github/workflows/get-example-changed.yml
|
||||
|
||||
get-matrix:
|
||||
needs: [get-example-changed]
|
||||
uses: ./.github/workflows/get-changed-examples-matrix.yml
|
||||
with:
|
||||
example_changed: ${{ fromJSON(needs.get-example-changed.outputs.example_changed) }}
|
||||
|
||||
test:
|
||||
name: CI
|
||||
needs: [get-example-changed, get-matrix]
|
||||
|
||||
4
.github/workflows/ci-examples.yml
vendored
4
.github/workflows/ci-examples.yml
vendored
@@ -1,13 +1,13 @@
|
||||
name: CI Examples
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- leptos_0.6
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
- leptos_0.6
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
|
||||
7
.github/workflows/ci-semver.yml
vendored
7
.github/workflows/ci-semver.yml
vendored
@@ -1,27 +1,24 @@
|
||||
name: CI semver
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- leptos_0.6
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
- leptos_0.6
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
|
||||
test:
|
||||
needs: [get-leptos-changed]
|
||||
if: github.event.pull_request.labels[0].name == 'semver' # needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
|
||||
name: Run semver check (nightly-2024-08-01)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Semver Checks
|
||||
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
with:
|
||||
|
||||
39
.github/workflows/ci.yml
vendored
39
.github/workflows/ci.yml
vendored
@@ -1,50 +1,25 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- leptos_0.6
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
- leptos_0.6
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
|
||||
get-leptos-matrix:
|
||||
uses: ./.github/workflows/get-leptos-matrix.yml
|
||||
test:
|
||||
name: CI
|
||||
needs: [get-leptos-changed]
|
||||
needs: [get-leptos-changed, get-leptos-matrix]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
strategy:
|
||||
matrix:
|
||||
directory:
|
||||
[
|
||||
any_error,
|
||||
any_spawner,
|
||||
const_str_slice_concat,
|
||||
either_of,
|
||||
hydration_context,
|
||||
integrations/actix,
|
||||
integrations/axum,
|
||||
integrations/utils,
|
||||
leptos,
|
||||
leptos_config,
|
||||
leptos_dom,
|
||||
leptos_hot_reload,
|
||||
leptos_macro,
|
||||
leptos_server,
|
||||
meta,
|
||||
next_tuple,
|
||||
oco,
|
||||
or_poisoned,
|
||||
reactive_graph,
|
||||
router,
|
||||
router_macro,
|
||||
server_fn,
|
||||
server_fn/server_fn_macro_default,
|
||||
server_fn_macro,
|
||||
]
|
||||
matrix: ${{ fromJSON(needs.get-leptos-matrix.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
- name: Get example project directories that changed
|
||||
id: changed-dirs
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
dir_names: true
|
||||
dir_names_max_depth: "2"
|
||||
|
||||
8
.github/workflows/get-example-changed.yml
vendored
8
.github/workflows/get-example-changed.yml
vendored
@@ -1,12 +1,10 @@
|
||||
name: Examples Changed Call
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
example_changed:
|
||||
description: "Example Changed"
|
||||
value: ${{ jobs.get-example-changed.outputs.example_changed }}
|
||||
|
||||
jobs:
|
||||
get-example-changed:
|
||||
name: Get Example Changed
|
||||
@@ -18,21 +16,17 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get example files that changed
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
examples/**
|
||||
!examples/cargo-make/**
|
||||
!examples/gtk/**
|
||||
!examples/Makefile.toml
|
||||
!examples/*.md
|
||||
|
||||
- name: List example files that changed
|
||||
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'
|
||||
|
||||
- name: Set example_changed
|
||||
id: set-example-changed
|
||||
run: |
|
||||
|
||||
18
.github/workflows/get-examples-matrix.yml
vendored
18
.github/workflows/get-examples-matrix.yml
vendored
@@ -1,38 +1,34 @@
|
||||
name: Get Examples Matrix Call
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
matrix:
|
||||
description: "Matrix"
|
||||
value: ${{ jobs.create.outputs.matrix }}
|
||||
|
||||
jobs:
|
||||
create:
|
||||
name: Create Examples Matrix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
env:
|
||||
# separate examples using "|" (vertical bar) char like "a|b|c".
|
||||
# cargo-make should be excluded by default.
|
||||
EXCLUDED_EXAMPLES: cargo-make
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install jq
|
||||
run: sudo apt-get install jq
|
||||
|
||||
- name: Set Matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
examples=$(ls examples |
|
||||
awk '{print "examples/" $0}' |
|
||||
grep -v .md |
|
||||
grep -v examples/Makefile.toml |
|
||||
grep -v examples/cargo-make |
|
||||
grep -v examples/gtk |
|
||||
examples=$(ls -1d examples/*/ |
|
||||
grep -vE "($EXCLUDED_EXAMPLES)" |
|
||||
sed 's/\/$//' |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "Example Directories: $examples"
|
||||
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Print Location Info
|
||||
run: |
|
||||
echo "Workspace: ${{ github.workspace }}"
|
||||
|
||||
39
.github/workflows/get-leptos-changed.yml
vendored
39
.github/workflows/get-leptos-changed.yml
vendored
@@ -1,12 +1,10 @@
|
||||
name: Get Leptos Changed Call
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
leptos_changed:
|
||||
description: "Leptos Changed"
|
||||
value: ${{ jobs.create.outputs.leptos_changed }}
|
||||
|
||||
jobs:
|
||||
create:
|
||||
name: Detect Source Change
|
||||
@@ -18,40 +16,19 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get source files that changed
|
||||
id: changed-source
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
any_error/**
|
||||
any_spawner/**
|
||||
const_str_slice_concat/**
|
||||
either_of/**
|
||||
hydration_context/**
|
||||
integrations/actix/**
|
||||
integrations/axum/**
|
||||
integrations/utils/**
|
||||
leptos/**
|
||||
leptos_config/**
|
||||
leptos_dom/**
|
||||
leptos_hot_reload/**
|
||||
leptos_macro/**
|
||||
leptos_server/**
|
||||
meta/**
|
||||
next_tuple/**
|
||||
oco/**
|
||||
or_poisoned/**
|
||||
reactive_graph/**
|
||||
router/**
|
||||
router_macro/**
|
||||
server_fn/**
|
||||
server_fn/server_fn_macro_default/**
|
||||
server_fn_macro/**
|
||||
|
||||
files_ignore: |
|
||||
.*/**/*
|
||||
cargo-make/**/*
|
||||
examples/**/*
|
||||
projects/**/*
|
||||
benchmarks/**/*
|
||||
docs/**/*
|
||||
- name: List source files that changed
|
||||
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
|
||||
|
||||
- name: Set leptos_changed
|
||||
id: set-source-changed
|
||||
run: |
|
||||
|
||||
32
.github/workflows/get-leptos-matrix.yml
vendored
Normal file
32
.github/workflows/get-leptos-matrix.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Get Leptos Matrix Call
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
matrix:
|
||||
description: "Matrix"
|
||||
value: ${{ jobs.create.outputs.matrix }}
|
||||
jobs:
|
||||
create:
|
||||
name: Create Leptos Matrix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install jq
|
||||
run: sudo apt-get install jq
|
||||
- name: Set Matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
crates=$(cargo metadata --no-deps --quiet --format-version 1 |
|
||||
jq -r '.packages[] | select(.name != "workspace") | .manifest_path| rtrimstr("/Cargo.toml")' |
|
||||
sed "s|$(pwd)/||" |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "Leptos Directories: $crates"
|
||||
echo "matrix={\"directory\":$crates}" >> "$GITHUB_OUTPUT"
|
||||
- name: Print Location Info
|
||||
run: |
|
||||
echo "Workspace: ${{ github.workspace }}"
|
||||
pwd
|
||||
ls | sort -u
|
||||
28
.github/workflows/run-cargo-make-task.yml
vendored
28
.github/workflows/run-cargo-make-task.yml
vendored
@@ -1,5 +1,4 @@
|
||||
name: Run Task
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
@@ -12,70 +11,53 @@ on:
|
||||
toolchain:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Setup environment
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ inputs.toolchain }}
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install binstall
|
||||
uses: cargo-bins/cargo-binstall@main
|
||||
|
||||
- name: Install wasm-bindgen
|
||||
run: cargo binstall wasm-bindgen-cli --no-confirm
|
||||
|
||||
- name: Install cargo-leptos
|
||||
run: cargo binstall cargo-leptos --no-confirm
|
||||
|
||||
- name: Install Trunk
|
||||
uses: jetli/trunk-action@v0.5.0
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Print Trunk Version
|
||||
run: trunk --version
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
@@ -83,7 +65,6 @@ jobs:
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Maybe install chromedriver
|
||||
run: |
|
||||
project_makefile=${{inputs.directory}}/Makefile.toml
|
||||
@@ -99,7 +80,6 @@ jobs:
|
||||
else
|
||||
echo chromedriver is not required
|
||||
fi
|
||||
|
||||
- name: Maybe install playwright browser dependencies
|
||||
run: |
|
||||
for pw_path in $(find ${{inputs.directory}} -name playwright.config.ts)
|
||||
@@ -113,12 +93,16 @@ jobs:
|
||||
echo Playwright is not required
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Install Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v1.x
|
||||
|
||||
- name: Maybe install gtk-rs dependencies
|
||||
run: |
|
||||
if [ ! -z $(echo ${{inputs.directory}} | grep gtk) ]; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev libgio2.0-cil-dev libgraphene-1.0-dev libcairo2-dev libpango1.0-dev libgtk-4-dev
|
||||
fi
|
||||
# Run Cargo Make Task
|
||||
- name: ${{ inputs.cargo_make_task }}
|
||||
run: |
|
||||
|
||||
42
Cargo.toml
42
Cargo.toml
@@ -40,36 +40,36 @@ members = [
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.7.0-beta2"
|
||||
version = "0.7.0-beta6"
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
throw_error = { path = "./any_error/", version = "0.2.0-beta2" }
|
||||
throw_error = { path = "./any_error/", version = "0.2.0-beta6" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.1.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1.0" }
|
||||
either_of = { path = "./either_of/", version = "0.1.0" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0-beta2" }
|
||||
leptos = { path = "./leptos", version = "0.7.0-beta2" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.0-beta2" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta2" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta2" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta2" }
|
||||
leptos_router = { path = "./router", version = "0.7.0-beta2" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta2" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.0-beta2" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.0-beta2" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0-beta2" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0-beta6" }
|
||||
leptos = { path = "./leptos", version = "0.7.0-beta6" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.0-beta6" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta6" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta6" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta6" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta6" }
|
||||
leptos_router = { path = "./router", version = "0.7.0-beta6" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta6" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.0-beta6" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.0-beta6" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0-beta6" }
|
||||
oco_ref = { path = "./oco", version = "0.2.0" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.0-beta2" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta2" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta2" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.0-beta2" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-beta2" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-beta2" }
|
||||
tachys = { path = "./tachys", version = "0.1.0-beta2" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.0-beta6" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta6" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta6" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.0-beta6" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-beta6" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-beta6" }
|
||||
tachys = { path = "./tachys", version = "0.1.0-beta6" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "throw_error"
|
||||
version = "0.2.0-beta2"
|
||||
version = "0.2.0-beta6"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -28,3 +28,6 @@ futures-executor = ["futures/thread-pool", "futures/executor"]
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing"]
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
name = "benchmarks"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version.workspace = true
|
||||
# std::sync::LazyLock is stabilized in Rust version 1.80.0
|
||||
rust-version = "1.80.0"
|
||||
|
||||
[dependencies]
|
||||
l0410 = { package = "leptos", version = "0.4.10", features = [
|
||||
|
||||
@@ -18,7 +18,7 @@ fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
}
|
||||
}
|
||||
|
||||
let rendered = view! {
|
||||
let rendered = view! {
|
||||
<main>
|
||||
<h1>"Welcome to our benchmark page."</h1>
|
||||
<p>"Here's some introductory text."</p>
|
||||
@@ -58,7 +58,7 @@ fn tachys_ssr_bench(b: &mut Bencher) {
|
||||
}
|
||||
}
|
||||
|
||||
let rendered = view! {
|
||||
let rendered = view! {
|
||||
<main>
|
||||
<h1>"Welcome to our benchmark page."</h1>
|
||||
<p>"Here's some introductory text."</p>
|
||||
@@ -92,13 +92,13 @@ fn tera_ssr_bench(b: &mut Bencher) {
|
||||
{% endfor %}
|
||||
</main>"#;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref TERA: Tera = {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
|
||||
tera
|
||||
};
|
||||
}
|
||||
|
||||
static LazyCell<TERA>: Tera = LazyLock::new(|| {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
|
||||
tera
|
||||
});
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Counter {
|
||||
|
||||
@@ -55,7 +55,7 @@ static TEMPLATE: &str = r#"<main>
|
||||
{% else %}
|
||||
<li><a href="/">All</a></li>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if mode_active %}
|
||||
<li><a href="/active" class="selected">Active</a></li>
|
||||
{% else %}
|
||||
@@ -91,13 +91,13 @@ fn tera_todomvc_ssr(b: &mut Bencher) {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::*;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref TERA: Tera = {
|
||||
|
||||
static LazyLock<TERA>: Tera = LazyLock( || {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
|
||||
tera
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Todo {
|
||||
@@ -131,13 +131,13 @@ fn tera_todomvc_ssr_1000(b: &mut Bencher) {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::*;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref TERA: Tera = {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
|
||||
tera
|
||||
};
|
||||
}
|
||||
|
||||
static TERA: LazyLock<Tera> = LazyLock::new(|| {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
|
||||
tera
|
||||
});
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Todo {
|
||||
|
||||
@@ -133,3 +133,104 @@ tuples!(EitherOf13 + EitherOf13Future + EitherOf13FutureProj => A, B, C, D, E, F
|
||||
tuples!(EitherOf14 + EitherOf14Future + EitherOf14FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N);
|
||||
tuples!(EitherOf15 + EitherOf15Future + EitherOf15FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
|
||||
tuples!(EitherOf16 + EitherOf16Future + EitherOf16FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
|
||||
|
||||
/// Matches over the first expression and returns an either ([`Either`], [`EitherOf3`], ... [`EitherOf6`])
|
||||
/// composed of the values returned by the match arms.
|
||||
///
|
||||
/// The pattern syntax is exactly the same as found in a match arm.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use either_of::*;
|
||||
/// let either2 = either!(Some("hello"),
|
||||
/// Some(s) => s.len(),
|
||||
/// None => 0.0,
|
||||
/// );
|
||||
/// assert!(matches!(either2, Either::<usize, f64>::Left(5)));
|
||||
///
|
||||
/// let either3 = either!(Some("admin"),
|
||||
/// Some("admin") => "hello admin",
|
||||
/// Some(_) => 'x',
|
||||
/// _ => 0,
|
||||
/// );
|
||||
/// assert!(matches!(either3, EitherOf3::<&str, char, i32>::A("hello admin")));
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! either {
|
||||
($match:expr, $left_pattern:pat => $left_expression:expr, $right_pattern:pat => $right_expression:expr,) => {
|
||||
match $match {
|
||||
$left_pattern => $crate::Either::Left($left_expression),
|
||||
$right_pattern => $crate::Either::Right($right_expression),
|
||||
}
|
||||
};
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr,) => {
|
||||
match $match {
|
||||
$a_pattern => $crate::EitherOf3::A($a_expression),
|
||||
$b_pattern => $crate::EitherOf3::B($b_expression),
|
||||
$c_pattern => $crate::EitherOf3::C($c_expression),
|
||||
}
|
||||
};
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr,) => {
|
||||
match $match {
|
||||
$a_pattern => $crate::EitherOf4::A($a_expression),
|
||||
$b_pattern => $crate::EitherOf4::B($b_expression),
|
||||
$c_pattern => $crate::EitherOf4::C($c_expression),
|
||||
$d_pattern => $crate::EitherOf4::D($d_expression),
|
||||
}
|
||||
};
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr,) => {
|
||||
match $match {
|
||||
$a_pattern => $crate::EitherOf5::A($a_expression),
|
||||
$b_pattern => $crate::EitherOf5::B($b_expression),
|
||||
$c_pattern => $crate::EitherOf5::C($c_expression),
|
||||
$d_pattern => $crate::EitherOf5::D($d_expression),
|
||||
$e_pattern => $crate::EitherOf5::E($e_expression),
|
||||
}
|
||||
};
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr,) => {
|
||||
match $match {
|
||||
$a_pattern => $crate::EitherOf6::A($a_expression),
|
||||
$b_pattern => $crate::EitherOf6::B($b_expression),
|
||||
$c_pattern => $crate::EitherOf6::C($c_expression),
|
||||
$d_pattern => $crate::EitherOf6::D($d_expression),
|
||||
$e_pattern => $crate::EitherOf6::E($e_expression),
|
||||
$f_pattern => $crate::EitherOf6::F($f_expression),
|
||||
}
|
||||
}; // if you need more eithers feel free to open a PR ;-)
|
||||
}
|
||||
|
||||
// compile time test
|
||||
#[test]
|
||||
fn either_macro() {
|
||||
let _: Either<&str, f64> = either!(12,
|
||||
12 => "12",
|
||||
_ => 0.0,
|
||||
);
|
||||
let _: EitherOf3<&str, f64, i32> = either!(12,
|
||||
12 => "12",
|
||||
13 => 0.0,
|
||||
_ => 12,
|
||||
);
|
||||
let _: EitherOf4<&str, f64, char, i32> = either!(12,
|
||||
12 => "12",
|
||||
13 => 0.0,
|
||||
14 => ' ',
|
||||
_ => 12,
|
||||
);
|
||||
let _: EitherOf5<&str, f64, char, f32, i32> = either!(12,
|
||||
12 => "12",
|
||||
13 => 0.0,
|
||||
14 => ' ',
|
||||
15 => 0.0f32,
|
||||
_ => 12,
|
||||
);
|
||||
let _: EitherOf6<&str, f64, char, f32, u8, i32> = either!(12,
|
||||
12 => "12",
|
||||
13 => 0.0,
|
||||
14 => ' ',
|
||||
15 => 0.0f32,
|
||||
16 => 24u8,
|
||||
_ => 12,
|
||||
);
|
||||
}
|
||||
|
||||
111
examples/axum_js_ssr/Cargo.toml
Normal file
111
examples/axum_js_ssr/Cargo.toml
Normal file
@@ -0,0 +1,111 @@
|
||||
[package]
|
||||
name = "axum_js_ssr"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1.0"
|
||||
gloo-utils = "0.2.0"
|
||||
html-escape = "0.2.13"
|
||||
http-body-util = { version = "0.1.0", optional = true }
|
||||
js-sys = { version = "0.3.69", optional = true }
|
||||
leptos = { path = "../../leptos", features = ["tracing"] }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "0.2.92"
|
||||
web-sys = { version = "0.3.69", features = [ "AddEventListenerOptions", "Document", "Element", "Event", "EventListener", "EventTarget", "Performance", "Window" ], optional = true }
|
||||
|
||||
[features]
|
||||
hydrate = [
|
||||
"leptos/hydrate",
|
||||
"dep:js-sys",
|
||||
"dep:web-sys",
|
||||
]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:http-body-util",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"dep:leptos_axum",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
opt-level = 'z'
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "axum_js_ssr"
|
||||
# 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/main.scss"
|
||||
# Assets source dir. All files found here will be copied and synchronized to site-root.
|
||||
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
|
||||
#
|
||||
# Optional. Env: LEPTOS_ASSETS_DIR.
|
||||
assets-dir = "assets"
|
||||
# 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.
|
||||
# [Windows] for non-WSL use "npx.cmd playwright test"
|
||||
# This binary name can be checked in Powershell with Get-Command npx
|
||||
end2end-cmd = "npx playwright test"
|
||||
end2end-dir = "end2end"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = ["ssr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
lib-features = ["hydrate"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
|
||||
lib-profile-release = "wasm-release"
|
||||
21
examples/axum_js_ssr/LICENSE
Normal file
21
examples/axum_js_ssr/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Tommy Yu
|
||||
|
||||
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.
|
||||
8
examples/axum_js_ssr/Makefile.toml
Normal file
8
examples/axum_js_ssr/Makefile.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
|
||||
CLIENT_PROCESS_NAME = "axum_js_ssr"
|
||||
10
examples/axum_js_ssr/README.md
Normal file
10
examples/axum_js_ssr/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Leptos Axum JS SSR Example
|
||||
|
||||
This example shows the various ways that JavaScript may be included into
|
||||
a Leptos application. The intent is to demonstrate how this may be done
|
||||
and how it may cause the application to fail in an unexpected manner if
|
||||
done incorrectly.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run `cargo leptos watch` to run this example.
|
||||
BIN
examples/axum_js_ssr/assets/favicon.ico
Normal file
BIN
examples/axum_js_ssr/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
29
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/LICENSE
generated
vendored
Normal file
29
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2006, Ivan Sagalaev.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
47
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/README.md
generated
vendored
Normal file
47
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/README.md
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
# Highlight.js CDN Assets
|
||||
|
||||
**Note: this contains only a subset of files from the full package from NPM.**
|
||||
|
||||
[](https://packagephobia.now.sh/result?p=highlight.js)
|
||||
|
||||
**This package contains only the CDN build assets of highlight.js.**
|
||||
|
||||
This may be what you want if you'd like to install the pre-built distributable highlight.js client-side assets via NPM. If you're wanting to use highlight.js mainly on the server-side you likely want the [highlight.js][1] package instead.
|
||||
|
||||
To access these files via CDN:<br>
|
||||
https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@latest/build/
|
||||
|
||||
**If you just want a single .js file with the common languages built-in:
|
||||
<https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@latest/build/highlight.min.js>**
|
||||
|
||||
---
|
||||
|
||||
## Highlight.js
|
||||
|
||||
Highlight.js is a syntax highlighter written in JavaScript. It works in
|
||||
the browser as well as on the server. It works with pretty much any
|
||||
markup, doesn’t depend on any framework, and has automatic language
|
||||
detection.
|
||||
|
||||
If you'd like to read the full README:<br>
|
||||
<https://github.com/highlightjs/highlight.js/blob/main/README.md>
|
||||
|
||||
## License
|
||||
|
||||
Highlight.js is released under the BSD License. See [LICENSE][7] file
|
||||
for details.
|
||||
|
||||
## Links
|
||||
|
||||
The official site for the library is at <https://highlightjs.org/>.
|
||||
|
||||
The Github project may be found at: <https://github.com/highlightjs/highlight.js>
|
||||
|
||||
Further in-depth documentation for the API and other topics is at
|
||||
<http://highlightjs.readthedocs.io/>.
|
||||
|
||||
A list of the Core Team and contributors can be found in the [CONTRIBUTORS.md][8] file.
|
||||
|
||||
[1]: https://www.npmjs.com/package/highlight.js
|
||||
[7]: https://github.com/highlightjs/highlight.js/blob/main/LICENSE
|
||||
[8]: https://github.com/highlightjs/highlight.js/blob/main/CONTRIBUTORS.md
|
||||
1230
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/es/highlight.min.js
generated
vendored
Normal file
1230
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/es/highlight.min.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1232
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/highlight.min.js
generated
vendored
Normal file
1232
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/highlight.min.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
93
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/package.json
generated
vendored
Normal file
93
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/package.json
generated
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"name": "@highlightjs/cdn-assets",
|
||||
"description": "Syntax highlighting with language autodetection. (pre-compiled CDN assets)",
|
||||
"keywords": [
|
||||
"highlight",
|
||||
"syntax"
|
||||
],
|
||||
"homepage": "https://highlightjs.org/",
|
||||
"version": "11.10.0",
|
||||
"author": "Josh Goebel <hello@joshgoebel.com>",
|
||||
"contributors": [
|
||||
"Josh Goebel <hello@joshgoebel.com>",
|
||||
"Egor Rogov <e.rogov@postgrespro.ru>",
|
||||
"Vladimir Jimenez <me@allejo.io>",
|
||||
"Ivan Sagalaev <maniac@softwaremaniacs.org>",
|
||||
"Jeremy Hull <sourdrums@gmail.com>",
|
||||
"Oleg Efimov <efimovov@gmail.com>",
|
||||
"Gidi Meir Morris <gidi@gidi.io>",
|
||||
"Jan T. Sott <git@idleberg.com>",
|
||||
"Li Xuanji <xuanji@gmail.com>",
|
||||
"Marcos Cáceres <marcos@marcosc.com>",
|
||||
"Sang Dang <sang.dang@polku.io>"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/highlightjs/highlight.js/issues"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/highlightjs/highlight.js.git"
|
||||
},
|
||||
"sideEffects": [
|
||||
"./es/common.js",
|
||||
"./lib/common.js",
|
||||
"*.css",
|
||||
"*.scss"
|
||||
],
|
||||
"scripts": {
|
||||
"mocha": "mocha",
|
||||
"lint": "eslint src/*.js src/lib/*.js demo/*.js tools/**/*.js --ignore-pattern vendor",
|
||||
"lint-languages": "eslint --no-eslintrc -c .eslintrc.lang.js src/languages/**/*.js",
|
||||
"build_and_test": "npm run build && npm run test",
|
||||
"build_and_test_browser": "npm run build-browser && npm run test-browser",
|
||||
"build": "node ./tools/build.js -t node",
|
||||
"build-cdn": "node ./tools/build.js -t cdn",
|
||||
"build-browser": "node ./tools/build.js -t browser :common",
|
||||
"devtool": "npx http-server",
|
||||
"test": "mocha test",
|
||||
"test-markup": "mocha test/markup",
|
||||
"test-detect": "mocha test/detect",
|
||||
"test-browser": "mocha test/browser",
|
||||
"test-parser": "mocha test/parser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@rollup/plugin-commonjs": "^26.0.1",
|
||||
"@rollup/plugin-json": "^6.0.1",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@types/mocha": "^10.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
||||
"@typescript-eslint/parser": "^7.15.0",
|
||||
"clean-css": "^5.3.2",
|
||||
"cli-table": "^0.3.1",
|
||||
"commander": "^12.1.0",
|
||||
"css": "^3.0.0",
|
||||
"css-color-names": "^1.0.1",
|
||||
"deep-freeze-es6": "^3.0.2",
|
||||
"del": "^7.1.0",
|
||||
"dependency-resolver": "^2.0.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"glob": "^8.1.0",
|
||||
"glob-promise": "^6.0.5",
|
||||
"handlebars": "^4.7.8",
|
||||
"http-server": "^14.1.1",
|
||||
"jsdom": "^24.1.0",
|
||||
"lodash": "^4.17.20",
|
||||
"mocha": "^10.2.0",
|
||||
"refa": "^0.4.1",
|
||||
"rollup": "^4.0.2",
|
||||
"should": "^13.2.3",
|
||||
"terser": "^5.21.0",
|
||||
"tiny-worker": "^2.3.0",
|
||||
"typescript": "^5.2.2",
|
||||
"wcag-contrast": "^3.0.0"
|
||||
}
|
||||
}
|
||||
10
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/styles/github-dark.min.css
generated
vendored
Normal file
10
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/styles/github-dark.min.css
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: GitHub Dark
|
||||
Description: Dark theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-dark
|
||||
Current colors taken from GitHub's CSS
|
||||
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
|
||||
10
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/styles/github.min.css
generated
vendored
Normal file
10
examples/axum_js_ssr/node_modules/@highlightjs/cdn-assets/styles/github.min.css
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: GitHub
|
||||
Description: Light theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-light
|
||||
Current colors taken from GitHub's CSS
|
||||
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}
|
||||
6
examples/axum_js_ssr/package.json
Normal file
6
examples/axum_js_ssr/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "axum_js_ssr",
|
||||
"dependencies": {
|
||||
"@highlightjs/cdn-assets": "^11.10.0"
|
||||
}
|
||||
}
|
||||
2
examples/axum_js_ssr/rust-toolchain.toml
Normal file
2
examples/axum_js_ssr/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "stable" # test change
|
||||
8
examples/axum_js_ssr/src/api.rs
Normal file
8
examples/axum_js_ssr/src/api.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use leptos::{prelude::ServerFnError, server};
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_code() -> Result<String, ServerFnError> {
|
||||
// emulate loading of code from a database/version control/etc
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
Ok(crate::consts::CH05_02A.to_string())
|
||||
}
|
||||
1133
examples/axum_js_ssr/src/app.rs
Normal file
1133
examples/axum_js_ssr/src/app.rs
Normal file
File diff suppressed because it is too large
Load Diff
39
examples/axum_js_ssr/src/consts.rs
Normal file
39
examples/axum_js_ssr/src/consts.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
// Example programs from the Rust Programming Language Book
|
||||
|
||||
pub const CH03_05A: &str = r#"fn main() {
|
||||
let number = 3;
|
||||
|
||||
if number < 5 {
|
||||
println!("condition was true");
|
||||
} else {
|
||||
println!("condition was false");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
"#;
|
||||
|
||||
// For some reason, swapping the code examples "fixes" example 6. It
|
||||
// might have something to do with the lower complexity of highlighting
|
||||
// a shorter example. Anyway, including extra newlines for the shorter
|
||||
// example to match with the longer in order to avoid reflowing the
|
||||
// table during the async resource loading for CSR.
|
||||
|
||||
pub const CH05_02A: &str = r#"fn main() {
|
||||
let width1 = 30;
|
||||
let height1 = 50;
|
||||
|
||||
println!(
|
||||
"The area of the rectangle is {} square pixels.",
|
||||
area(width1, height1)
|
||||
);
|
||||
}
|
||||
|
||||
fn area(width: u32, height: u32) -> u32 {
|
||||
width * height
|
||||
}
|
||||
"#;
|
||||
|
||||
pub const LEPTOS_HYDRATED: &str = "_leptos_hydrated";
|
||||
59
examples/axum_js_ssr/src/hljs.rs
Normal file
59
examples/axum_js_ssr/src/hljs.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
mod csr {
|
||||
use gloo_utils::format::JsValueSerdeExt;
|
||||
use js_sys::{
|
||||
Object,
|
||||
Reflect::{get, set},
|
||||
};
|
||||
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
|
||||
|
||||
#[wasm_bindgen(
|
||||
module = "/node_modules/@highlightjs/cdn-assets/es/highlight.min.js"
|
||||
)]
|
||||
extern "C" {
|
||||
type HighlightOptions;
|
||||
|
||||
#[wasm_bindgen(catch, js_namespace = default, js_name = highlight)]
|
||||
fn highlight_lang(
|
||||
code: String,
|
||||
options: Object,
|
||||
) -> Result<Object, JsValue>;
|
||||
|
||||
#[wasm_bindgen(js_namespace = default, js_name = highlightAll)]
|
||||
pub fn highlight_all();
|
||||
}
|
||||
|
||||
// Keeping the `ignoreIllegals` argument out of the default case, and since there is no optional arguments
|
||||
// in Rust, this will have to be provided in a separate function (e.g. `highlight_ignore_illegals`), much
|
||||
// like how `web_sys` does it for the browser APIs. For simplicity, only the highlighted HTML code is
|
||||
// returned on success, and None on error.
|
||||
pub fn highlight(code: String, lang: String) -> Option<String> {
|
||||
let options = js_sys::Object::new();
|
||||
set(&options, &"language".into(), &lang.into())
|
||||
.expect("failed to assign lang to options");
|
||||
highlight_lang(code, options)
|
||||
.map(|result| {
|
||||
let value = get(&result, &"value".into())
|
||||
.expect("HighlightResult failed to contain the value key");
|
||||
value.into_serde().expect("Value should have been a string")
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr {
|
||||
// noop under ssr
|
||||
pub fn highlight_all() {}
|
||||
|
||||
// TODO see if there is a Rust-based solution that will enable isomorphic rendering for this feature.
|
||||
// the current (disabled) implementation simply calls html_escape.
|
||||
// pub fn highlight(code: String, _lang: String) -> Option<String> {
|
||||
// Some(html_escape::encode_text(&code).into_owned())
|
||||
// }
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub use csr::*;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use ssr::*;
|
||||
51
examples/axum_js_ssr/src/lib.rs
Normal file
51
examples/axum_js_ssr/src/lib.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
pub mod api;
|
||||
pub mod app;
|
||||
pub mod consts;
|
||||
pub mod hljs;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
use consts::LEPTOS_HYDRATED;
|
||||
use std::panic;
|
||||
panic::set_hook(Box::new(|info| {
|
||||
// this custom hook will call out to show the usual error log at
|
||||
// the console while also attempt to update the UI to indicate
|
||||
// a restart of the application is required to continue.
|
||||
console_error_panic_hook::hook(info);
|
||||
let window = leptos::prelude::window();
|
||||
if !matches!(
|
||||
js_sys::Reflect::get(&window, &wasm_bindgen::JsValue::from_str(LEPTOS_HYDRATED)),
|
||||
Ok(t) if t == true
|
||||
) {
|
||||
let document = leptos::prelude::document();
|
||||
let _ = document.query_selector("#reset").map(|el| {
|
||||
el.map(|el| {
|
||||
el.set_class_name("panicked");
|
||||
})
|
||||
});
|
||||
let _ = document.query_selector("#notice").map(|el| {
|
||||
el.map(|el| {
|
||||
el.set_class_name("panicked");
|
||||
})
|
||||
});
|
||||
}
|
||||
}));
|
||||
leptos::mount::hydrate_body(App);
|
||||
|
||||
let window = leptos::prelude::window();
|
||||
js_sys::Reflect::set(
|
||||
&window,
|
||||
&wasm_bindgen::JsValue::from_str(LEPTOS_HYDRATED),
|
||||
&wasm_bindgen::JsValue::TRUE,
|
||||
)
|
||||
.expect("error setting hydrated status");
|
||||
let event = web_sys::Event::new(LEPTOS_HYDRATED)
|
||||
.expect("error creating hydrated event");
|
||||
let document = leptos::prelude::document();
|
||||
document
|
||||
.dispatch_event(&event)
|
||||
.expect("error dispatching hydrated event");
|
||||
leptos::logging::log!("dispatched hydrated event");
|
||||
}
|
||||
152
examples/axum_js_ssr/src/main.rs
Normal file
152
examples/axum_js_ssr/src/main.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
mod latency {
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
pub static LATENCY: OnceLock<
|
||||
Mutex<std::iter::Cycle<std::slice::Iter<'_, u64>>>,
|
||||
> = OnceLock::new();
|
||||
pub static ES_LATENCY: OnceLock<
|
||||
Mutex<std::iter::Cycle<std::slice::Iter<'_, u64>>>,
|
||||
> = OnceLock::new();
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::Request,
|
||||
http::{
|
||||
header::{self, HeaderValue},
|
||||
StatusCode,
|
||||
},
|
||||
middleware::{self, Next},
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum_js_ssr::app::*;
|
||||
use http_body_util::BodyExt;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
|
||||
latency::LATENCY.get_or_init(|| [0, 4, 40, 400].iter().cycle().into());
|
||||
latency::ES_LATENCY.get_or_init(|| [0].iter().cycle().into());
|
||||
// Having the ES_LATENCY (a cycle of latency for the loading of the es
|
||||
// module) in an identical cycle as LATENCY (for the standard version)
|
||||
// adversely influences the intended demo, as this ultimately delays
|
||||
// hydration when set too high which can cause panic under every case.
|
||||
// If you want to test the effects of the delay just modify the list of
|
||||
// values for the desired cycle of delays.
|
||||
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let leptos_options = conf.leptos_options;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
async fn highlight_js() -> impl IntoResponse {
|
||||
(
|
||||
[(header::CONTENT_TYPE, "text/javascript")],
|
||||
include_str!(
|
||||
"../node_modules/@highlightjs/cdn-assets/highlight.min.js"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
async fn latency_for_highlight_js(
|
||||
req: Request,
|
||||
next: Next,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let uri_parts = &mut req.uri().path().rsplit('/');
|
||||
|
||||
let is_highlightjs = uri_parts.next() == Some("highlight.min.js");
|
||||
let es = uri_parts.next() == Some("es");
|
||||
let module_type = if es { "es module " } else { "standard " };
|
||||
let res = next.run(req).await;
|
||||
if is_highlightjs {
|
||||
// additional processing if the filename is the test subject
|
||||
let (mut parts, body) = res.into_parts();
|
||||
let bytes = body
|
||||
.collect()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("error reading body: {err}"),
|
||||
)
|
||||
})?
|
||||
.to_bytes();
|
||||
let latency = if es {
|
||||
&latency::ES_LATENCY
|
||||
} else {
|
||||
&latency::LATENCY
|
||||
};
|
||||
|
||||
let delay = match latency
|
||||
.get()
|
||||
.expect("latency cycle wasn't set up")
|
||||
.try_lock()
|
||||
{
|
||||
Ok(ref mut mutex) => {
|
||||
*mutex.next().expect("cycle always has next")
|
||||
}
|
||||
Err(_) => 0,
|
||||
};
|
||||
|
||||
// inject the logging of the delay used into the target script
|
||||
log!(
|
||||
"loading {module_type}highlight.min.js with latency of \
|
||||
{delay} ms"
|
||||
);
|
||||
let js_log = format!(
|
||||
"\nconsole.log('loaded {module_type}highlight.js with a \
|
||||
minimum latency of {delay} ms');"
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
|
||||
|
||||
let bytes = [bytes, js_log.into()].concat();
|
||||
let length = bytes.len();
|
||||
let body = Body::from(bytes);
|
||||
|
||||
// Provide the bare minimum set of headers to avoid browser cache.
|
||||
parts.headers = header::HeaderMap::from_iter(
|
||||
[
|
||||
(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("text/javascript"),
|
||||
),
|
||||
(header::CONTENT_LENGTH, HeaderValue::from(length)),
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
Ok(Response::from_parts(parts, body))
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
let app = Router::new()
|
||||
.route("/highlight.min.js", get(highlight_js))
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
})
|
||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||
.layer(middleware::from_fn(latency_for_highlight_js))
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
// no client-side main function
|
||||
// unless we want this to work with e.g., Trunk for pure client-side testing
|
||||
// see lib.rs for hydration function instead
|
||||
}
|
||||
171
examples/axum_js_ssr/style/main.scss
Normal file
171
examples/axum_js_ssr/style/main.scss
Normal file
@@ -0,0 +1,171 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
nav {
|
||||
min-width: 17em;
|
||||
height: 100vh;
|
||||
counter-reset: example-counter 0;
|
||||
list-style-type: none;
|
||||
list-style-position: outside;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: block;
|
||||
padding: 0.5em 2em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav a small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
nav a.example::before {
|
||||
counter-reset: subexample-counter 0;
|
||||
counter-increment: example-counter 1;
|
||||
content: counter(example-counter) ". ";
|
||||
}
|
||||
|
||||
nav a.subexample::before {
|
||||
counter-increment: subexample-counter 1;
|
||||
content: counter(example-counter) "." counter(subexample-counter) " ";
|
||||
}
|
||||
|
||||
div#notice {
|
||||
display: none;
|
||||
}
|
||||
|
||||
main div#notice.panicked {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 0.5em 2em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
main article {
|
||||
max-width: 60em;
|
||||
margin: 0 1em;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
main p, main li {
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
main li pre code, main div pre code {
|
||||
display: block;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
main ol, main ul {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
h2>code, p>code, li>code {
|
||||
border-radius: 3px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
li pre code, div pre code {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#code-demo {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
#code-demo table {
|
||||
width: 50em;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#code-demo table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#code-demo table code {
|
||||
display: block;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
nav {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
nav a[aria-current="page"] {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
nav a:hover, h2>code, p>code, li>code {
|
||||
background-color: #e7e7e7;
|
||||
}
|
||||
|
||||
nav a.panicked, main div#notice.panicked {
|
||||
background: #fdd;
|
||||
}
|
||||
|
||||
main div#notice.panicked a {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
nav a.section {
|
||||
border-bottom: 1px solid #777;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
nav {
|
||||
background: #080808;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
nav a[aria-current="page"] {
|
||||
background-color: #3f3f3f;
|
||||
}
|
||||
|
||||
nav a:hover, h2>code, p>code, li>code {
|
||||
background-color: #383838;
|
||||
}
|
||||
|
||||
nav a.panicked, main div#notice.panicked {
|
||||
background: #733;
|
||||
}
|
||||
|
||||
main div#notice.panicked a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
nav a.section {
|
||||
border-bottom: 1px solid #888;
|
||||
}
|
||||
}
|
||||
|
||||
// Just include the raw style as-is because I can't find a quick and easy way to import them just for the
|
||||
// appropriate media type...
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}
|
||||
@media (prefers-color-scheme: light){.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}}
|
||||
@media (prefers-color-scheme: dark){.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}}
|
||||
@@ -2,6 +2,8 @@
|
||||
name = "counter_isomorphic"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
# std::sync::LazyLock is stabilized in Rust version 1.80.0
|
||||
rust-version = "1.80.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
@@ -17,7 +19,6 @@ broadcaster = "1.0"
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
lazy_static = "1.5"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
@@ -46,13 +47,13 @@ denylist = ["actix-files", "actix-web", "leptos_actix"]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "counter_isomorphic"
|
||||
# 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.
|
||||
# When NOT using cargo-leptos this must be updated to "." or the counters will not work. The above warning still applies if you do switch to cargo-leptos later.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
# 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 = "src/styles/tailwind.css"
|
||||
|
||||
@@ -10,12 +10,12 @@ use tracing::instrument;
|
||||
pub mod ssr_imports {
|
||||
pub use broadcaster::BroadcastChannel;
|
||||
pub use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub static COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
|
||||
}
|
||||
pub static COUNT_CHANNEL: LazyLock<BroadcastChannel<i32>> =
|
||||
LazyLock::new(BroadcastChannel::<i32>::new);
|
||||
}
|
||||
|
||||
#[server]
|
||||
|
||||
6
examples/counters/.gitignore
vendored
Normal file
6
examples/counters/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Support playwright testing
|
||||
node_modules/
|
||||
test-results/
|
||||
end2end/playwright-report/
|
||||
playwright/.cache/
|
||||
pnpm-lock.yaml
|
||||
@@ -2,4 +2,5 @@ extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
{ path = "../cargo-make/playwright-trunk-test.toml" },
|
||||
]
|
||||
|
||||
4
examples/counters/e2e/.gitignore
vendored
Normal file
4
examples/counters/e2e/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
83
examples/counters/e2e/package-lock.json
generated
Normal file
83
examples/counters/e2e/package-lock.json
generated
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "grip",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "grip",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1"
|
||||
}
|
||||
},
|
||||
"node_modules/.pnpm/@playwright+test@1.33.0": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/.pnpm/@types+node@20.2.1/node_modules/@types/node": {
|
||||
"version": "20.2.1",
|
||||
"extraneous": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/.pnpm/playwright-core@1.33.0/node_modules/playwright-core": {
|
||||
"version": "1.33.0",
|
||||
"extraneous": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz",
|
||||
"integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.35.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz",
|
||||
"integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz",
|
||||
"integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
examples/counters/e2e/package.json
Normal file
10
examples/counters/e2e/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"private": "true",
|
||||
"scripts": {},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.46.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"pnpm": "^9.7.1"
|
||||
}
|
||||
}
|
||||
77
examples/counters/e2e/playwright.config.ts
Normal file
77
examples/counters/e2e/playwright.config.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !process.env.DEV,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.DEV ? 0 : 10,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.DEV ? 1 : 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [["html", { open: "never" }], ["list"]],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1:8080",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: "firefox",
|
||||
// use: { ...devices["Desktop Firefox"] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: "webkit",
|
||||
// use: { ...devices["Desktop Safari"] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: "cd ../ && trunk serve",
|
||||
// url: "http://127.0.0.1:8080",
|
||||
// reuseExistingServer: false, //!process.env.CI,
|
||||
// },
|
||||
});
|
||||
19
examples/counters/e2e/tests/add_1k_counters.spec.ts
Normal file
19
examples/counters/e2e/tests/add_1k_counters.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Add 1000 Counters", () => {
|
||||
test("should increase the number of counters", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
|
||||
await Promise.all([
|
||||
await ui.goto(),
|
||||
await ui.addOneThousandCountersButton.waitFor(),
|
||||
]);
|
||||
|
||||
await ui.addOneThousandCounters();
|
||||
await ui.addOneThousandCounters();
|
||||
await ui.addOneThousandCounters();
|
||||
|
||||
await expect(ui.counters).toHaveText("3000");
|
||||
});
|
||||
});
|
||||
15
examples/counters/e2e/tests/add_counter.spec.ts
Normal file
15
examples/counters/e2e/tests/add_counter.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Add Counter", () => {
|
||||
test("should increase the number of counters", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await expect(ui.counters).toHaveText("3");
|
||||
});
|
||||
});
|
||||
18
examples/counters/e2e/tests/clear_counters.spec.ts
Normal file
18
examples/counters/e2e/tests/clear_counters.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Clear Counters", () => {
|
||||
test("should reset the counts", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.clearCounters();
|
||||
|
||||
await expect(ui.total).toHaveText("0");
|
||||
await expect(ui.counters).toHaveText("0");
|
||||
});
|
||||
});
|
||||
16
examples/counters/e2e/tests/decrement_count.spec.ts
Normal file
16
examples/counters/e2e/tests/decrement_count.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Decrement Count", () => {
|
||||
test("should decrease the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.decrementCount();
|
||||
await ui.decrementCount();
|
||||
await ui.decrementCount();
|
||||
|
||||
await expect(ui.total).toHaveText("-3");
|
||||
});
|
||||
});
|
||||
30
examples/counters/e2e/tests/enter_count.spec.ts
Normal file
30
examples/counters/e2e/tests/enter_count.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Enter Count", () => {
|
||||
test("should increase the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.enterCount("5");
|
||||
|
||||
await expect(ui.total).toHaveText("5");
|
||||
await expect(ui.counters).toHaveText("1");
|
||||
});
|
||||
|
||||
test("should decrease the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.enterCount("100");
|
||||
await ui.enterCount("100", 1);
|
||||
await ui.enterCount("100", 2);
|
||||
await ui.enterCount("50", 1);
|
||||
|
||||
await expect(ui.total).toHaveText("250");
|
||||
});
|
||||
});
|
||||
98
examples/counters/e2e/tests/fixtures/counters_page.ts
vendored
Normal file
98
examples/counters/e2e/tests/fixtures/counters_page.ts
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
import { expect, Locator, Page } from "@playwright/test";
|
||||
|
||||
export class CountersPage {
|
||||
readonly page: Page;
|
||||
readonly addCounterButton: Locator;
|
||||
readonly addOneThousandCountersButton: Locator;
|
||||
readonly clearCountersButton: Locator;
|
||||
|
||||
readonly incrementCountButton: Locator;
|
||||
readonly counterInput: Locator;
|
||||
readonly decrementCountButton: Locator;
|
||||
readonly removeCountButton: Locator;
|
||||
|
||||
readonly total: Locator;
|
||||
readonly counters: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
this.addCounterButton = page.locator("button", { hasText: "Add Counter" });
|
||||
|
||||
this.addOneThousandCountersButton = page.locator("button", {
|
||||
hasText: "Add 1000 Counters",
|
||||
});
|
||||
|
||||
this.clearCountersButton = page.locator("button", {
|
||||
hasText: "Clear Counters",
|
||||
});
|
||||
|
||||
this.decrementCountButton = page.locator("button", {
|
||||
hasText: "-1",
|
||||
});
|
||||
|
||||
this.incrementCountButton = page.locator("button", {
|
||||
hasText: "+1",
|
||||
});
|
||||
|
||||
this.removeCountButton = page.locator("button", {
|
||||
hasText: "x",
|
||||
});
|
||||
|
||||
this.total = page.getByTestId("total");
|
||||
|
||||
this.counters = page.getByTestId("counters");
|
||||
|
||||
this.counterInput = page.getByRole("textbox");
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto("/");
|
||||
}
|
||||
|
||||
async addCounter() {
|
||||
await Promise.all([
|
||||
this.addCounterButton.waitFor(),
|
||||
this.addCounterButton.click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async addOneThousandCounters() {
|
||||
this.addOneThousandCountersButton.click();
|
||||
}
|
||||
|
||||
async decrementCount(index: number = 0) {
|
||||
await Promise.all([
|
||||
this.decrementCountButton.nth(index).waitFor(),
|
||||
this.decrementCountButton.nth(index).click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async incrementCount(index: number = 0) {
|
||||
await Promise.all([
|
||||
this.incrementCountButton.nth(index).waitFor(),
|
||||
this.incrementCountButton.nth(index).click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async clearCounters() {
|
||||
await Promise.all([
|
||||
this.clearCountersButton.waitFor(),
|
||||
this.clearCountersButton.click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async enterCount(count: string, index: number = 0) {
|
||||
await Promise.all([
|
||||
this.counterInput.nth(index).waitFor(),
|
||||
this.counterInput.nth(index).fill(count),
|
||||
]);
|
||||
}
|
||||
|
||||
async removeCounter(index: number = 0) {
|
||||
await Promise.all([
|
||||
this.removeCountButton.nth(index).waitFor(),
|
||||
this.removeCountButton.nth(index).click(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
16
examples/counters/e2e/tests/increment_count.spec.ts
Normal file
16
examples/counters/e2e/tests/increment_count.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Increment Count", () => {
|
||||
test("should increase the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.incrementCount();
|
||||
await ui.incrementCount();
|
||||
await ui.incrementCount();
|
||||
|
||||
await expect(ui.total).toHaveText("3");
|
||||
});
|
||||
});
|
||||
17
examples/counters/e2e/tests/remove_counter.spec.ts
Normal file
17
examples/counters/e2e/tests/remove_counter.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Remove Counter", () => {
|
||||
test("should decrement the number of counters", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.removeCounter(1);
|
||||
|
||||
await expect(ui.counters).toHaveText("2");
|
||||
});
|
||||
});
|
||||
19
examples/counters/e2e/tests/view_counters.spec.ts
Normal file
19
examples/counters/e2e/tests/view_counters.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("View Counters", () => {
|
||||
test("should see the title", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await expect(page).toHaveTitle("Counters");
|
||||
});
|
||||
|
||||
test("should see the initial counts", async ({ page }) => {
|
||||
const counters = new CountersPage(page);
|
||||
await counters.goto();
|
||||
|
||||
await expect(counters.total).toHaveText("0");
|
||||
await expect(counters.counters).toHaveText("0");
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
|
||||
<title>Counters</title>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::prelude::{signal::*, *};
|
||||
use leptos::prelude::*;
|
||||
|
||||
const MANY_COUNTERS: usize = 1000;
|
||||
|
||||
@@ -44,12 +44,13 @@ pub fn Counters() -> impl IntoView {
|
||||
<button on:click=clear_counters>"Clear Counters"</button>
|
||||
<p>
|
||||
"Total: "
|
||||
<span>
|
||||
<span data-testid="total">
|
||||
{move || {
|
||||
counters.get().iter().map(|(_, count)| count.get()).sum::<i32>().to_string()
|
||||
}}
|
||||
|
||||
</span> " from " <span>{move || counters.get().len().to_string()}</span>
|
||||
</span> " from "
|
||||
<span data-testid="counters">{move || counters.get().len().to_string()}</span>
|
||||
" counters."
|
||||
</p>
|
||||
<ul>
|
||||
|
||||
@@ -23,7 +23,7 @@ async fn inc() {
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span>0</span> from <span>0</span> counters.</p><ul><!----></ul>"
|
||||
<span data-testid=\"total\">0</span> from <span data-testid=\"counters\">0</span> counters.</p><ul><!----></ul>"
|
||||
);
|
||||
|
||||
// add 3 counters
|
||||
@@ -38,7 +38,7 @@ async fn inc() {
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span>0</span> from <span>3</span> \
|
||||
<span data-testid=\"total\">0</span> from <span data-testid=\"counters\">3</span> \
|
||||
counters.</p><ul><li><button>-1</button><input \
|
||||
type=\"text\"><span>0</span><button>+1</button><button>x</button></\
|
||||
li><li><button>-1</button><input \
|
||||
@@ -80,7 +80,7 @@ async fn inc() {
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span>6</span> from <span>3</span> \
|
||||
<span data-testid=\"total\">6</span> from <span data-testid=\"counters\">3</span> \
|
||||
counters.</p><ul><li><button>-1</button><input \
|
||||
type=\"text\"><span>1</span><button>+1</button><button>x</button></\
|
||||
li><li><button>-1</button><input \
|
||||
@@ -105,7 +105,7 @@ async fn inc() {
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span>5</span> from <span>2</span> \
|
||||
<span data-testid=\"total\">5</span> from <span data-testid=\"counters\">2</span> \
|
||||
counters.</p><ul><li><button>-1</button><input \
|
||||
type=\"text\"><span>2</span><button>+1</button><button>x</button></\
|
||||
li><li><button>-1</button><input \
|
||||
|
||||
1
examples/gtk/Makefile.toml
Normal file
1
examples/gtk/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
@@ -1,6 +1,5 @@
|
||||
use self::properties::Connect;
|
||||
use gtk::{
|
||||
ffi::GtkWidget,
|
||||
glib::{
|
||||
object::{IsA, IsClass, ObjectExt},
|
||||
Object, Value,
|
||||
@@ -16,7 +15,7 @@ use leptos::{
|
||||
},
|
||||
};
|
||||
use next_tuple::NextTuple;
|
||||
use std::{borrow::Cow, marker::PhantomData};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LeptosGtk;
|
||||
@@ -157,13 +156,13 @@ impl Renderer for LeptosGtk {
|
||||
}
|
||||
|
||||
fn remove_node(
|
||||
parent: &Self::Element,
|
||||
child: &Self::Node,
|
||||
_parent: &Self::Element,
|
||||
_child: &Self::Node,
|
||||
) -> Option<Self::Node> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn remove(node: &Self::Node) {
|
||||
fn remove(_node: &Self::Node) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
@@ -171,19 +170,19 @@ impl Renderer for LeptosGtk {
|
||||
node.0.parent().map(Element::from)
|
||||
}
|
||||
|
||||
fn first_child(node: &Self::Node) -> Option<Self::Node> {
|
||||
fn first_child(_node: &Self::Node) -> Option<Self::Node> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn next_sibling(node: &Self::Node) -> Option<Self::Node> {
|
||||
fn next_sibling(_node: &Self::Node) -> Option<Self::Node> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn log_node(node: &Self::Node) {
|
||||
todo!()
|
||||
println!("{node:?}");
|
||||
}
|
||||
|
||||
fn clear_children(parent: &Self::Element) {
|
||||
fn clear_children(_parent: &Self::Element) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -368,7 +367,22 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
fn rebuild(self, widget: &Element, state: &mut Self::State) {}
|
||||
fn rebuild(self, widget: &Element, state: &mut Self::State) {
|
||||
let prev_value = state.take_value();
|
||||
let widget = widget.to_owned();
|
||||
*state = RenderEffect::new_with_value(
|
||||
move |prev| {
|
||||
let value = self();
|
||||
if let Some(mut state) = prev {
|
||||
value.rebuild(&widget, &mut state);
|
||||
state
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
},
|
||||
prev_value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button() -> LGtkWidget<gtk::Button, (), ()> {
|
||||
@@ -400,9 +414,9 @@ mod widgets {
|
||||
}
|
||||
|
||||
pub mod properties {
|
||||
use super::{
|
||||
Element, LGtkWidget, LGtkWidgetState, LeptosGtk, Property, WidgetClass,
|
||||
};
|
||||
#![allow(dead_code)]
|
||||
|
||||
use super::{Element, LGtkWidget, LeptosGtk, Property, WidgetClass};
|
||||
use gtk::glib::{object::ObjectExt, Value};
|
||||
use leptos::tachys::{renderer::Renderer, view::Render};
|
||||
use next_tuple::NextTuple;
|
||||
@@ -425,7 +439,9 @@ pub mod properties {
|
||||
element.0.connect(self.signal_name, false, self.callback);
|
||||
}
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State) {}
|
||||
fn rebuild(self, _element: &Element, _state: &mut Self::State) {
|
||||
// TODO we want to *remove* the previous listener, and reconnect with this new one
|
||||
}
|
||||
}
|
||||
|
||||
/* examples for macro */
|
||||
@@ -528,7 +544,7 @@ pub mod properties {
|
||||
}
|
||||
|
||||
/* end examples for properties macro */
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Label {
|
||||
value: String,
|
||||
}
|
||||
@@ -554,7 +570,9 @@ pub mod properties {
|
||||
}
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State) {
|
||||
todo!()
|
||||
if self.value != state.value {
|
||||
LeptosGtk::set_attribute(element, "label", &self.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
http = { version = "1.1", optional = true }
|
||||
web-sys = { version = "0.3.70", features = ["AbortController", "AbortSignal"] }
|
||||
wasm-bindgen = "0.2.93"
|
||||
lazy_static = "1.5"
|
||||
rust-embed = { version = "8.5", features = [
|
||||
"axum",
|
||||
"mime_guess",
|
||||
|
||||
@@ -162,22 +162,24 @@ pub fn App() -> impl IntoView {
|
||||
<table class="table table-hover table-striped test-data">
|
||||
<tbody>
|
||||
<For
|
||||
each={move || data.get()}
|
||||
key={|row| row.id}
|
||||
each=move || data.get()
|
||||
key=|row| row.id
|
||||
children=move |row: RowData| {
|
||||
let row_id = row.id;
|
||||
let label = row.label;
|
||||
let is_selected = is_selected.clone();
|
||||
ViewTemplate::new(view! {
|
||||
<tr class:danger={move || is_selected.selected(Some(row_id))}>
|
||||
<td class="col-md-1">{row_id.to_string()}</td>
|
||||
<td class="col-md-4"><a on:click=move |_| set_selected.set(Some(row_id))>{move || label.get()}</a></td>
|
||||
<td class="col-md-1"><a on:click=move |_| remove(row_id)><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a></td>
|
||||
<td class="col-md-6"/>
|
||||
</tr>
|
||||
})
|
||||
template! {
|
||||
< tr class : danger = { move || is_selected.selected(Some(row_id)) }
|
||||
> < td class = "col-md-1" > { row_id.to_string() } </ td > < td
|
||||
class = "col-md-4" >< a on : click = move | _ | set_selected
|
||||
.set(Some(row_id)) > { move || label.get() } </ a ></ td > < td
|
||||
class = "col-md-1" >< a on : click = move | _ | remove(row_id) ><
|
||||
span class = "glyphicon glyphicon-remove" aria - hidden = "true" ></
|
||||
span ></ a ></ td > < td class = "col-md-6" /> </ tr >
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<span class="preloadicon glyphicon glyphicon-remove" aria-hidden="true"></span>
|
||||
|
||||
@@ -16,6 +16,7 @@ server_fn = { path = "../../server_fn", features = [
|
||||
"serde-lite",
|
||||
"rkyv",
|
||||
"multipart",
|
||||
"postcard",
|
||||
] }
|
||||
log = "0.4.22"
|
||||
simple_logger = "5.0"
|
||||
|
||||
@@ -6,7 +6,8 @@ use server_fn::{
|
||||
client::{browser::BrowserClient, Client},
|
||||
codec::{
|
||||
Encoding, FromReq, FromRes, GetUrl, IntoReq, IntoRes, MultipartData,
|
||||
MultipartFormData, Rkyv, SerdeLite, StreamingText, TextStream,
|
||||
MultipartFormData, Postcard, Rkyv, SerdeLite, StreamingText,
|
||||
TextStream,
|
||||
},
|
||||
request::{browser::BrowserRequest, ClientReq, Req},
|
||||
response::{browser::BrowserResponse, ClientRes, Res},
|
||||
@@ -65,6 +66,7 @@ pub fn HomePage() -> impl IntoView {
|
||||
<h2>"Alternative Encodings"</h2>
|
||||
<ServerFnArgumentExample/>
|
||||
<RkyvExample/>
|
||||
<PostcardExample/>
|
||||
<FileUpload/>
|
||||
<FileUploadWithProgress/>
|
||||
<FileWatcher/>
|
||||
@@ -880,3 +882,67 @@ pub fn CustomClientExample() -> impl IntoView {
|
||||
})>Click me</button>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct PostcardData {
|
||||
name: String,
|
||||
age: u32,
|
||||
hobbies: Vec<String>,
|
||||
}
|
||||
|
||||
/// This server function uses Postcard for both input and output encoding.
|
||||
/// Postcard provides efficient binary serialization, almost as fast as rkyv, while also being
|
||||
/// serde compatible
|
||||
#[server(input = Postcard, output = Postcard)]
|
||||
pub async fn postcard_example(
|
||||
data: PostcardData,
|
||||
) -> Result<PostcardData, ServerFnError> {
|
||||
// Simulate some processing time
|
||||
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
|
||||
|
||||
// Modify the data to demonstrate server-side changes
|
||||
let mut modified_data = data.clone();
|
||||
modified_data.age += 1;
|
||||
modified_data.hobbies.push("Rust programming".to_string());
|
||||
|
||||
Ok(modified_data)
|
||||
}
|
||||
|
||||
/// This component demonstrates the usage of Postcard encoding with server functions.
|
||||
/// It allows incrementing the age of a person and shows how the data is
|
||||
/// serialized, sent to the server, processed, and returned.
|
||||
#[component]
|
||||
pub fn PostcardExample() -> impl IntoView {
|
||||
// Initialize the input data
|
||||
let (input, set_input) = signal(PostcardData {
|
||||
name: "Alice".to_string(),
|
||||
age: 30,
|
||||
hobbies: vec!["reading".to_string(), "hiking".to_string()],
|
||||
});
|
||||
|
||||
// Create a resource that will call the server function whenever the input changes
|
||||
let postcard_result = Resource::new(
|
||||
move || input.get(),
|
||||
|data| async move { postcard_example(data).await },
|
||||
);
|
||||
|
||||
view! {
|
||||
<h3>Using <code>postcard</code> encoding</h3>
|
||||
<p>"This example demonstrates using Postcard for efficient binary serialization."</p>
|
||||
<button on:click=move |_| {
|
||||
// Update the input data when the button is clicked
|
||||
set_input.update(|data| {
|
||||
data.age += 1;
|
||||
});
|
||||
}>
|
||||
"Increment Age"
|
||||
</button>
|
||||
// Display the current input data
|
||||
<p>"Input: " {move || format!("{:?}", input.get())}</p>
|
||||
<Transition>
|
||||
// Display the result from the server, which will update automatically
|
||||
// when the input changes due to the resource
|
||||
<p>"Result: " {move || postcard_result.get().map(|r| format!("{:?}", r))}</p>
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
name = "ssr_modes"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
# std::sync::LazyLock is stabilized in Rust version 1.80.0
|
||||
rust-version = "1.80.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
@@ -11,7 +13,6 @@ actix-files = { version = "0.6.6", optional = true }
|
||||
actix-web = { version = "4.8", optional = true, features = ["macros"] }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1.0"
|
||||
lazy_static = "1.5"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
@@ -38,12 +39,12 @@ denylist = ["actix-files", "actix-web", "leptos_actix"]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "ssr_modes"
|
||||
# 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
|
||||
# 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/main.scss"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::{
|
||||
@@ -146,8 +147,9 @@ fn Post() -> impl IntoView {
|
||||
}
|
||||
|
||||
// Dummy API
|
||||
lazy_static! {
|
||||
static ref POSTS: Vec<Post> = vec![
|
||||
|
||||
static POSTS: LazyLock<[Post; 3]> = LazyLock::new(|| {
|
||||
[
|
||||
Post {
|
||||
id: 0,
|
||||
title: "My first post".to_string(),
|
||||
@@ -163,8 +165,8 @@ lazy_static! {
|
||||
title: "My third post".to_string(),
|
||||
content: "This is my third post".to_string(),
|
||||
},
|
||||
];
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
#[derive(Error, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PostError {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
name = "ssr_modes_axum"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
# std::sync::LazyLock is stabilized in Rust version 1.80.0
|
||||
rust-version = "1.80.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
@@ -9,7 +11,6 @@ crate-type = ["cdylib", "rlib"]
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1.0"
|
||||
lazy_static = "1.5"
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"hydration",
|
||||
] } #"nightly", "hydration"] }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::MetaTags;
|
||||
use leptos_meta::*;
|
||||
@@ -261,8 +262,9 @@ pub fn Admin() -> impl IntoView {
|
||||
}
|
||||
|
||||
// Dummy API
|
||||
lazy_static! {
|
||||
static ref POSTS: Vec<Post> = vec![
|
||||
|
||||
static POSTS: LazyLock<[Post; 3]> = LazyLock::new(|| {
|
||||
[
|
||||
Post {
|
||||
id: 0,
|
||||
title: "My first post".to_string(),
|
||||
@@ -278,8 +280,8 @@ lazy_static! {
|
||||
title: "My third post".to_string(),
|
||||
content: "This is my third post".to_string(),
|
||||
},
|
||||
];
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
#[derive(Error, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PostError {
|
||||
|
||||
13
examples/static_routing/.gitignore
vendored
Normal file
13
examples/static_routing/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
pkg
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# node e2e test tools and outputs
|
||||
node_modules/
|
||||
test-results/
|
||||
end2end/playwright-report/
|
||||
playwright/.cache/
|
||||
115
examples/static_routing/Cargo.toml
Normal file
115
examples/static_routing/Cargo.toml
Normal file
@@ -0,0 +1,115 @@
|
||||
[package]
|
||||
name = "static_routing"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1.0"
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"hydration",
|
||||
] } #"nightly", "hydration"] }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = [
|
||||
"fs",
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
], optional = true }
|
||||
tokio-stream = { version = "0.1", features = ["fs"], optional = true }
|
||||
futures = "0.3"
|
||||
wasm-bindgen = "0.2.93"
|
||||
notify = { version = "6", optional = true }
|
||||
http = { version = "1", optional = true }
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:tokio-stream",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"dep:leptos_axum",
|
||||
"leptos_router/ssr",
|
||||
"dep:notify",
|
||||
"dep:http"
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
opt-level = 'z'
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "ssr_modes"
|
||||
# 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/main.scss"
|
||||
# Assets source dir. All files found here will be copied and synchronized to site-root.
|
||||
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
|
||||
#
|
||||
# Optional. Env: LEPTOS_ASSETS_DIR.
|
||||
assets-dir = "assets"
|
||||
# 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:3007"
|
||||
# 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.
|
||||
# [Windows] for non-WSL use "npx.cmd playwright test"
|
||||
# This binary name can be checked in Powershell with Get-Command npx
|
||||
end2end-cmd = "npx playwright test"
|
||||
end2end-dir = "end2end"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = ["ssr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
lib-features = ["hydrate"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
|
||||
lib-profile-release = "wasm-release"
|
||||
21
examples/static_routing/LICENSE
Normal file
21
examples/static_routing/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 henrik
|
||||
|
||||
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.
|
||||
8
examples/static_routing/Makefile.toml
Normal file
8
examples/static_routing/Makefile.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
|
||||
CLIENT_PROCESS_NAME = "ssr_modes_axum"
|
||||
11
examples/static_routing/README.md
Normal file
11
examples/static_routing/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Static Routing Example
|
||||
|
||||
This example shows the static routing features, which can be used to generate the HTML content for some routes before a request.
|
||||
|
||||
## Getting Started
|
||||
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run `cargo leptos watch` to run this example.
|
||||
BIN
examples/static_routing/assets/favicon.ico
Normal file
BIN
examples/static_routing/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
3
examples/static_routing/posts/post1.md
Normal file
3
examples/static_routing/posts/post1.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# My first blog post
|
||||
|
||||
Having a blog is *fun*.
|
||||
3
examples/static_routing/posts/post2.md
Normal file
3
examples/static_routing/posts/post2.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# My second blog post
|
||||
|
||||
Coming up with content is hard.
|
||||
3
examples/static_routing/posts/post3.md
Normal file
3
examples/static_routing/posts/post3.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# My third blog post
|
||||
|
||||
Could I just have AI write this for me instead?
|
||||
3
examples/static_routing/posts/post4.md
Normal file
3
examples/static_routing/posts/post4.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# My fourth post
|
||||
|
||||
Here is some content. It should regenerate the static page.
|
||||
2
examples/static_routing/rust-toolchain.toml
Normal file
2
examples/static_routing/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "stable" # test change
|
||||
323
examples/static_routing/src/app.rs
Normal file
323
examples/static_routing/src/app.rs
Normal file
@@ -0,0 +1,323 @@
|
||||
use std::path::Path;
|
||||
|
||||
use futures::{channel::mpsc, Stream};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::MetaTags;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Redirect, Route, Router},
|
||||
hooks::use_params,
|
||||
params::Params,
|
||||
path,
|
||||
static_routes::StaticRoute,
|
||||
SsrMode,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<AutoReload options=options.clone()/>
|
||||
<HydrationScripts options/>
|
||||
<MetaTags/>
|
||||
</head>
|
||||
<body>
|
||||
<App/>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
provide_meta_context();
|
||||
let fallback = || view! { "Page not found." }.into_view();
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/ssr_modes.css"/>
|
||||
<Title text="Welcome to Leptos"/>
|
||||
<Meta name="color-scheme" content="dark light"/>
|
||||
<Router>
|
||||
<nav>
|
||||
<a href="/">"Home"</a>
|
||||
</nav>
|
||||
<main>
|
||||
<FlatRoutes fallback>
|
||||
<Route
|
||||
path=path!("/")
|
||||
view=HomePage
|
||||
ssr=SsrMode::Static(
|
||||
StaticRoute::new().regenerate(|_| watch_path(Path::new("./posts"))),
|
||||
)
|
||||
/>
|
||||
|
||||
<Route
|
||||
path=path!("/about")
|
||||
view=move || view! { <Redirect path="/"/> }
|
||||
ssr=SsrMode::Static(StaticRoute::new())
|
||||
/>
|
||||
|
||||
<Route
|
||||
path=path!("/post/:slug/")
|
||||
view=Post
|
||||
ssr=SsrMode::Static(
|
||||
StaticRoute::new()
|
||||
.prerender_params(|| async move {
|
||||
[("slug".into(), list_slugs().await.unwrap_or_default())]
|
||||
.into_iter()
|
||||
.collect()
|
||||
})
|
||||
.regenerate(|params| {
|
||||
let slug = params.get("slug").unwrap();
|
||||
watch_path(Path::new(&format!("./posts/{slug}.md")))
|
||||
}),
|
||||
)
|
||||
/>
|
||||
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
// load the posts
|
||||
let posts = Resource::new(|| (), |_| list_posts());
|
||||
let posts = move || {
|
||||
posts
|
||||
.get()
|
||||
.map(|n| n.unwrap_or_default())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
view! {
|
||||
<h1>"My Great Blog"</h1>
|
||||
<Suspense fallback=move || view! { <p>"Loading posts..."</p> }>
|
||||
<ul>
|
||||
<For each=posts key=|post| post.slug.clone() let:post>
|
||||
<li>
|
||||
<a href=format!("/post/{}/", post.slug)>{post.title.clone()}</a>
|
||||
</li>
|
||||
</For>
|
||||
</ul>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Params, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PostParams {
|
||||
slug: Option<String>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Post() -> impl IntoView {
|
||||
let query = use_params::<PostParams>();
|
||||
let slug = move || {
|
||||
query
|
||||
.get()
|
||||
.map(|q| q.slug.unwrap_or_default())
|
||||
.map_err(|_| PostError::InvalidId)
|
||||
};
|
||||
let post_resource = Resource::new_blocking(slug, |slug| async move {
|
||||
match slug {
|
||||
Err(e) => Err(e),
|
||||
Ok(slug) => get_post(slug)
|
||||
.await
|
||||
.map(|data| data.ok_or(PostError::PostNotFound))
|
||||
.map_err(|e| PostError::ServerError(e.to_string())),
|
||||
}
|
||||
});
|
||||
|
||||
let post_view = move || {
|
||||
Suspend::new(async move {
|
||||
match post_resource.await {
|
||||
Ok(Ok(post)) => {
|
||||
Ok(view! {
|
||||
<h1>{post.title.clone()}</h1>
|
||||
<p>{post.content.clone()}</p>
|
||||
|
||||
// since we're using async rendering for this page,
|
||||
// this metadata should be included in the actual HTML <head>
|
||||
// when it's first served
|
||||
<Title text=post.title/>
|
||||
<Meta name="description" content=post.content/>
|
||||
})
|
||||
}
|
||||
Ok(Err(e)) | Err(e) => {
|
||||
Err(PostError::ServerError(e.to_string()))
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
<em>"The world's best content."</em>
|
||||
<Suspense fallback=move || view! { <p>"Loading post..."</p> }>
|
||||
<ErrorBoundary fallback=|errors| {
|
||||
#[cfg(feature = "ssr")]
|
||||
expect_context::<leptos_axum::ResponseOptions>()
|
||||
.set_status(http::StatusCode::NOT_FOUND);
|
||||
view! {
|
||||
<div class="error">
|
||||
<h1>"Something went wrong."</h1>
|
||||
<ul>
|
||||
{move || {
|
||||
errors
|
||||
.get()
|
||||
.into_iter()
|
||||
.map(|(_, error)| view! { <li>{error.to_string()}</li> })
|
||||
.collect::<Vec<_>>()
|
||||
}}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}>{post_view}</ErrorBoundary>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PostError {
|
||||
#[error("Invalid post ID.")]
|
||||
InvalidId,
|
||||
#[error("Post not found.")]
|
||||
PostNotFound,
|
||||
#[error("Server error: {0}.")]
|
||||
ServerError(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Post {
|
||||
slug: String,
|
||||
title: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn list_slugs() -> Result<Vec<String>, ServerFnError> {
|
||||
use tokio::fs;
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
let files = ReadDirStream::new(fs::read_dir("./posts").await?);
|
||||
Ok(files
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
return None;
|
||||
}
|
||||
let extension = path.extension()?;
|
||||
if extension != "md" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let slug = path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or_default()
|
||||
.replace(".md", "");
|
||||
Some(slug)
|
||||
})
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn list_posts() -> Result<Vec<Post>, ServerFnError> {
|
||||
println!("calling list_posts");
|
||||
|
||||
use futures::TryStreamExt;
|
||||
use tokio::fs;
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
|
||||
let files = ReadDirStream::new(fs::read_dir("./posts").await?);
|
||||
files
|
||||
.try_filter_map(|entry| async move {
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(extension) = path.extension() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if extension != "md" {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let slug = path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or_default()
|
||||
.replace(".md", "");
|
||||
let content = fs::read_to_string(path).await?;
|
||||
// world's worst Markdown frontmatter parser
|
||||
let title = content.lines().next().unwrap().replace("# ", "");
|
||||
|
||||
Ok(Some(Post {
|
||||
slug,
|
||||
title,
|
||||
content,
|
||||
}))
|
||||
})
|
||||
.try_collect()
|
||||
.await
|
||||
.map_err(ServerFnError::from)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn get_post(slug: String) -> Result<Option<Post>, ServerFnError> {
|
||||
println!("reading ./posts/{slug}.md");
|
||||
let content =
|
||||
tokio::fs::read_to_string(&format!("./posts/{slug}.md")).await?;
|
||||
// world's worst Markdown frontmatter parser
|
||||
let title = content.lines().next().unwrap().replace("# ", "");
|
||||
|
||||
Ok(Some(Post {
|
||||
slug,
|
||||
title,
|
||||
content,
|
||||
}))
|
||||
}
|
||||
|
||||
#[allow(unused)] // path is not used in non-SSR
|
||||
fn watch_path(path: &Path) -> impl Stream<Item = ()> {
|
||||
#[allow(unused)]
|
||||
let (mut tx, rx) = mpsc::channel(0);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
use notify::RecursiveMode;
|
||||
use notify::Watcher;
|
||||
|
||||
let mut watcher =
|
||||
notify::recommended_watcher(move |res: Result<_, _>| {
|
||||
if res.is_ok() {
|
||||
// if this fails, it's because the buffer is full
|
||||
// this means we've already notified before it's regenerated,
|
||||
// so this page will be queued for regeneration already
|
||||
_ = tx.try_send(());
|
||||
}
|
||||
})
|
||||
.expect("could not create watcher");
|
||||
|
||||
// Add a path to be watched. All files and directories at that path and
|
||||
// below will be monitored for changes.
|
||||
watcher
|
||||
.watch(path, RecursiveMode::NonRecursive)
|
||||
.expect("could not watch path");
|
||||
|
||||
// we want this to run as long as the server is alive
|
||||
std::mem::forget(watcher);
|
||||
}
|
||||
|
||||
rx
|
||||
}
|
||||
9
examples/static_routing/src/lib.rs
Normal file
9
examples/static_routing/src/lib.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub mod app;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount::hydrate_body(App);
|
||||
}
|
||||
42
examples/static_routing/src/main.rs
Normal file
42
examples/static_routing/src/main.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::Router;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list_with_ssg, LeptosRoutes};
|
||||
use static_routing::app::*;
|
||||
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let leptos_options = conf.leptos_options;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let (routes, static_routes) = generate_route_list_with_ssg({
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
});
|
||||
|
||||
static_routes.generate(&leptos_options).await;
|
||||
|
||||
let app = Router::new()
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
})
|
||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
// no client-side main function
|
||||
// unless we want this to work with e.g., Trunk for pure client-side testing
|
||||
// see lib.rs for hydration function instead
|
||||
}
|
||||
3
examples/static_routing/style/main.scss
Normal file
3
examples/static_routing/style/main.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
@@ -13,6 +13,9 @@ leptos = { path = "../../leptos", features = ["csr"] }
|
||||
reactive_stores = { path = "../../reactive_stores" }
|
||||
reactive_stores_macro = { path = "../../reactive_stores_macro" }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde_json = "1.0.128"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen = "0.2.93"
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||
<style>
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,43 +1,88 @@
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::{Field, Store, StoreFieldIterator};
|
||||
use reactive_stores_macro::Store;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
#[derive(Debug, Store)]
|
||||
use chrono::{Local, NaiveDate};
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::{Field, Patch, Store};
|
||||
use reactive_stores_macro::{Patch, Store};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ID starts higher than 0 because we have a few starting todos by default
|
||||
static NEXT_ID: AtomicUsize = AtomicUsize::new(3);
|
||||
|
||||
#[derive(Debug, Store, Serialize, Deserialize)]
|
||||
struct Todos {
|
||||
user: String,
|
||||
user: User,
|
||||
#[store(key: usize = |todo| todo.id)]
|
||||
todos: Vec<Todo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Store)]
|
||||
#[derive(Debug, Store, Patch, Serialize, Deserialize)]
|
||||
struct User {
|
||||
name: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Store, Serialize, Deserialize)]
|
||||
struct Todo {
|
||||
id: usize,
|
||||
label: String,
|
||||
completed: bool,
|
||||
status: Status,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Store, Serialize, Deserialize)]
|
||||
enum Status {
|
||||
#[default]
|
||||
Pending,
|
||||
Scheduled,
|
||||
ScheduledFor {
|
||||
date: NaiveDate,
|
||||
},
|
||||
Done,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub fn next_step(&mut self) {
|
||||
*self = match self {
|
||||
Status::Pending => Status::ScheduledFor {
|
||||
date: Local::now().naive_local().into(),
|
||||
},
|
||||
Status::Scheduled | Status::ScheduledFor { .. } => Status::Done,
|
||||
Status::Done => Status::Done,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Todo {
|
||||
pub fn new(label: impl ToString) -> Self {
|
||||
Self {
|
||||
id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
|
||||
label: label.to_string(),
|
||||
completed: false,
|
||||
status: Status::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn data() -> Todos {
|
||||
Todos {
|
||||
user: "Bob".to_string(),
|
||||
user: User {
|
||||
name: "Bob".to_string(),
|
||||
email: "lawblog@bobloblaw.com".into(),
|
||||
},
|
||||
todos: vec![
|
||||
Todo {
|
||||
id: 0,
|
||||
label: "Create reactive store".to_string(),
|
||||
completed: true,
|
||||
status: Status::Pending,
|
||||
},
|
||||
Todo {
|
||||
id: 1,
|
||||
label: "???".to_string(),
|
||||
completed: false,
|
||||
status: Status::Pending,
|
||||
},
|
||||
Todo {
|
||||
id: 2,
|
||||
label: "Profit".to_string(),
|
||||
completed: false,
|
||||
status: Status::Pending,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -49,17 +94,10 @@ pub fn App() -> impl IntoView {
|
||||
|
||||
let input_ref = NodeRef::new();
|
||||
|
||||
let rows = move || {
|
||||
store
|
||||
.todos()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, todo)| view! { <TodoRow store idx todo/> })
|
||||
.collect_view()
|
||||
};
|
||||
|
||||
view! {
|
||||
<p>"Hello, " {move || store.user().get()}</p>
|
||||
<p>"Hello, " {move || store.user().name().get()}</p>
|
||||
<UserForm user=store.user()/>
|
||||
<hr/>
|
||||
<form on:submit=move |ev| {
|
||||
ev.prevent_default();
|
||||
store.todos().write().push(Todo::new(input_ref.get().unwrap().value()));
|
||||
@@ -67,30 +105,69 @@ pub fn App() -> impl IntoView {
|
||||
<label>"Add a Todo" <input type="text" node_ref=input_ref/></label>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
<ol>{rows}</ol>
|
||||
<div style="display: flex"></div>
|
||||
<ol>
|
||||
// because `todos` is a keyed field, `store.todos()` returns a struct that
|
||||
// directly implements IntoIterator, so we can use it in <For/> and
|
||||
// it will manage reactivity for the store fields correctly
|
||||
<For
|
||||
each=move || {
|
||||
leptos::logging::log!("RERUNNING FOR CALCULATION");
|
||||
store.todos()
|
||||
}
|
||||
|
||||
key=|row| row.id().get()
|
||||
let:todo
|
||||
>
|
||||
<TodoRow store todo/>
|
||||
</For>
|
||||
|
||||
</ol>
|
||||
<pre>{move || serde_json::to_string_pretty(&*store.read())}</pre>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn UserForm(#[prop(into)] user: Field<User>) -> impl IntoView {
|
||||
let error = RwSignal::new(None);
|
||||
|
||||
view! {
|
||||
{move || error.get().map(|n| view! { <p>{n}</p> })}
|
||||
<form on:submit:target=move |ev| {
|
||||
ev.prevent_default();
|
||||
match User::from_event(&ev) {
|
||||
Ok(new_user) => {
|
||||
error.set(None);
|
||||
user.patch(new_user);
|
||||
}
|
||||
Err(e) => error.set(Some(e.to_string())),
|
||||
}
|
||||
}>
|
||||
<label>
|
||||
"Name" <input type="text" name="name" prop:value=move || user.name().get()/>
|
||||
</label>
|
||||
<label>
|
||||
"Email" <input type="email" name="email" prop:value=move || user.email().get()/>
|
||||
</label>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn TodoRow(
|
||||
store: Store<Todos>,
|
||||
idx: usize,
|
||||
#[prop(into)] todo: Field<Todo>,
|
||||
) -> impl IntoView {
|
||||
let completed = todo.completed();
|
||||
let status = todo.status();
|
||||
let title = todo.label();
|
||||
|
||||
let editing = RwSignal::new(false);
|
||||
let editing = RwSignal::new(true);
|
||||
|
||||
view! {
|
||||
<li
|
||||
style:text-decoration=move || {
|
||||
completed.get().then_some("line-through").unwrap_or_default()
|
||||
}
|
||||
<li style:text-decoration=move || {
|
||||
status.done().then_some("line-through").unwrap_or_default()
|
||||
}>
|
||||
|
||||
class:foo=move || completed.get()
|
||||
>
|
||||
<p
|
||||
class:hidden=move || editing.get()
|
||||
on:click=move |_| {
|
||||
@@ -106,25 +183,48 @@ fn TodoRow(
|
||||
prop:value=move || title.get()
|
||||
on:change=move |ev| {
|
||||
title.set(event_target_value(&ev));
|
||||
editing.set(false);
|
||||
}
|
||||
|
||||
on:blur=move |_| editing.set(false)
|
||||
autofocus
|
||||
/>
|
||||
<input
|
||||
type="checkbox"
|
||||
prop:checked=move || completed.get()
|
||||
on:click=move |_| { completed.update(|n| *n = !*n) }
|
||||
/>
|
||||
|
||||
<button on:click=move |_| {
|
||||
store
|
||||
.todos()
|
||||
.update(|todos| {
|
||||
todos.remove(idx);
|
||||
});
|
||||
status.write().next_step()
|
||||
}>
|
||||
{move || {
|
||||
if todo.status().done() {
|
||||
"Done"
|
||||
} else if status.scheduled() || status.scheduled_for() {
|
||||
"Scheduled"
|
||||
} else {
|
||||
"Pending"
|
||||
}
|
||||
}}
|
||||
|
||||
</button>
|
||||
|
||||
<button on:click=move |_| {
|
||||
let id = todo.id().get();
|
||||
store.todos().write().retain(|todo| todo.id != id);
|
||||
}>"X"</button>
|
||||
<input
|
||||
type="date"
|
||||
prop:value=move || {
|
||||
todo.status().scheduled_for_date().map(|n| n.get().to_string())
|
||||
}
|
||||
|
||||
class:hidden=move || !todo.status().scheduled_for()
|
||||
on:change:target=move |ev| {
|
||||
if let Some(date) = todo.status().scheduled_for_date() {
|
||||
let value = ev.target().value();
|
||||
match NaiveDate::parse_from_str(&value, "%Y-%m-%d") {
|
||||
Ok(new_date) => {
|
||||
date.set(new_date);
|
||||
}
|
||||
Err(e) => warn!("{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,14 +147,13 @@ fn Nested() -> impl IntoView {
|
||||
"Loading 1..."
|
||||
}>
|
||||
{move || {
|
||||
one_second.get().map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
|
||||
one_second.map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
|
||||
}}
|
||||
<Suspense fallback=|| {
|
||||
"Loading 2..."
|
||||
}>
|
||||
{move || {
|
||||
two_second
|
||||
.get()
|
||||
.map(|_| {
|
||||
view! {
|
||||
<p id="loaded-2">"Two Second: Loaded 2!"</p>
|
||||
@@ -217,7 +216,6 @@ fn Parallel() -> impl IntoView {
|
||||
}>
|
||||
{move || {
|
||||
one_second
|
||||
.get()
|
||||
.map(move |_| {
|
||||
view! {
|
||||
<p id="loaded-1">"One Second: Loaded 1!"</p>
|
||||
@@ -234,7 +232,6 @@ fn Parallel() -> impl IntoView {
|
||||
}>
|
||||
{move || {
|
||||
two_second
|
||||
.get()
|
||||
.map(move |_| {
|
||||
view! {
|
||||
<p id="loaded-2">"Two Second: Loaded 2!"</p>
|
||||
@@ -264,7 +261,7 @@ fn Single() -> impl IntoView {
|
||||
"Loading 1..."
|
||||
}>
|
||||
{move || {
|
||||
one_second.get().map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
|
||||
one_second.map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
|
||||
}}
|
||||
|
||||
</Suspense>
|
||||
@@ -300,7 +297,7 @@ fn InsideComponentChild() -> impl IntoView {
|
||||
"Loading 1..."
|
||||
}>
|
||||
{move || {
|
||||
one_second.get().map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
|
||||
one_second.map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
|
||||
}}
|
||||
|
||||
</Suspense>
|
||||
@@ -319,7 +316,7 @@ fn LocalResource() -> impl IntoView {
|
||||
"Loading 1..."
|
||||
}>
|
||||
{move || {
|
||||
one_second.get().map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
|
||||
one_second.map(|_| view! { <p id="loaded-1">"One Second: Loaded 1!"</p> })
|
||||
}}
|
||||
{move || {
|
||||
Suspend::new(async move {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hydration_context"
|
||||
version = "0.2.0-beta2"
|
||||
version = "0.2.0-beta6"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -27,6 +27,7 @@ type SealedErrors = Arc<RwLock<HashSet<SerializedDataId>>>;
|
||||
/// The shared context that should be used on the server side.
|
||||
pub struct SsrSharedContext {
|
||||
id: AtomicUsize,
|
||||
non_hydration_id: AtomicUsize,
|
||||
is_hydrating: AtomicBool,
|
||||
sync_buf: RwLock<Vec<ResolvedData>>,
|
||||
async_buf: AsyncDataBuf,
|
||||
@@ -41,6 +42,7 @@ impl SsrSharedContext {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_hydrating: AtomicBool::new(true),
|
||||
non_hydration_id: AtomicUsize::new(usize::MAX),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -52,6 +54,7 @@ impl SsrSharedContext {
|
||||
pub fn new_islands() -> Self {
|
||||
Self {
|
||||
is_hydrating: AtomicBool::new(false),
|
||||
non_hydration_id: AtomicUsize::new(usize::MAX),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -73,8 +76,13 @@ impl SharedContext for SsrSharedContext {
|
||||
false
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn next_id(&self) -> SerializedDataId {
|
||||
let id = self.id.fetch_add(1, Ordering::Relaxed);
|
||||
let id = if self.get_is_hydrating() {
|
||||
self.id.fetch_add(1, Ordering::Relaxed)
|
||||
} else {
|
||||
self.non_hydration_id.fetch_sub(1, Ordering::Relaxed)
|
||||
};
|
||||
SerializedDataId(id)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
actix-http = "3.8"
|
||||
actix-files = "0.6"
|
||||
actix-web = "4.8"
|
||||
futures = "0.3.30"
|
||||
any_spawner = { workspace = true, features = ["tokio"] }
|
||||
@@ -22,12 +23,18 @@ leptos_router = { workspace = true, features = ["ssr"] }
|
||||
server_fn = { workspace = true, features = ["actix"] }
|
||||
serde_json = "1.0"
|
||||
parking_lot = "0.12.3"
|
||||
tracing = "0.1.40"
|
||||
tracing = { version = "0.1", optional = true }
|
||||
tokio = { version = "1.39", features = ["rt", "fs"] }
|
||||
send_wrapper = "0.6.0"
|
||||
dashmap = "6"
|
||||
once_cell = "1"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[features]
|
||||
islands-router = []
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing"]
|
||||
|
||||
@@ -6,30 +6,38 @@
|
||||
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
|
||||
//! directory in the Leptos repository.
|
||||
|
||||
use actix_files::NamedFile;
|
||||
use actix_http::header::{HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER};
|
||||
use actix_web::{
|
||||
body::BoxBody,
|
||||
dev::{ServiceFactory, ServiceRequest},
|
||||
http::header,
|
||||
web::{Payload, ServiceConfig},
|
||||
test,
|
||||
web::{Data, Payload, ServiceConfig},
|
||||
*,
|
||||
};
|
||||
use dashmap::DashMap;
|
||||
use futures::{stream::once, Stream, StreamExt};
|
||||
use http::StatusCode;
|
||||
use hydration_context::SsrSharedContext;
|
||||
use leptos::{
|
||||
config::LeptosOptions,
|
||||
context::{provide_context, use_context},
|
||||
prelude::expect_context,
|
||||
reactive_graph::{computed::ScopedFuture, owner::Owner},
|
||||
IntoView, *,
|
||||
IntoView,
|
||||
};
|
||||
use leptos_integration_utils::{
|
||||
BoxedFnOnce, ExtendResponse, PinnedFuture, PinnedStream,
|
||||
};
|
||||
use leptos_meta::ServerMetaContext;
|
||||
use leptos_router::{
|
||||
components::provide_server_redirect, location::RequestUrl, PathSegment,
|
||||
RouteList, RouteListing, SsrMode, StaticDataMap, StaticMode, *,
|
||||
components::provide_server_redirect,
|
||||
location::RequestUrl,
|
||||
static_routes::{RegenerationFn, ResolvedStaticPath},
|
||||
Method, PathSegment, RouteList, RouteListing, SsrMode,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::RwLock;
|
||||
use send_wrapper::SendWrapper;
|
||||
use server_fn::{
|
||||
@@ -37,7 +45,9 @@ use server_fn::{
|
||||
};
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
future::Future,
|
||||
ops::{Deref, DerefMut},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
@@ -207,7 +217,10 @@ impl ExtendResponse for ActixResponse {
|
||||
/// without actually setting the status code. This means that the client will not follow the
|
||||
/// redirect, and can therefore return the value of the server function and then handle
|
||||
/// the redirect with client-side routing.
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn redirect(path: &str) {
|
||||
if let (Some(req), Some(res)) =
|
||||
(use_context::<Request>(), use_context::<ResponseOptions>())
|
||||
@@ -239,10 +252,14 @@ pub fn redirect(path: &str) {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Couldn't retrieve either Parts or ResponseOptions while trying \
|
||||
to redirect()."
|
||||
);
|
||||
let msg = "Couldn't retrieve either Parts or ResponseOptions while \
|
||||
trying to redirect().";
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("{}", &msg);
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
eprintln!("{}", &msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +299,10 @@ pub fn redirect(path: &str) {
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn handle_server_fns() -> Route {
|
||||
handle_server_fns_with_context(|| {})
|
||||
}
|
||||
@@ -306,7 +326,10 @@ pub fn handle_server_fns() -> Route {
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn handle_server_fns_with_context(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
) -> Route {
|
||||
@@ -438,7 +461,10 @@ pub fn handle_server_fns_with_context(
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
method: Method,
|
||||
@@ -505,7 +531,10 @@ where
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_in_order<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
method: Method,
|
||||
@@ -567,7 +596,10 @@ where
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_async<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
method: Method,
|
||||
@@ -590,7 +622,10 @@ where
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
@@ -624,7 +659,10 @@ where
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
@@ -655,7 +693,10 @@ where
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_in_order_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
@@ -685,7 +726,10 @@ where
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_async_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
@@ -694,17 +738,32 @@ pub fn render_app_async_with_context<IV>(
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
handle_response(method, additional_context, app_fn, |app, chunks| {
|
||||
Box::pin(async move {
|
||||
let app = app.to_html_stream_in_order().collect::<String>().await;
|
||||
let chunks = chunks();
|
||||
Box::pin(once(async move { app }).chain(chunks))
|
||||
as PinnedStream<String>
|
||||
})
|
||||
handle_response(method, additional_context, app_fn, async_stream_builder)
|
||||
}
|
||||
|
||||
fn async_stream_builder<IV>(
|
||||
app: IV,
|
||||
chunks: BoxedFnOnce<PinnedStream<String>>,
|
||||
) -> PinnedFuture<PinnedStream<String>>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
};
|
||||
let app = app.collect::<String>().await;
|
||||
let chunks = chunks();
|
||||
Box::pin(once(async move { app }).chain(chunks)) as PinnedStream<String>
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
fn provide_contexts(
|
||||
req: Request,
|
||||
meta_context: &ServerMetaContext,
|
||||
@@ -785,7 +844,7 @@ where
|
||||
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
|
||||
pub fn generate_route_list<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
app_fn: impl Fn() -> IV + 'static + Send + Clone,
|
||||
) -> Vec<ActixRouteListing>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
@@ -797,8 +856,8 @@ where
|
||||
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
|
||||
pub fn generate_route_list_with_ssg<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
) -> (Vec<ActixRouteListing>, StaticDataMap)
|
||||
app_fn: impl Fn() -> IV + 'static + Send + Clone,
|
||||
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
@@ -810,7 +869,7 @@ where
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes
|
||||
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format
|
||||
pub fn generate_route_list_with_exclusions<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
app_fn: impl Fn() -> IV + 'static + Send + Clone,
|
||||
excluded_routes: Option<Vec<String>>,
|
||||
) -> Vec<ActixRouteListing>
|
||||
where
|
||||
@@ -824,9 +883,9 @@ where
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes
|
||||
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format
|
||||
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
app_fn: impl Fn() -> IV + 'static + Send + Clone,
|
||||
excluded_routes: Option<Vec<String>>,
|
||||
) -> (Vec<ActixRouteListing>, StaticDataMap)
|
||||
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
@@ -875,7 +934,7 @@ pub struct ActixRouteListing {
|
||||
path: String,
|
||||
mode: SsrMode,
|
||||
methods: Vec<leptos_router::Method>,
|
||||
static_mode: Option<(StaticMode, StaticDataMap)>,
|
||||
regenerate: Vec<RegenerationFn>,
|
||||
}
|
||||
|
||||
impl From<RouteListing> for ActixRouteListing {
|
||||
@@ -888,12 +947,12 @@ impl From<RouteListing> for ActixRouteListing {
|
||||
};
|
||||
let mode = value.mode();
|
||||
let methods = value.methods().collect();
|
||||
let static_mode = value.into_static_parts();
|
||||
let regenerate = value.regenerate().into();
|
||||
Self {
|
||||
path,
|
||||
mode,
|
||||
mode: mode.clone(),
|
||||
methods,
|
||||
static_mode,
|
||||
regenerate,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -904,13 +963,13 @@ impl ActixRouteListing {
|
||||
path: String,
|
||||
mode: SsrMode,
|
||||
methods: impl IntoIterator<Item = leptos_router::Method>,
|
||||
static_mode: Option<(StaticMode, StaticDataMap)>,
|
||||
regenerate: impl Into<Vec<RegenerationFn>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
path,
|
||||
mode,
|
||||
methods: methods.into_iter().collect(),
|
||||
static_mode,
|
||||
regenerate: regenerate.into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -921,19 +980,13 @@ impl ActixRouteListing {
|
||||
|
||||
/// The rendering mode for this path.
|
||||
pub fn mode(&self) -> SsrMode {
|
||||
self.mode
|
||||
self.mode.clone()
|
||||
}
|
||||
|
||||
/// The HTTP request methods this path can handle.
|
||||
pub fn methods(&self) -> impl Iterator<Item = leptos_router::Method> + '_ {
|
||||
self.methods.iter().copied()
|
||||
}
|
||||
|
||||
/// Whether this route is statically rendered.
|
||||
#[inline(always)]
|
||||
pub fn static_mode(&self) -> Option<StaticMode> {
|
||||
self.static_mode.as_ref().map(|n| n.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
|
||||
@@ -942,10 +995,10 @@ impl ActixRouteListing {
|
||||
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format.
|
||||
/// Additional context will be provided to the app Element.
|
||||
pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
app_fn: impl Fn() -> IV + 'static + Send + Clone,
|
||||
excluded_routes: Option<Vec<String>>,
|
||||
additional_context: impl Fn() + 'static + Clone,
|
||||
) -> (Vec<ActixRouteListing>, StaticDataMap)
|
||||
additional_context: impl Fn() + 'static + Send + Clone,
|
||||
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
@@ -964,6 +1017,12 @@ where
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let generator = StaticRouteGenerator::new(
|
||||
&routes,
|
||||
app_fn.clone(),
|
||||
additional_context.clone(),
|
||||
);
|
||||
|
||||
// Axum's Router defines Root routes as "/" not ""
|
||||
let mut routes = routes
|
||||
.into_inner()
|
||||
@@ -977,7 +1036,7 @@ where
|
||||
"/".to_string(),
|
||||
Default::default(),
|
||||
[leptos_router::Method::Get],
|
||||
None,
|
||||
vec![],
|
||||
)]
|
||||
} else {
|
||||
// Routes to exclude from auto generation
|
||||
@@ -987,192 +1046,251 @@ where
|
||||
}
|
||||
routes
|
||||
},
|
||||
StaticDataMap::new(), // TODO
|
||||
//static_data_map,
|
||||
generator,
|
||||
)
|
||||
}
|
||||
|
||||
/// Allows generating any prerendered routes.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct StaticRouteGenerator(
|
||||
Box<dyn FnOnce(&LeptosOptions) -> PinnedFuture<()> + Send>,
|
||||
);
|
||||
|
||||
impl StaticRouteGenerator {
|
||||
fn render_route<IV: IntoView + 'static>(
|
||||
path: String,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + Clone + Send + 'static,
|
||||
) -> impl Future<Output = (Owner, String)> {
|
||||
let (meta_context, meta_output) = ServerMetaContext::new();
|
||||
let additional_context = {
|
||||
let add_context = additional_context.clone();
|
||||
move || {
|
||||
let mock_req = test::TestRequest::with_uri(&path)
|
||||
.insert_header(("Accept", "text/html"))
|
||||
.to_http_request();
|
||||
let res_options = ResponseOptions::default();
|
||||
provide_contexts(
|
||||
Request::new(&mock_req),
|
||||
&meta_context,
|
||||
&res_options,
|
||||
);
|
||||
add_context();
|
||||
}
|
||||
};
|
||||
|
||||
let (owner, stream) = leptos_integration_utils::build_response(
|
||||
app_fn.clone(),
|
||||
additional_context,
|
||||
async_stream_builder,
|
||||
);
|
||||
|
||||
let sc = owner.shared_context().unwrap();
|
||||
|
||||
async move {
|
||||
let stream = stream.await;
|
||||
while let Some(pending) = sc.await_deferred() {
|
||||
pending.await;
|
||||
}
|
||||
|
||||
let html = meta_output
|
||||
.inject_meta_context(stream)
|
||||
.await
|
||||
.collect::<String>()
|
||||
.await;
|
||||
(owner, html)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new static route generator from the given list of route definitions.
|
||||
pub fn new<IV>(
|
||||
routes: &RouteList,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Self({
|
||||
let routes = routes.clone();
|
||||
Box::new(move |options| {
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
let additional_context = additional_context.clone();
|
||||
|
||||
Box::pin(routes.generate_static_files(
|
||||
move |path: &ResolvedStaticPath| {
|
||||
Self::render_route(
|
||||
path.to_string(),
|
||||
app_fn.clone(),
|
||||
additional_context.clone(),
|
||||
)
|
||||
},
|
||||
move |path: &ResolvedStaticPath,
|
||||
owner: &Owner,
|
||||
html: String| {
|
||||
let options = options.clone();
|
||||
let path = path.to_owned();
|
||||
let response_options = owner.with(use_context);
|
||||
async move {
|
||||
write_static_route(
|
||||
&options,
|
||||
response_options,
|
||||
path.as_ref(),
|
||||
&html,
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
was_404,
|
||||
))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Generates the routes.
|
||||
pub async fn generate(self, options: &LeptosOptions) {
|
||||
(self.0)(options).await
|
||||
}
|
||||
}
|
||||
|
||||
static STATIC_HEADERS: Lazy<DashMap<String, ResponseOptions>> =
|
||||
Lazy::new(DashMap::new);
|
||||
|
||||
fn was_404(owner: &Owner) -> bool {
|
||||
let resp = owner.with(|| expect_context::<ResponseOptions>());
|
||||
let status = resp.0.read().status;
|
||||
|
||||
if let Some(status) = status {
|
||||
return status == StatusCode::NOT_FOUND;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn static_path(options: &LeptosOptions, path: &str) -> String {
|
||||
use leptos_integration_utils::static_file_path;
|
||||
|
||||
// If the path ends with a trailing slash, we generate the path
|
||||
// as a directory with a index.html file inside.
|
||||
if path != "/" && path.ends_with("/") {
|
||||
static_file_path(options, &format!("{}index", path))
|
||||
} else {
|
||||
static_file_path(options, path)
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_static_route(
|
||||
options: &LeptosOptions,
|
||||
response_options: Option<ResponseOptions>,
|
||||
path: &str,
|
||||
html: &str,
|
||||
) -> Result<(), std::io::Error> {
|
||||
if let Some(options) = response_options {
|
||||
STATIC_HEADERS.insert(path.to_string(), options);
|
||||
}
|
||||
|
||||
let path = static_path(options, path);
|
||||
let path = Path::new(&path);
|
||||
if let Some(path) = path.parent() {
|
||||
tokio::fs::create_dir_all(path).await?;
|
||||
}
|
||||
tokio::fs::write(path, &html).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_static_route<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
regenerate: Vec<RegenerationFn>,
|
||||
) -> Route
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let handler = move |req: HttpRequest, data: Data<LeptosOptions>| {
|
||||
Box::pin({
|
||||
let app_fn = app_fn.clone();
|
||||
let additional_context = additional_context.clone();
|
||||
let regenerate = regenerate.clone();
|
||||
async move {
|
||||
let options = data.into_inner();
|
||||
let orig_path = req.uri().path();
|
||||
let path = static_path(&options, orig_path);
|
||||
let path = Path::new(&path);
|
||||
let exists = tokio::fs::try_exists(path).await.unwrap_or(false);
|
||||
|
||||
let (response_options, html) = if !exists {
|
||||
let path = ResolvedStaticPath::new(orig_path);
|
||||
|
||||
let (owner, html) = path
|
||||
.build(
|
||||
move |path: &ResolvedStaticPath| {
|
||||
StaticRouteGenerator::render_route(
|
||||
path.to_string(),
|
||||
app_fn.clone(),
|
||||
additional_context.clone(),
|
||||
)
|
||||
},
|
||||
move |path: &ResolvedStaticPath,
|
||||
owner: &Owner,
|
||||
html: String| {
|
||||
let options = options.clone();
|
||||
let path = path.to_owned();
|
||||
let response_options = owner.with(use_context);
|
||||
async move {
|
||||
write_static_route(
|
||||
&options,
|
||||
response_options,
|
||||
path.as_ref(),
|
||||
&html,
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
was_404,
|
||||
regenerate,
|
||||
)
|
||||
.await;
|
||||
(owner.with(use_context::<ResponseOptions>), html)
|
||||
} else {
|
||||
let headers =
|
||||
STATIC_HEADERS.get(orig_path).map(|v| v.clone());
|
||||
(headers, None)
|
||||
};
|
||||
|
||||
// if html is Some(_), it means that `was_error_response` is true and we're not
|
||||
// actually going to cache this route, just return it as HTML
|
||||
//
|
||||
// this if for thing like 404s, where we do not want to cache an endless series of
|
||||
// typos (or malicious requests)
|
||||
let mut res = ActixResponse(match html {
|
||||
Some(html) => {
|
||||
HttpResponse::Ok().content_type("text/html").body(html)
|
||||
}
|
||||
None => match NamedFile::open(path) {
|
||||
Ok(res) => res.into_response(&req),
|
||||
Err(err) => HttpResponse::InternalServerError()
|
||||
.body(err.to_string()),
|
||||
},
|
||||
});
|
||||
|
||||
if let Some(options) = response_options {
|
||||
res.extend_response(&options);
|
||||
}
|
||||
|
||||
res.0
|
||||
}
|
||||
})
|
||||
};
|
||||
web::get().to(handler)
|
||||
}
|
||||
|
||||
pub enum DataResponse<T> {
|
||||
Data(T),
|
||||
Response(actix_web::dev::Response<BoxBody>),
|
||||
}
|
||||
|
||||
// TODO static response
|
||||
/*
|
||||
fn handle_static_response<'a, IV>(
|
||||
path: &'a str,
|
||||
options: &'a LeptosOptions,
|
||||
app_fn: &'a (impl Fn() -> IV + Clone + Send + 'static),
|
||||
additional_context: &'a (impl Fn() + 'static + Clone + Send),
|
||||
res: StaticResponse,
|
||||
) -> Pin<Box<dyn Future<Output = HttpResponse<String>> + 'a>>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Box::pin(async move {
|
||||
match res {
|
||||
StaticResponse::ReturnResponse {
|
||||
body,
|
||||
status,
|
||||
content_type,
|
||||
} => {
|
||||
let mut res = HttpResponse::new(match status {
|
||||
StaticStatusCode::Ok => StatusCode::OK,
|
||||
StaticStatusCode::NotFound => StatusCode::NOT_FOUND,
|
||||
StaticStatusCode::InternalServerError => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
});
|
||||
if let Some(v) = content_type {
|
||||
res.headers_mut().insert(
|
||||
HeaderName::from_static("content-type"),
|
||||
HeaderValue::from_static(v),
|
||||
);
|
||||
}
|
||||
res.set_body(body)
|
||||
}
|
||||
StaticResponse::RenderDynamic => {
|
||||
handle_static_response(
|
||||
path,
|
||||
options,
|
||||
app_fn,
|
||||
additional_context,
|
||||
render_dynamic(
|
||||
path,
|
||||
options,
|
||||
app_fn.clone(),
|
||||
additional_context.clone(),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
.await
|
||||
}
|
||||
StaticResponse::RenderNotFound => {
|
||||
handle_static_response(
|
||||
path,
|
||||
options,
|
||||
app_fn,
|
||||
additional_context,
|
||||
not_found_page(
|
||||
tokio::fs::read_to_string(not_found_path(options))
|
||||
.await,
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
StaticResponse::WriteFile { body, path } => {
|
||||
if let Some(path) = path.parent() {
|
||||
if let Err(e) = std::fs::create_dir_all(path) {
|
||||
tracing::error!(
|
||||
"encountered error {} writing directories {}",
|
||||
e,
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Err(e) = std::fs::write(&path, &body) {
|
||||
tracing::error!(
|
||||
"encountered error {} writing file {}",
|
||||
e,
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
handle_static_response(
|
||||
path.to_str().unwrap(),
|
||||
options,
|
||||
app_fn,
|
||||
additional_context,
|
||||
StaticResponse::ReturnResponse {
|
||||
body,
|
||||
status: StaticStatusCode::Ok,
|
||||
content_type: Some("text/html"),
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn static_route<IV>(
|
||||
options: LeptosOptions,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
method: Method,
|
||||
mode: StaticMode,
|
||||
) -> Route
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
match mode {
|
||||
StaticMode::Incremental => {
|
||||
let handler = move |req: HttpRequest| {
|
||||
Box::pin({
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
let additional_context = additional_context.clone();
|
||||
async move {
|
||||
handle_static_response(
|
||||
req.path(),
|
||||
&options,
|
||||
&app_fn,
|
||||
&additional_context,
|
||||
incremental_static_route(
|
||||
tokio::fs::read_to_string(static_file_path(
|
||||
&options,
|
||||
req.path(),
|
||||
))
|
||||
.await,
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
};
|
||||
match method {
|
||||
Method::Get => web::get().to(handler),
|
||||
Method::Post => web::post().to(handler),
|
||||
Method::Put => web::put().to(handler),
|
||||
Method::Delete => web::delete().to(handler),
|
||||
Method::Patch => web::patch().to(handler),
|
||||
}
|
||||
}
|
||||
StaticMode::Upfront => {
|
||||
let handler = move |req: HttpRequest| {
|
||||
Box::pin({
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
let additional_context = additional_context.clone();
|
||||
async move {
|
||||
handle_static_response(
|
||||
req.path(),
|
||||
&options,
|
||||
&app_fn,
|
||||
&additional_context,
|
||||
upfront_static_route(
|
||||
tokio::fs::read_to_string(static_file_path(
|
||||
&options,
|
||||
req.path(),
|
||||
))
|
||||
.await,
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
};
|
||||
match method {
|
||||
Method::Get => web::get().to(handler),
|
||||
Method::Post => web::post().to(handler),
|
||||
Method::Put => web::put().to(handler),
|
||||
Method::Delete => web::delete().to(handler),
|
||||
Method::Patch => web::patch().to(handler),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/// This trait allows one to pass a list of routes and a render function to Actix's router, letting us avoid
|
||||
/// having to use wildcards or manually define all routes in multiple places.
|
||||
pub trait LeptosRoutes {
|
||||
@@ -1205,7 +1323,10 @@ where
|
||||
InitError = (),
|
||||
>,
|
||||
{
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
paths: Vec<ActixRouteListing>,
|
||||
@@ -1217,7 +1338,10 @@ where
|
||||
self.leptos_routes_with_context(paths, || {}, app_fn)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
paths: Vec<ActixRouteListing>,
|
||||
@@ -1247,19 +1371,15 @@ where
|
||||
provide_context(method);
|
||||
additional_context();
|
||||
};
|
||||
router = if let Some(static_mode) = listing.static_mode() {
|
||||
_ = static_mode;
|
||||
todo!() /*
|
||||
router.route(
|
||||
path,
|
||||
static_route(
|
||||
app_fn.clone(),
|
||||
additional_context_and_method.clone(),
|
||||
method,
|
||||
static_mode,
|
||||
),
|
||||
)
|
||||
*/
|
||||
router = if matches!(listing.mode(), SsrMode::Static(_)) {
|
||||
router.route(
|
||||
path,
|
||||
handle_static_route(
|
||||
additional_context_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
listing.regenerate.clone(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
router.route(
|
||||
path,
|
||||
@@ -1291,6 +1411,7 @@ where
|
||||
app_fn.clone(),
|
||||
method,
|
||||
),
|
||||
_ => unreachable!()
|
||||
},
|
||||
)
|
||||
};
|
||||
@@ -1304,7 +1425,10 @@ where
|
||||
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
|
||||
/// to those paths to Leptos's renderer.
|
||||
impl LeptosRoutes for &mut ServiceConfig {
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
paths: Vec<ActixRouteListing>,
|
||||
@@ -1316,7 +1440,10 @@ impl LeptosRoutes for &mut ServiceConfig {
|
||||
self.leptos_routes_with_context(paths, || {}, app_fn)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
paths: Vec<ActixRouteListing>,
|
||||
@@ -1341,7 +1468,17 @@ impl LeptosRoutes for &mut ServiceConfig {
|
||||
let mode = listing.mode();
|
||||
|
||||
for method in listing.methods() {
|
||||
router = router.route(
|
||||
if matches!(listing.mode(), SsrMode::Static(_)) {
|
||||
router = router.route(
|
||||
path,
|
||||
handle_static_route(
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
listing.regenerate.clone(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
router = router.route(
|
||||
path,
|
||||
match mode {
|
||||
SsrMode::OutOfOrder => {
|
||||
@@ -1371,8 +1508,10 @@ impl LeptosRoutes for &mut ServiceConfig {
|
||||
app_fn.clone(),
|
||||
method,
|
||||
),
|
||||
_ => unreachable!()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ hydration_context = { workspace = true }
|
||||
axum = { version = "0.7.5", default-features = false, features = [
|
||||
"matched-path",
|
||||
] }
|
||||
dashmap = "6"
|
||||
futures = "0.3.30"
|
||||
http = "1.1"
|
||||
http-body-util = "0.1.2"
|
||||
@@ -23,12 +24,13 @@ leptos_macro = { workspace = true, features = ["axum"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
once_cell = "1"
|
||||
parking_lot = "0.12.3"
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.39", default-features = false }
|
||||
tower = "0.4.13"
|
||||
tower-http = "0.5.2"
|
||||
tracing = "0.1.40"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
axum = "0.7.5"
|
||||
@@ -36,8 +38,12 @@ tokio = { version = "1.39", features = ["net", "rt-multi-thread"] }
|
||||
|
||||
[features]
|
||||
wasm = []
|
||||
default = ["tokio/fs", "tokio/sync", "tower-http/fs"]
|
||||
default = ["tokio/fs", "tokio/sync", "tower-http/fs", "tower/util"]
|
||||
islands-router = []
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing"]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,6 @@ leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_config = { workspace = true }
|
||||
reactive_graph = { workspace = true, features = ["sandboxed-arenas"] }
|
||||
tracing = "0.1.40"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
@@ -5,6 +5,7 @@ use leptos::{
|
||||
reactive_graph::owner::{Owner, Sandboxed},
|
||||
IntoView,
|
||||
};
|
||||
use leptos_config::LeptosOptions;
|
||||
use leptos_meta::ServerMetaContextOutput;
|
||||
use std::{future::Future, pin::Pin, sync::Arc};
|
||||
|
||||
@@ -58,7 +59,7 @@ pub trait ExtendResponse: Sized {
|
||||
// drop the owner, cleaning up the reactive runtime,
|
||||
// once the stream is over
|
||||
.chain(once(async move {
|
||||
drop(owner);
|
||||
owner.unset();
|
||||
Default::default()
|
||||
})),
|
||||
));
|
||||
@@ -132,3 +133,13 @@ where
|
||||
}));
|
||||
(owner, stream)
|
||||
}
|
||||
|
||||
pub fn static_file_path(options: &LeptosOptions, path: &str) -> String {
|
||||
let trimmed_path = path.trim_start_matches('/');
|
||||
let path = if trimmed_path.is_empty() {
|
||||
"index"
|
||||
} else {
|
||||
trimmed_path
|
||||
};
|
||||
format!("{}/{}.html", options.site_root, path)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ reactive_graph = { workspace = true, features = ["serde"] }
|
||||
rustc-hash = "2.0"
|
||||
tachys = { workspace = true, features = ["reactive_graph", "oco"] }
|
||||
thiserror = "1.0"
|
||||
tracing = "0.1.40"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
typed-builder = "0.19.1"
|
||||
typed-builder-macro = "0.19.1"
|
||||
serde = "1.0"
|
||||
@@ -56,6 +56,7 @@ hydration = [
|
||||
"reactive_graph/hydration",
|
||||
"leptos_server/hydration",
|
||||
"hydration_context/browser",
|
||||
"leptos_dom/hydration"
|
||||
]
|
||||
csr = ["leptos_macro/csr", "reactive_graph/effects"]
|
||||
hydrate = [
|
||||
@@ -76,13 +77,20 @@ ssr = [
|
||||
nightly = ["leptos_macro/nightly", "reactive_graph/nightly", "tachys/nightly"]
|
||||
rkyv = ["server_fn/rkyv"]
|
||||
tracing = [
|
||||
"dep:tracing",
|
||||
"reactive_graph/tracing",
|
||||
"tachys/tracing",
|
||||
] #, "leptos_macro/tracing", "leptos_dom/tracing"]
|
||||
"leptos_macro/tracing",
|
||||
"leptos_dom/tracing",
|
||||
"leptos_server/tracing",
|
||||
]
|
||||
nonce = ["base64", "rand"]
|
||||
spin = ["leptos-spin-macro"]
|
||||
experimental-islands = ["leptos_macro/experimental-islands", "dep:serde_json"]
|
||||
trace-component-props = ["leptos_macro/trace-component-props"]
|
||||
trace-component-props = [
|
||||
"leptos_macro/trace-component-props",
|
||||
"leptos_dom/trace-component-props"
|
||||
]
|
||||
delegation = ["tachys/delegation"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
@@ -93,52 +101,22 @@ denylist = [
|
||||
"rustls",
|
||||
"default-tls",
|
||||
"wasm-bindgen",
|
||||
"rkyv", # was causing clippy issues on nightly
|
||||
"rkyv", # was causing clippy issues on nightly
|
||||
"trace-component-props",
|
||||
"spin",
|
||||
"experimental-islands",
|
||||
]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"csr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"ssr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"serde-lite",
|
||||
],
|
||||
[
|
||||
"serde-lite",
|
||||
"miniserde",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"miniserde",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"rkyv",
|
||||
],
|
||||
[
|
||||
"miniserde",
|
||||
"rkyv",
|
||||
],
|
||||
[
|
||||
"serde-lite",
|
||||
"rkyv",
|
||||
],
|
||||
[
|
||||
"default-tls",
|
||||
"rustls",
|
||||
],
|
||||
["csr", "ssr"],
|
||||
["csr", "hydrate"],
|
||||
["ssr", "hydrate"],
|
||||
["serde", "serde-lite"],
|
||||
["serde-lite", "miniserde"],
|
||||
["serde", "miniserde"],
|
||||
["serde", "rkyv"],
|
||||
["miniserde", "rkyv"],
|
||||
["serde-lite", "rkyv"],
|
||||
["default-tls", "rustls"],
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
||||
@@ -43,10 +43,7 @@ use leptos_reactive::{
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
|
||||
#[component]
|
||||
pub fn AnimatedShow(
|
||||
/// The components Show wraps
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
//!
|
||||
//! #[component]
|
||||
//! fn MyComponent(
|
||||
//! #[prop(into)] render_number: Callback<i32, String>,
|
||||
//! #[prop(into)] render_number: Callback<(i32,), String>,
|
||||
//! ) -> impl IntoView {
|
||||
//! view! {
|
||||
//! <div>
|
||||
//! {render_number.run(1)}
|
||||
//! {render_number.run((1,))}
|
||||
//! // callbacks can be called multiple times
|
||||
//! {render_number.run(42)}
|
||||
//! {render_number.run((42,))}
|
||||
//! </div>
|
||||
//! }
|
||||
//! }
|
||||
@@ -85,17 +85,39 @@ impl<In: 'static, Out: 'static> Callable<In, Out> for UnsyncCallback<In, Out> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, In, T, Out> From<F> for UnsyncCallback<In, Out>
|
||||
where
|
||||
F: Fn(In) -> T + 'static,
|
||||
T: Into<Out> + 'static,
|
||||
In: 'static,
|
||||
{
|
||||
fn from(f: F) -> Self {
|
||||
Self::new(move |x| f(x).into())
|
||||
}
|
||||
macro_rules! impl_unsync_callable_from_fn {
|
||||
($($arg:ident),*) => {
|
||||
impl<F, $($arg,)* T, Out> From<F> for UnsyncCallback<($($arg,)*), Out>
|
||||
where
|
||||
F: Fn($($arg),*) -> T + 'static,
|
||||
T: Into<Out> + 'static,
|
||||
$($arg: 'static,)*
|
||||
{
|
||||
fn from(f: F) -> Self {
|
||||
paste::paste!(
|
||||
Self::new(move |($([<$arg:lower>],)*)| f($([<$arg:lower>]),*).into())
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_unsync_callable_from_fn!();
|
||||
impl_unsync_callable_from_fn!(P1);
|
||||
impl_unsync_callable_from_fn!(P1, P2);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3, P4);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11);
|
||||
impl_unsync_callable_from_fn!(
|
||||
P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12
|
||||
);
|
||||
|
||||
/// Callbacks define a standard way to store functions and closures.
|
||||
///
|
||||
/// # Example
|
||||
@@ -104,11 +126,11 @@ where
|
||||
/// # use leptos::callback::{Callable, Callback};
|
||||
/// #[component]
|
||||
/// fn MyComponent(
|
||||
/// #[prop(into)] render_number: Callback<i32, String>,
|
||||
/// #[prop(into)] render_number: Callback<(i32,), String>,
|
||||
/// ) -> impl IntoView {
|
||||
/// view! {
|
||||
/// <div>
|
||||
/// {render_number.run(42)}
|
||||
/// {render_number.run((42,))}
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
@@ -148,17 +170,37 @@ impl<In, Out> Clone for Callback<In, Out> {
|
||||
|
||||
impl<In, Out> Copy for Callback<In, Out> {}
|
||||
|
||||
impl<F, In, T, Out> From<F> for Callback<In, Out>
|
||||
where
|
||||
F: Fn(In) -> T + Send + Sync + 'static,
|
||||
T: Into<Out> + 'static,
|
||||
In: Send + Sync + 'static,
|
||||
{
|
||||
fn from(f: F) -> Self {
|
||||
Self::new(move |x| f(x).into())
|
||||
}
|
||||
macro_rules! impl_callable_from_fn {
|
||||
($($arg:ident),*) => {
|
||||
impl<F, $($arg,)* T, Out> From<F> for Callback<($($arg,)*), Out>
|
||||
where
|
||||
F: Fn($($arg),*) -> T + Send + Sync + 'static,
|
||||
T: Into<Out> + 'static,
|
||||
$($arg: Send + Sync + 'static,)*
|
||||
{
|
||||
fn from(f: F) -> Self {
|
||||
paste::paste!(
|
||||
Self::new(move |($([<$arg:lower>],)*)| f($([<$arg:lower>]),*).into())
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_callable_from_fn!();
|
||||
impl_callable_from_fn!(P1);
|
||||
impl_callable_from_fn!(P1, P2);
|
||||
impl_callable_from_fn!(P1, P2, P3);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4, P5);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12);
|
||||
|
||||
impl<In: 'static, Out: 'static> Callback<In, Out> {
|
||||
/// Creates a new callback from the given function.
|
||||
pub fn new<F>(fun: F) -> Self
|
||||
@@ -190,11 +232,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn runback_from() {
|
||||
let _callback: Callback<(), String> = (|()| "test").into();
|
||||
let _callback: Callback<(), String> = (|| "test").into();
|
||||
let _callback: Callback<(i32, String), String> =
|
||||
(|num, s| format!("{num} {s}")).into();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_callback_from() {
|
||||
let _callback: UnsyncCallback<(), String> = (|()| "test").into();
|
||||
let _callback: UnsyncCallback<(), String> = (|| "test").into();
|
||||
let _callback: UnsyncCallback<(i32, String), String> =
|
||||
(|num, s| format!("{num} {s}")).into();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use leptos_macro::component;
|
||||
use reactive_graph::{
|
||||
computed::ArcMemo,
|
||||
effect::RenderEffect,
|
||||
owner::Owner,
|
||||
owner::{provide_context, Owner},
|
||||
signal::ArcRwSignal,
|
||||
traits::{Get, Update, With, WithUntracked},
|
||||
};
|
||||
@@ -13,6 +13,7 @@ use std::{fmt::Debug, marker::PhantomData, sync::Arc};
|
||||
use tachys::{
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
reactive_graph::OwnedView,
|
||||
renderer::Renderer,
|
||||
ssr::StreamBuilder,
|
||||
view::{
|
||||
@@ -96,17 +97,25 @@ where
|
||||
let hook = hook as Arc<dyn ErrorHook>;
|
||||
|
||||
let _guard = throw_error::set_error_hook(Arc::clone(&hook));
|
||||
let children = children.into_inner()();
|
||||
|
||||
ErrorBoundaryView {
|
||||
hook,
|
||||
boundary_id,
|
||||
errors_empty,
|
||||
children,
|
||||
errors,
|
||||
fallback,
|
||||
rndr: PhantomData,
|
||||
}
|
||||
let owner = Owner::new();
|
||||
let children = owner.with(|| {
|
||||
provide_context(Arc::clone(&hook));
|
||||
children.into_inner()()
|
||||
});
|
||||
|
||||
OwnedView::new_with_owner(
|
||||
ErrorBoundaryView {
|
||||
hook,
|
||||
boundary_id,
|
||||
errors_empty,
|
||||
children,
|
||||
errors,
|
||||
fallback,
|
||||
rndr: PhantomData,
|
||||
},
|
||||
owner,
|
||||
)
|
||||
}
|
||||
|
||||
struct ErrorBoundaryView<Chil, FalFn, Rndr> {
|
||||
|
||||
@@ -69,10 +69,7 @@ use web_sys::{
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
|
||||
#[component]
|
||||
pub fn ActionForm<ServFn>(
|
||||
/// The action from which to build the form. This should include a URL, which can be generated
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user