mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
2 Commits
v0.8.13
...
context-do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ed0bc5abf | ||
|
|
d40c48ab9b |
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,3 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: gbj
|
||||
43
.github/ISSUE_TEMPLATE/bug_report.md
vendored
43
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,43 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Leptos Dependencies**
|
||||
|
||||
Please copy and paste the Leptos dependencies and features from your `Cargo.toml`.
|
||||
|
||||
For example:
|
||||
```toml
|
||||
leptos = { version = "0.3", features = ["serde"] }
|
||||
leptos_axum = { version = "0.3", optional = true }
|
||||
leptos_meta = { version = "0.3"}
|
||||
leptos_router = { version = "0.3"}
|
||||
```
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Next Steps**
|
||||
|
||||
- [ ] I will make a PR
|
||||
- [ ] I would like to make a PR, but need help getting started
|
||||
- [ ] I want someone else to take the time to fix this
|
||||
- [ ] This is a low priority for me and is just shared for your information
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
7
.github/ISSUE_TEMPLATE/config.yml
vendored
7
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,7 +0,0 @@
|
||||
contact_links:
|
||||
- name: Support or Question
|
||||
url: https://github.com/leptos-rs/leptos/discussions/new?category=q-a
|
||||
about: Do you need help figuring out how to do something, or want some help troubleshooting a bug? You can ask in our Discussions section.
|
||||
- name: Discord Discussions
|
||||
url: https://discord.gg/YdRAhS7eQB
|
||||
about: For more informal, real-time conversation and support, you can join our Discord server.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
19
.github/dependabot.yml
vendored
19
.github/dependabot.yml
vendored
@@ -1,19 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
# Grouping all dependencies in one PR weekly
|
||||
- package-ecosystem: cargo
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
open-pull-requests-limit: 1
|
||||
allow:
|
||||
- dependency-type: "all"
|
||||
groups:
|
||||
rust-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
39
.github/workflows/autofix.yml
vendored
39
.github/workflows/autofix.yml
vendored
@@ -1,39 +0,0 @@
|
||||
name: autofix.ci
|
||||
on:
|
||||
pull_request:
|
||||
# Running this workflow on main branch pushes requires write permission to apply changes.
|
||||
# Leave it alone for future uses.
|
||||
# push:
|
||||
# branches: ["main"]
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_BACKTRACE: 1
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
jobs:
|
||||
autofix:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with: {toolchain: "nightly-2025-07-16", components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
|
||||
- name: Install Glib
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev
|
||||
- name: Install cargo-all-features
|
||||
run: cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
- name: Install jq
|
||||
run: sudo apt-get install jq
|
||||
- name: Format the workspace
|
||||
run: cargo fmt --all
|
||||
- name: Clippy the workspace
|
||||
run: cargo all-features clippy --allow-dirty --fix --lib --no-deps
|
||||
- uses: autofix-ci/action@v1.3.2
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
fail-fast: false
|
||||
68
.github/workflows/ci.yml
vendored
68
.github/workflows/ci.yml
vendored
@@ -1,68 +0,0 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- leptos_0.6
|
||||
- leptos_0.8
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- leptos_0.6
|
||||
- leptos_0.8
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
get-leptos-matrix:
|
||||
uses: ./.github/workflows/get-leptos-matrix.yml
|
||||
get-example-changed:
|
||||
uses: ./.github/workflows/get-example-changed.yml
|
||||
get-examples-matrix:
|
||||
uses: ./.github/workflows/get-examples-matrix.yml
|
||||
test-members:
|
||||
name: CI (members)
|
||||
needs: [get-leptos-changed, get-leptos-matrix]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.get-leptos-matrix.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
test-examples:
|
||||
name: CI (examples)
|
||||
needs: [test-members, get-examples-matrix]
|
||||
if: ${{ success() }}
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.get-examples-matrix.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
test-only-examples:
|
||||
name: CI (examples)
|
||||
needs: [get-leptos-changed, get-example-changed]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed != 'true' && needs.get-example-changed.outputs.example_changed == 'true'
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.get-example-changed.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
semver-check:
|
||||
name: SemVer check (stable)
|
||||
needs: [get-leptos-changed, test-members, test-examples]
|
||||
if: ${{ success() && needs.get-leptos-changed.outputs.leptos_changed == 'true' && !contains(github.event.pull_request.labels.*.name, 'breaking') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Glib
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
- name: Semver Checks
|
||||
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
43
.github/workflows/get-example-changed.yml
vendored
43
.github/workflows/get-example-changed.yml
vendored
@@ -1,43 +0,0 @@
|
||||
name: Examples Changed Call
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
example_changed:
|
||||
description: "Example Changed"
|
||||
value: ${{ jobs.get-example-changed.outputs.example_changed }}
|
||||
# This is for test-only-examples workflow in ci.yml
|
||||
matrix:
|
||||
description: "Example Changed Directories"
|
||||
value: ${{ jobs.get-example-changed.outputs.matrix }}
|
||||
jobs:
|
||||
get-example-changed:
|
||||
name: Get Example Changed
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
example_changed: ${{ steps.set-example-changed.outputs.example_changed }}
|
||||
# This is for test-only-examples workflow in ci.yml
|
||||
matrix: ${{ steps.set-example-changed.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get example files that changed
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v47
|
||||
with:
|
||||
files: |
|
||||
examples/**
|
||||
!examples/cargo-make/**
|
||||
!examples/Makefile.toml
|
||||
!examples/*.md
|
||||
- name: List example files that changed
|
||||
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'
|
||||
- name: Install jq
|
||||
run: sudo apt-get install jq
|
||||
- name: Set example_changed
|
||||
id: set-example-changed
|
||||
run: |
|
||||
echo "example_changed=${{ steps.changed-files.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
|
||||
# This is for test-only-examples workflow in ci.yml
|
||||
echo "matrix={\"directory\": $(echo '${{ steps.changed-files.outputs.all_changed_files }}' | tr ' ' '\n' | awk -F'/' '{print $1 "/" $2}'| sort -u | jq -R -s -c 'split("\n") | .[:-1]')}" >> "$GITHUB_OUTPUT"
|
||||
36
.github/workflows/get-examples-matrix.yml
vendored
36
.github/workflows/get-examples-matrix.yml
vendored
@@ -1,36 +0,0 @@
|
||||
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@v5
|
||||
- name: Install jq
|
||||
run: sudo apt-get install jq
|
||||
- name: Set Matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
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 }}"
|
||||
pwd
|
||||
ls | sort -u
|
||||
35
.github/workflows/get-leptos-changed.yml
vendored
35
.github/workflows/get-leptos-changed.yml
vendored
@@ -1,35 +0,0 @@
|
||||
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
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
leptos_changed: ${{ steps.set-source-changed.outputs.leptos_changed }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get source files that changed
|
||||
id: changed-source
|
||||
uses: tj-actions/changed-files@v47
|
||||
with:
|
||||
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: |
|
||||
echo "leptos_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
|
||||
32
.github/workflows/get-leptos-matrix.yml
vendored
32
.github/workflows/get-leptos-matrix.yml
vendored
@@ -1,32 +0,0 @@
|
||||
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@v5
|
||||
- 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
|
||||
37
.github/workflows/publish-book.yml
vendored
37
.github/workflows/publish-book.yml
vendored
@@ -1,37 +0,0 @@
|
||||
name: Deploy book
|
||||
on:
|
||||
push:
|
||||
paths: ["docs/book/**"]
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # To push a branch
|
||||
pull-requests: write # To create a PR from that branch
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install mdbook
|
||||
run: |
|
||||
mkdir mdbook
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.27/mdbook-v0.4.27-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=./mdbook
|
||||
echo `pwd`/mdbook >> $GITHUB_PATH
|
||||
- name: Deploy GitHub Pages
|
||||
run: |
|
||||
cd docs/book
|
||||
mdbook build
|
||||
git worktree add gh-pages
|
||||
git config user.name "Deploy book from CI"
|
||||
git config user.email ""
|
||||
cd gh-pages
|
||||
# Delete the ref to avoid keeping history.
|
||||
git update-ref -d refs/heads/gh-pages
|
||||
rm -rf *
|
||||
mv ../book/* .
|
||||
git add .
|
||||
git commit -m "Deploy book $GITHUB_SHA to gh-pages"
|
||||
git push --force --set-upstream origin gh-pages
|
||||
188
.github/workflows/run-cargo-make-task.yml
vendored
188
.github/workflows/run-cargo-make-task.yml
vendored
@@ -1,188 +0,0 @@
|
||||
name: Run Task
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
directory:
|
||||
required: true
|
||||
type: string
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
RUSTFLAGS: ${{ inputs.erased_mode && '--cfg erase_components' || '' }}
|
||||
LEPTOS_TAILWIND_VERSION: v4.0.14
|
||||
LEPTOS_SASS_VERSION: 1.86.0
|
||||
jobs:
|
||||
test:
|
||||
name: "Run (${{ matrix.toolchain }}) (erased_mode: ${{ matrix.erased_mode && 'enabled' || 'disabled' }})"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
toolchain: [stable, nightly-2025-07-16]
|
||||
erased_mode: [true, false]
|
||||
steps:
|
||||
- name: Free Disk Space
|
||||
run: |
|
||||
echo "Disk space before cleanup:"
|
||||
df -h
|
||||
sudo rm -rf /usr/local/.ghcup
|
||||
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/share/boost
|
||||
sudo rm -rf /usr/local/lib/node_modules
|
||||
|
||||
# following lines currenly not needed as it takes too much time
|
||||
# the new isolated CI doesn't need much space to test libraries
|
||||
#
|
||||
# uncommet only if nneded
|
||||
#
|
||||
# sudo apt-get clean
|
||||
# sudo apt-get purge -y '^ghc-.*' '^dotnet-.*' '^llvm-.*' '^mono-.*' '^php.*' '^ruby.*'
|
||||
# sudo apt-get autoremove -y
|
||||
# sudo apt-get clean
|
||||
# sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
# docker system prune -af
|
||||
# docker image prune -af
|
||||
# docker volume prune -f
|
||||
echo "Disk space after cleanup:"
|
||||
df -h
|
||||
# Setup environment
|
||||
- name: Install Glib
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
targets: wasm32-unknown-unknown
|
||||
components: clippy,rustfmt
|
||||
- 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 --locked --no-confirm
|
||||
- name: Install cargo-make
|
||||
run: cargo binstall cargo-make --no-confirm
|
||||
- name: Install nextest
|
||||
run: cargo binstall cargo-nextest --no-confirm
|
||||
- name: Install cargo-all-features
|
||||
run: cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
# Part of direct-minimal-versions check
|
||||
- name: Install cargo-hack
|
||||
if: contains(matrix.toolchain, 'nightly')
|
||||
uses: taiki-e/install-action@cargo-hack
|
||||
# Part of direct-minimal-versions check
|
||||
- name: Install cargo-minimal-versions
|
||||
if: contains(matrix.toolchain, 'nightly')
|
||||
uses: taiki-e/install-action@cargo-minimal-versions
|
||||
- name: Install Trunk
|
||||
if: contains(inputs.directory, 'examples')
|
||||
run: cargo binstall trunk --no-confirm
|
||||
- name: Print Trunk Version
|
||||
if: contains(inputs.directory, 'examples')
|
||||
run: trunk --version
|
||||
- name: Install Node.js
|
||||
if: contains(inputs.directory, 'examples')
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
if: contains(inputs.directory, 'examples')
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
- name: Get pnpm store directory
|
||||
if: contains(inputs.directory, 'examples')
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v4
|
||||
if: contains(inputs.directory, 'examples')
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Maybe install chromedriver
|
||||
if: contains(inputs.directory, 'examples')
|
||||
run: |
|
||||
project_makefile='${{inputs.directory}}/Makefile.toml'
|
||||
webdriver_count=$(cat $project_makefile | grep "cargo-make/webdriver.toml" | wc -l)
|
||||
if [ $webdriver_count -eq 1 ]; then
|
||||
if ! command -v chromedriver &>/dev/null; then
|
||||
echo chromedriver required
|
||||
sudo apt-get update
|
||||
sudo apt-get install chromium-chromedriver
|
||||
else
|
||||
echo chromedriver is already installed
|
||||
fi
|
||||
else
|
||||
echo chromedriver is not required
|
||||
fi
|
||||
- name: Maybe install playwright browser dependencies
|
||||
if: contains(inputs.directory, 'examples')
|
||||
run: |
|
||||
for pw_path in $(find '${{inputs.directory}}' -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
if [ ! -v $pw_dir ]; then
|
||||
echo "Playwright required in $pw_dir"
|
||||
cd $pw_dir
|
||||
pnpm dlx playwright install --with-deps
|
||||
else
|
||||
echo Playwright is not required
|
||||
fi
|
||||
done
|
||||
- name: Install Deno
|
||||
if: contains(inputs.directory, 'examples')
|
||||
uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v1.x
|
||||
- name: Maybe install gtk-rs dependencies
|
||||
if: contains(inputs.directory, 'gtk')
|
||||
run: |
|
||||
sudo apt-get install -y libglib2.0-dev libgio2.0-cil-dev libgraphene-1.0-dev libcairo2-dev libpango1.0-dev libgtk-4-dev
|
||||
- name: Install Tailwind and Sass dependencies
|
||||
if: contains(inputs.directory, 'examples')
|
||||
run: |
|
||||
cd '${{ inputs.directory }}'
|
||||
tailwindcss_version=$(echo "$LEPTOS_TAILWIND_VERSION" | sed 's/^v//')
|
||||
sass_version="$LEPTOS_SASS_VERSION"
|
||||
pnpm add "tailwindcss@$tailwindcss_version" "@tailwindcss/cli@$tailwindcss_version" "sass@$sass_version"
|
||||
|
||||
echo "Tailwind CSS version:"
|
||||
./node_modules/.bin/tailwindcss --version
|
||||
|
||||
echo "Sass version:"
|
||||
./node_modules/.bin/sass --version
|
||||
# Run Cargo Make Task
|
||||
- name: ${{ inputs.cargo_make_task }}
|
||||
run: |
|
||||
cd '${{ inputs.directory }}'
|
||||
cargo make --no-workspace --profile=github-actions ci
|
||||
# check the direct-minimal-versions on release
|
||||
COMMIT_MSG=$(git log -1 --pretty=format:'%s')
|
||||
# Supports: v1.2.3, v1.2.3-alpha, v1.2.3-beta1, v1.2.3-rc.1, etc.
|
||||
if [[ "$COMMIT_MSG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.?[0-9]+)?)?$ ]]; then
|
||||
cargo make --no-workspace --profile=github-actions check-minimal-versions
|
||||
fi
|
||||
# Check if the counter_isomorphic can be built with leptos_debuginfo cfg flag in release mode
|
||||
- name: ${{ inputs.cargo_make_task }} with --cfg=leptos_debuginfo
|
||||
if: contains(inputs.directory, 'counter_isomorphic')
|
||||
run: |
|
||||
cd '${{ inputs.directory }}'
|
||||
RUSTFLAGS="$RUSTFLAGS --cfg leptos_debuginfo" cargo leptos build --release
|
||||
- name: Clean up ${{ inputs.directory }}
|
||||
if: always()
|
||||
run: |
|
||||
cd '${{ inputs.directory }}'
|
||||
cargo clean || true
|
||||
rm -rf node_modules || true
|
||||
51
.github/workflows/test.yml
vendored
Normal file
51
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test on ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- name: Cargo cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ matrix.rust }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Run tests with all features
|
||||
run: cargo make ci
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -3,15 +3,7 @@ dist
|
||||
pkg
|
||||
comparisons
|
||||
blob.rs
|
||||
**/projects/**/Cargo.lock
|
||||
**/examples/**/Cargo.lock
|
||||
**/benchmarks/**/Cargo.lock
|
||||
Cargo.lock
|
||||
**/*.rs.bk
|
||||
.DS_Store
|
||||
.idea
|
||||
.direnv
|
||||
.envrc
|
||||
|
||||
.vscode
|
||||
vendor
|
||||
hash.txt
|
||||
.leptos.kdl
|
||||
|
||||
231
ARCHITECTURE.md
231
ARCHITECTURE.md
@@ -1,231 +0,0 @@
|
||||
# Architecture
|
||||
|
||||
The goal of this document is to make it easier for contributors (and anyone
|
||||
who’s interested!) to understand the architecture of the framework.
|
||||
|
||||
The whole Leptos framework is built from a series of layers. Each of these layers
|
||||
depends on the one below it, but each can be used independently from the ones
|
||||
built on top of it. While running a command like `cargo leptos new --git
|
||||
leptos-rs/start` pulls in the whole framework, it’s important to remember that
|
||||
none of this is magic: each layer of that onion can be stripped away and
|
||||
reimplemented, configured, or adapted as needed, incrementally.
|
||||
|
||||
> Everything that follows will assume you have a good working understanding
|
||||
> of the framework. There will be explanations of how some parts of it work
|
||||
> or fit together, but these are not docs. They assume you know what I’m
|
||||
> talking about.
|
||||
|
||||
## The Reactive System: `leptos_reactive`
|
||||
|
||||
The reactive system allows you to define dynamic values (signals),
|
||||
the relationships between them (derived signals and memos), and the side effects
|
||||
that run in response to them (effects).
|
||||
|
||||
These concepts are completely independent of the DOM and can be used to drive
|
||||
any kind of reactive updates. The reactive system is based on the assumption
|
||||
that data is relatively cheap, and side effects are relatively expensive. Its
|
||||
goal is to minimize those side effects (like updating the DOM or making a network
|
||||
requests) as infrequently as possible.
|
||||
|
||||
The reactive system is implemented as a single data structure that exists at
|
||||
runtime. In exchange for giving ownership over a value to the reactive system
|
||||
(by creating a signal), you receive a `Copy + 'static` identifier for its
|
||||
location in the reactive system. This enables most of the ergonomics of storing
|
||||
and sharing state, the use of callback closures without lifetime issues, etc.
|
||||
This is implemented by storing signals in a slotmap arena. The signal, memo,
|
||||
and scope types that are exposed to users simply carry around an index into that
|
||||
slotmap.
|
||||
|
||||
> Items owned by the reactive system are dropped when the corresponding reactive
|
||||
> scope is dropped, i.e., when the component or section of the UI they’re
|
||||
> created in is removed. In a sense, Leptos implements a “garbage collector”
|
||||
> in which the lifetime of data is tied to the lifetime of the UI, not Rust’s
|
||||
> lexical scopes.
|
||||
|
||||
## The DOM Renderer: `leptos_dom`
|
||||
|
||||
The reactive system can be used to drive any kinds of side effects. One very
|
||||
common side effect is calling an imperative method, for example to update the
|
||||
DOM.
|
||||
|
||||
The entire DOM renderer is built on top of the reactive system. It provides
|
||||
a builder pattern that can be used to create DOM elements dynamically.
|
||||
|
||||
The renderer assumes, as a convention, that dynamic attributes, classes,
|
||||
styles, and children are defined by being passed a `Fn() -> T`, where their
|
||||
static equivalents just receive `T`. There’s nothing about this that is
|
||||
divinely ordained, but it’s a useful convention because it allows us to use
|
||||
zero-overhead derived signals as one of several ways to indicate dynamic
|
||||
content.
|
||||
|
||||
`leptos_dom` also contains code for server-side rendering of the same
|
||||
UI views to HTML, either for out-of-order streaming (`src/ssr.rs`) or
|
||||
in-order streaming/async rendering (`src/ssr_in_order.rs`).
|
||||
|
||||
## The Macros: `leptos_macro`
|
||||
|
||||
It’s entirely possible to write Leptos code with no macros at all. The
|
||||
`view` and `component` macros, the most common, can be replaced by
|
||||
the builder syntax and simple functions (see the `counter_without_macros`
|
||||
example). But the macros enable a JSX-like syntax for describing views.
|
||||
|
||||
This package also contains the `Params` derive macro used for typed
|
||||
queries and route params in the router.
|
||||
|
||||
### Macro-based Optimizations
|
||||
|
||||
Leptos 0.0.x was built much more heavily on macros. Taking its cues
|
||||
from SolidJS, the `view` macro emitted different code for CSR, SSR, and
|
||||
hydration, optimizing each. The CSR/hydrate versions worked by compiling
|
||||
the view to an HTML template string, cloning that `<template>`, and
|
||||
traversing the DOM to set up reactivity. The SSR version worked similarly
|
||||
by compiling the static parts of the view to strings at compile time,
|
||||
reducing the amount of work that needed to be done on each request.
|
||||
|
||||
Proc macros are hard, and this system was brittle. 0.1 introduced a
|
||||
more robust renderer, including the builder syntax, and rebuilt the `view`
|
||||
macro to use that builder syntax instead. It moved the optimized-but-buggy
|
||||
CSR version of the macro to a more-limited `template` macro.
|
||||
|
||||
The `view` macro now separately optimizes SSR to use the same static-string
|
||||
optimizations, which (by our benchmarks) makes Leptos about 3-4x faster
|
||||
than similar Rust frontend frameworks in its HTML rendering.
|
||||
|
||||
> The optimization is pretty straightforward. Consider the following view:
|
||||
>
|
||||
> ```rust
|
||||
> view! { cx,
|
||||
> <main class="text-center">
|
||||
> <div class="flex-col">
|
||||
> <button>"Click me."</button>
|
||||
> <p class="italic">"Text."</p>
|
||||
> </div>
|
||||
> </main>
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> Internally, with the builder this is something like
|
||||
>
|
||||
> ```rust
|
||||
> Element {
|
||||
> tag: "main",
|
||||
> attrs: vec![("class", "text-center")],
|
||||
> children: vec![
|
||||
> Element {
|
||||
> tag: "div",
|
||||
> attrs: vec![("class", "flex-col")],
|
||||
> children: vec![
|
||||
> Element {
|
||||
> tag: "button",
|
||||
> attrs: vec![],
|
||||
> children: vec!["Click me"]
|
||||
> },
|
||||
> Element {
|
||||
> tag: "p",
|
||||
> attrs: vec![("class", "italic")],
|
||||
> children: vec!["Text"]
|
||||
> }
|
||||
> ]
|
||||
> }
|
||||
> ]
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> This is a _bunch_ of small allocations and separate strings,
|
||||
> and in early 0.1 versions we used a `SmallVec` for children and
|
||||
> attributes and actually caused some stack overflows.
|
||||
>
|
||||
> But if you look at the view itself you can see that none of this
|
||||
> will _ever_ change. So we can actually optimize it at compile
|
||||
> time to a single `&'static str`:
|
||||
>
|
||||
> ```rust
|
||||
> r#"<main class="text-center">
|
||||
> <div class="flex-col">
|
||||
> <button>"Click me."</button>
|
||||
> <p class="italic">"Text."</p>
|
||||
> </div>
|
||||
> </main>"#
|
||||
> ```
|
||||
|
||||
## Server Functions (`leptos_server`, `server_fn`, and `server_fn_macro`)
|
||||
|
||||
Server functions are a framework-agnostic shorthand for converting
|
||||
a function, whose body can only be run on the server, into an ad hoc
|
||||
REST API endpoint, and then generating code on the client to call that
|
||||
endpoint when you call the function.
|
||||
|
||||
These are inspired by Solid/Bling’s `server$` functions, and there’s
|
||||
similar work being done in a number of other JavaScript frameworks.
|
||||
|
||||
RPC is not a new idea, but these kinds of server functions may be.
|
||||
Specifically, by using web standards (defaulting to `POST`/`GET` requests
|
||||
with URL-encoded form data) they allow easy graceful degradation and the
|
||||
use of the `<form>` element.
|
||||
|
||||
This function is split across three packages so that `server_fn` and
|
||||
`server_fn_macro` can be used by other frameworks. `leptos_server`
|
||||
includes some Leptos-specific reactive functionality (like actions).
|
||||
|
||||
## `leptos`
|
||||
|
||||
This package is built on and reexports most of the layers already
|
||||
mentioned, and implements a number of control-flow components (`<Show/>`,
|
||||
`<ErrorBoundary/>`, `<For/>`, `<Suspense/>`, `<Transition/>`) that use
|
||||
public APIs of the other packages.
|
||||
|
||||
This is the main entrypoint for users, but is relatively light itself.
|
||||
|
||||
## `leptos_meta`
|
||||
|
||||
This package exists to allow you to work with tags normally found in
|
||||
the `<head>`, from within your components.
|
||||
|
||||
It is implemented as a distinct package, rather than part of
|
||||
`leptos_dom`, on the principle that “what can be implemented in userland,
|
||||
should be.” The framework can be used without it, so it’s not in core.
|
||||
|
||||
## `leptos_router`
|
||||
|
||||
The router originates as a direct port of `solid-router`, which is the
|
||||
origin of most of its terminology, architecture, and route-matching logic.
|
||||
|
||||
Subsequent developments (like animated routing, and managing route transitions
|
||||
given the lack of `useTransition` in Leptos) have caused it to diverge
|
||||
slightly from Solid’s exact code, but it is still very closely related.
|
||||
|
||||
The core principle here is “nested routing,” dividing a single page
|
||||
into independently-rendered parts. This is described in some detail in the docs.
|
||||
|
||||
Like `leptos_meta`, it is implemented as a distinct package, because it
|
||||
can be replaced with another router or with none. The framework can be used
|
||||
without it, so it’s not in core.
|
||||
|
||||
## Server Integrations
|
||||
|
||||
The server integrations are the most “frameworky” layer of the whole framework.
|
||||
These **do** assume the use of `leptos`, `leptos_router`, and `leptos_meta`.
|
||||
They specifically draw routing data from `leptos_router`, and inject the
|
||||
metadata from `leptos_meta` into the `<head>` appropriately.
|
||||
|
||||
But of course, if you one day create `leptos-helmet` and `leptos-better-router`,
|
||||
you can create new server integrations that plug them into the SSR rendering
|
||||
methods from `leptos_dom` instead. Everything involved is quite modular.
|
||||
|
||||
These packages essentially provide helpers that save the templates and user apps
|
||||
from including a huge amount of boilerplate to connect the various other packages
|
||||
correctly. Again, early versions of the framework examples are illustrative here
|
||||
for reference: they include large amounts of manual SSR route handling, etc.
|
||||
|
||||
## `cargo-leptos` helpers
|
||||
|
||||
`leptos_config` and `leptos_hot_reload` exist to support two different features
|
||||
of `cargo-leptos`, namely its configuration and its view-patching/hot-reloading
|
||||
features.
|
||||
|
||||
It’s important to say that the main feature `cargo-leptos` remains its ability
|
||||
to conveniently tie together different build tooling, compiling your app to
|
||||
WASM for the browser, building the server version, pulling in SASS and
|
||||
Tailwind, etc. It is an extremely good build tool, not a magic formula. Each
|
||||
of the examples includes instructions for how to run the examples without
|
||||
`cargo-leptos`.
|
||||
@@ -1,51 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
_This Code of Conduct is based on the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct)
|
||||
and the [Bevy Code of Conduct](https://raw.githubusercontent.com/bevyengine/bevy/main/CODE_OF_CONDUCT.md),
|
||||
which are adapted from the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling)
|
||||
and the [Contributor Covenant](https://www.contributor-covenant.org)._
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
We are a community of people learning and exploring how to build better web applications
|
||||
with Rust. When interacting with one another, please remember that there are no experts and there are
|
||||
no stupid questions. Assume the best in other people's communication, and take a step back if
|
||||
you find yourself getting defensive.
|
||||
|
||||
Please note the following guidelines as well:
|
||||
|
||||
* Please avoid using overtly sexual aliases or other nicknames that might detract from a friendly, safe and welcoming environment for all.
|
||||
* Please be kind and courteous. There’s no need to be mean or rude.
|
||||
* Respect that people have differences of opinion and that every design or implementation choice carries a trade-off and numerous costs. There is seldom a right answer.
|
||||
* Please keep unstructured critique to a minimum. If you have solid ideas you want to experiment with, make a fork and see how it works.
|
||||
* We will exclude you from interaction if you insult, demean or harass anyone. That is not welcome behavior. We interpret the term “harassment” as including the definition in the [Citizen Code of Conduct](https://github.com/stumpsyn/policies/blob/master/citizen_code_of_conduct.md); if you have any lack of clarity about what might be included in that concept, please read their definition. In particular, we don’t tolerate behavior that excludes people in socially marginalized groups.
|
||||
* Private harassment is also unacceptable. No matter who you are, if you feel you have been or are being harassed or made uncomfortable by a community member, please contact the maintainers immediately. Whether you’re a regular contributor or a newcomer, we care about making this community a safe place for you and we’ve got your back.
|
||||
* Do not make casual mention of slavery or indentured servitude and/or false comparisons of one's occupation or situation to slavery. Please consider using or asking about alternate terminology when referring to such metaphors in technology.
|
||||
* Likewise any spamming, trolling, flaming, baiting or other attention-stealing behavior is not welcome.
|
||||
|
||||
## Moderation
|
||||
|
||||
These are the policies for upholding [our community’s standards of conduct](#our-standards). If you feel that a thread needs moderation, please contact the maintainers.
|
||||
|
||||
1. Remarks that violate the community standards of conduct, including hateful, hurtful, oppressive, or exclusionary remarks, are not allowed. (Cursing is allowed, but never targeting another user, and never in a hateful manner).
|
||||
2. Remarks that maintainers find inappropriate, whether listed in the code of conduct or not, are also not allowed.
|
||||
3. Maintainers will first respond to such remarks with a warning.
|
||||
4. If the warning is unheeded, the user will be “kicked,” i.e., kicked out of the communication channel to cool off.
|
||||
5. If the user comes back and continues to make trouble, they will be banned, i.e., indefinitely excluded.
|
||||
6. Maintainers may choose at their discretion to un-ban the user if it was a first offense and they offer the offended party a genuine apology.
|
||||
7. If a maintainer bans someone and you think it was unjustified, please take it up with that maintainer, or with a different maintainer, in private. Complaints about bans in-channel are not allowed.
|
||||
8. Maintainers are held to a higher standard than other community members. If a maintainer creates an inappropriate situation, they should expect less leeway than others.
|
||||
|
||||
The enforcement policies in the code of conduct apply to all official venues, including Discord channels, GitHub repositories, and all other forums.
|
||||
101
CONTRIBUTING.md
101
CONTRIBUTING.md
@@ -1,101 +0,0 @@
|
||||
# Contributing to Leptos
|
||||
|
||||
Thanks for your interesting in contributing to Leptos! This is a truly
|
||||
community-driven framework, and while we have a central maintainer (@gbj)
|
||||
large parts of the renderer, reactive system, and server integrations have
|
||||
all been written by other contributors. Contributions are always welcome.
|
||||
|
||||
Participation in this community is governed by a [Code of Conduct](./CODE_OF_CONDUCT.md).
|
||||
Some of the most active conversations around development take place on our
|
||||
[Discord server](https://discord.gg/YdRAhS7eQB).
|
||||
|
||||
This guide seeks to
|
||||
|
||||
- describe some of the framework’s values (in a technical, not an ethical, sense)
|
||||
- provide a high-level overview of how the pieces of the framework fit together
|
||||
- orient you to the organization of this repository
|
||||
|
||||
## Values
|
||||
|
||||
Leptos, as a framework, reflects certain technical values:
|
||||
|
||||
- **Expose primitives rather than imposing patterns.** Provide building blocks
|
||||
that users can combine together to build up more complex behavior, rather than
|
||||
requiring users follow certain templates, file formats, etc. e.g., components
|
||||
are defined as functions, rather than a bespoke single-file component format.
|
||||
The reactive system feeds into the rendering system, rather than being defined
|
||||
by it.
|
||||
- **Bottom-up over top-down.** If you envision a user’s application as a tree
|
||||
(like an HTML document), push meaning toward the leaves of the tree. e.g., If data
|
||||
needs to be loaded, load it in a granular primitive (resources) rather than a
|
||||
route- or page-level data structure.
|
||||
- **Performance by default.** When possible, users should only pay for what they
|
||||
use. e.g., we don’t make all component props reactive by default. This is
|
||||
because doing so would force the overhead of a reactive prop onto props that don’t
|
||||
need to be reactive.
|
||||
- **Full-stack performance.** Performance can’t be limited to a single metric,
|
||||
whether that’s a DOM rendering benchmark, WASM binary size, or server response
|
||||
time. Use methods like HTTP streaming and progressive enhancement to enable
|
||||
applications to load, become interactive, and respond as quickly as possible.
|
||||
- **Use safe Rust.** There’s no need for `unsafe` Rust in the framework, and
|
||||
avoiding it at all costs reduces the maintenance and testing burden significantly.
|
||||
- **Embrace Rust semantics.** Especially in things like UI templating, use Rust
|
||||
semantics or extend them in a predictable way with control-flow components
|
||||
rather than overloading the meaning of Rust terms like `if` or `for` in a
|
||||
framework-specific way.
|
||||
- **Enhance ergonomics without obfuscating what’s happening.** This is by far
|
||||
the hardest to achieve. It’s often the case that adding additional layers to
|
||||
improve DX (like a custom build tool and starter templates) comes across as
|
||||
“too magic” to some people who haven’t had to build the same things manually.
|
||||
When possible, make it easier to see how the pieces fit together, without
|
||||
sacrificing the improved DX.
|
||||
|
||||
## Processes
|
||||
|
||||
We do not have PR templates or formal processes for approving PRs. But there
|
||||
are a few guidelines that will make it a better experience for everyone:
|
||||
|
||||
- Run `cargo fmt` before submitting your code.
|
||||
- Keep PRs limited to addressing one feature or one issue, in general. In some
|
||||
cases (e.g., “reduce allocations in the reactive system”) this may touch a number
|
||||
of different areas, but is still conceptually one thing.
|
||||
- If it’s an unsolicited PR not linked to an open issue, please include a
|
||||
specific explanation for what it’s trying to achieve. For example: “When I
|
||||
was trying to deploy my app under _circumstances X_, I found that the way
|
||||
_function Y_ was implemented caused _issue Z_. This PR should fix that by
|
||||
_solution._”
|
||||
- Our CI tests every PR against all the existing examples, sometimes requiring
|
||||
compilation for both server and client side, etc. It’s thorough but slow. If
|
||||
you want to run CI locally to reduce frustration, you can do that by installing
|
||||
`cargo-make` and using `cargo make check && cargo make test && cargo make
|
||||
check-examples`.
|
||||
|
||||
## Before Submitting a PR
|
||||
|
||||
We have a fairly extensive CI setup that runs both lints (like `rustfmt` and `clippy`)
|
||||
and tests on PRs. You can run most of these locally if you have `cargo-make` installed.
|
||||
|
||||
Note that some of the `rustfmt` settings used require usage of the nightly compiler.
|
||||
Formatting the code using the stable toolchain may result in a wrong code format and
|
||||
subsequently CI errors.
|
||||
Run `cargo +nightly fmt` if you want to keep the stable toolchain active.
|
||||
You may want to let your IDE automatically use the `+nightly` parameter when a
|
||||
"format on save" action is used.
|
||||
|
||||
If you added an example, make sure to add it to the list in `examples/Makefile.toml`.
|
||||
|
||||
From the root directory of the repo, run
|
||||
- `cargo +nightly fmt`
|
||||
- `cargo +nightly make check`
|
||||
- `cargo +nightly make test`
|
||||
- `cargo +nightly make check-examples`
|
||||
- `cargo +nightly make --profile=github-actions ci`
|
||||
|
||||
If you modified an example:
|
||||
- `cd examples/your_example`
|
||||
- `cargo +nightly fmt -- --config-path ../..`
|
||||
- `cargo +nightly make --profile=github-actions verify-flow`
|
||||
|
||||
## Architecture
|
||||
|
||||
See [ARCHITECTURE.md](./ARCHITECTURE.md).
|
||||
5003
Cargo.lock
generated
5003
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
216
Cargo.toml
216
Cargo.toml
@@ -1,177 +1,45 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
# utilities
|
||||
"any_spawner",
|
||||
"const_str_slice_concat",
|
||||
"either_of",
|
||||
"next_tuple",
|
||||
"oco",
|
||||
"or_poisoned",
|
||||
|
||||
# core
|
||||
"hydration_context",
|
||||
"leptos",
|
||||
"leptos_dom",
|
||||
"leptos_core",
|
||||
"leptos_config",
|
||||
"leptos_hot_reload",
|
||||
"leptos_macro",
|
||||
"leptos_reactive",
|
||||
"leptos_server",
|
||||
"reactive_graph",
|
||||
"reactive_stores",
|
||||
"reactive_stores_macro",
|
||||
"server_fn",
|
||||
"server_fn_macro",
|
||||
"server_fn/server_fn_macro_default",
|
||||
"tachys",
|
||||
|
||||
# integrations
|
||||
"integrations/actix",
|
||||
"integrations/axum",
|
||||
"integrations/utils",
|
||||
|
||||
# libraries
|
||||
"meta",
|
||||
"router",
|
||||
"router_macro",
|
||||
"any_error",
|
||||
|
||||
# examples
|
||||
"examples/cloudflare-worker",
|
||||
"examples/counter",
|
||||
"examples/counter-isomorphic",
|
||||
"examples/counters",
|
||||
"examples/counters-stable",
|
||||
"examples/fetch",
|
||||
"examples/hackernews",
|
||||
"examples/hackernews-axum",
|
||||
"examples/parent-child",
|
||||
"examples/router",
|
||||
"examples/todomvc",
|
||||
"examples/todo-app-sqlite",
|
||||
"examples/todo-app-sqlite-axum",
|
||||
"examples/todo-app-cbor",
|
||||
"examples/view-tests",
|
||||
|
||||
# book
|
||||
"docs/book/project/ch02_getting_started",
|
||||
"docs/book/project/ch03_building_ui",
|
||||
"docs/book/project/ch04_reactivity",
|
||||
]
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
rust-version = "1.88"
|
||||
|
||||
[workspace.dependencies]
|
||||
# members
|
||||
throw_error = { path = "./any_error/", version = "0.3.1" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.3.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
|
||||
either_of = { path = "./either_of/", version = "0.1.6" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.3.0" }
|
||||
leptos = { path = "./leptos", version = "0.8.12" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.7" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.7" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.5" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.6" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.11" }
|
||||
leptos_router = { path = "./router", version = "0.8.9" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.6" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.5" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.5" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0" }
|
||||
oco_ref = { path = "./oco", version = "0.2.1" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.9" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.3.0" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.6" }
|
||||
server_fn = { path = "./server_fn", version = "0.8.8" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.8" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.5" }
|
||||
tachys = { path = "./tachys", version = "0.2.10" }
|
||||
|
||||
# members deps
|
||||
async-once-cell = { default-features = false, version = "0.5.3" }
|
||||
itertools = { default-features = false, version = "0.14.0" }
|
||||
convert_case = { default-features = false, version = "0.8.0" }
|
||||
serde_json = { default-features = false, version = "1.0.143" }
|
||||
trybuild = { default-features = false, version = "1.0.110" }
|
||||
typed-builder = { default-features = false, version = "0.21.2" }
|
||||
thiserror = { default-features = false, version = "2.0.17" }
|
||||
wasm-bindgen = { default-features = false, version = "0.2.100" }
|
||||
indexmap = { default-features = false, version = "2.11.0" }
|
||||
rstml = { default-features = false, version = "0.12.1" }
|
||||
rustc_version = { default-features = false, version = "0.4.1" }
|
||||
guardian = { default-features = false, version = "1.3.0" }
|
||||
rustc-hash = { default-features = false, version = "2.1.1" }
|
||||
actix-web = { default-features = false, version = "4.11.0" }
|
||||
tracing = { default-features = false, version = "0.1.41" }
|
||||
slotmap = { default-features = false, version = "1.0.7" }
|
||||
futures = { default-features = false, version = "0.3.31" }
|
||||
dashmap = { default-features = false, version = "6.1.0" }
|
||||
pin-project-lite = { default-features = false, version = "0.2.16" }
|
||||
send_wrapper = { default-features = false, version = "0.6.0" }
|
||||
tokio-test = { default-features = false, version = "0.4.4" }
|
||||
html-escape = { default-features = false, version = "0.2.13" }
|
||||
proc-macro-error2 = { default-features = false, version = "2.0.1" }
|
||||
const_format = { default-features = false, version = "0.2.35" }
|
||||
gloo-net = { default-features = false, version = "0.6.0" }
|
||||
url = { default-features = false, version = "2.5.4" }
|
||||
tokio = { default-features = false, version = "1.47.1" }
|
||||
base64 = { default-features = false, version = "0.22.1" }
|
||||
cfg-if = { default-features = false, version = "1.0.3" }
|
||||
wasm-bindgen-futures = { default-features = false, version = "0.4.50" }
|
||||
tower = { default-features = false, version = "0.5.2" }
|
||||
proc-macro2 = { default-features = false, version = "1.0.101" }
|
||||
serde = { default-features = false, version = "1.0.219" }
|
||||
parking_lot = { default-features = false, version = "0.12.5" }
|
||||
axum = { default-features = false, version = "0.8.6" }
|
||||
serde_qs = { default-features = false, version = "0.15.0" }
|
||||
syn = { default-features = false, version = "2.0.106" }
|
||||
xxhash-rust = { default-features = false, version = "0.8.15" }
|
||||
paste = { default-features = false, version = "1.0.15" }
|
||||
quote = { default-features = false, version = "1.0.41" }
|
||||
web-sys = { default-features = false, version = "0.3.77" }
|
||||
js-sys = { default-features = false, version = "0.3.77" }
|
||||
rand = { default-features = false, version = "0.9.1" }
|
||||
serde-lite = { default-features = false, version = "0.5.0" }
|
||||
tokio-tungstenite = { default-features = false, version = "0.28.0" }
|
||||
serial_test = { default-features = false, version = "3.2.0" }
|
||||
erased = { default-features = false, version = "0.1.2" }
|
||||
glib = { default-features = false, version = "0.20.12" }
|
||||
async-trait = { default-features = false, version = "0.1.89" }
|
||||
typed-builder-macro = { default-features = false, version = "0.21.0" }
|
||||
linear-map = { default-features = false, version = "1.2.0" }
|
||||
anyhow = { default-features = false, version = "1.0.100" }
|
||||
walkdir = { default-features = false, version = "2.5.0" }
|
||||
actix-ws = { default-features = false, version = "0.3.0" }
|
||||
tower-http = { default-features = false, version = "0.6.4" }
|
||||
prettyplease = { default-features = false, version = "0.2.37" }
|
||||
inventory = { default-features = false, version = "0.3.21" }
|
||||
config = { default-features = false, version = "0.15.14" }
|
||||
camino = { default-features = false, version = "1.2.1" }
|
||||
ciborium = { default-features = false, version = "0.2.2" }
|
||||
bitcode = { default-features = false, version = "0.6.6" }
|
||||
multer = { default-features = false, version = "3.1.0" }
|
||||
leptos-spin-macro = { default-features = false, version = "0.2.0" }
|
||||
sledgehammer_utils = { default-features = false, version = "0.3.1" }
|
||||
sledgehammer_bindgen = { default-features = false, version = "0.6.0" }
|
||||
wasm-streams = { default-features = false, version = "0.4.2" }
|
||||
rkyv = { default-features = false, version = "0.8.12" }
|
||||
temp-env = { default-features = false, version = "0.3.6" }
|
||||
uuid = { default-features = false, version = "1.18.0" }
|
||||
bytes = { default-features = false, version = "1.10.1" }
|
||||
http = { default-features = false, version = "1.3.1" }
|
||||
regex = { default-features = false, version = "1.11.3" }
|
||||
drain_filter_polyfill = { default-features = false, version = "0.1.3" }
|
||||
tempfile = { default-features = false, version = "3.23.0" }
|
||||
futures-lite = { default-features = false, version = "2.6.1" }
|
||||
log = { default-features = false, version = "0.4.27" }
|
||||
percent-encoding = { default-features = false, version = "2.3.2" }
|
||||
async-executor = { default-features = false, version = "1.13.2" }
|
||||
const-str = { default-features = false, version = "0.6.4" }
|
||||
http-body-util = { default-features = false, version = "0.1.3" }
|
||||
hyper = { default-features = false, version = "1.7.0" }
|
||||
postcard = { default-features = false, version = "1.1.3" }
|
||||
rmp-serde = { default-features = false, version = "1.3.0" }
|
||||
reqwest = { default-features = false, version = "0.12.23" }
|
||||
tower-layer = { default-features = false, version = "0.3.3" }
|
||||
attribute-derive = { default-features = false, version = "0.10.5" }
|
||||
insta = { default-features = false, version = "1.43.1" }
|
||||
codee = { default-features = false, version = "0.3.0" }
|
||||
actix-http = { default-features = false, version = "3.11.2" }
|
||||
wasm-bindgen-test = { default-features = false, version = "0.3.50" }
|
||||
rustversion = { default-features = false, version = "1.0.22" }
|
||||
getrandom = { default-features = false, version = "0.3.3" }
|
||||
actix-files = { default-features = false, version = "0.6.6" }
|
||||
async-lock = { default-features = false, version = "3.4.1" }
|
||||
base16 = { default-features = false, version = "0.2.1" }
|
||||
digest = { default-features = false, version = "0.10.7" }
|
||||
sha2 = { default-features = false, version = "0.10.8" }
|
||||
subsecond = { default-features = false, version = "0.7.0-rc.0" }
|
||||
dioxus-cli-config = { default-features = false, version = "0.7.0-rc.0" }
|
||||
dioxus-devtools = { default-features = false, version = "0.7.0-rc.0" }
|
||||
wasm_split_helpers = { default-features = false, version = "0.2.0" }
|
||||
exclude = ["benchmarks"]
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
@@ -179,11 +47,29 @@ lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
[workspace.metadata.cargo-all-features]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
max_combination_size = 2
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(leptos_debuginfo)',
|
||||
'cfg(erase_components)',
|
||||
] }
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"csr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"ssr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"serde-lite",
|
||||
],
|
||||
[
|
||||
"serde-lite",
|
||||
"miniserde",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"miniserde",
|
||||
],
|
||||
]
|
||||
|
||||
@@ -3,37 +3,27 @@
|
||||
# cargo install --force cargo-make
|
||||
############
|
||||
|
||||
[env]
|
||||
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
[config]
|
||||
# make tasks run at the workspace root
|
||||
default_to_workspace = false
|
||||
|
||||
[tasks.check-stable]
|
||||
workspace = false
|
||||
[tasks.ci]
|
||||
dependencies = ["build", "test"]
|
||||
|
||||
[tasks.build]
|
||||
clear = true
|
||||
dependencies = [
|
||||
{ name = "lint", path = "examples/counter_without_macros" },
|
||||
{ name = "lint", path = "examples/counters_stable" },
|
||||
]
|
||||
dependencies = ["build-all"]
|
||||
|
||||
[tasks.ci-examples]
|
||||
workspace = false
|
||||
cwd = "examples"
|
||||
[tasks.build-all]
|
||||
command = "cargo"
|
||||
args = ["make", "ci-clean"]
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-examples]
|
||||
workspace = false
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "check-clean"]
|
||||
[tasks.test]
|
||||
clear = true
|
||||
dependencies = ["test-all"]
|
||||
|
||||
[tasks.build-examples]
|
||||
workspace = false
|
||||
cwd = "examples"
|
||||
[tasks.test-all]
|
||||
command = "cargo"
|
||||
args = ["make", "build-clean"]
|
||||
|
||||
[tasks.clean-examples]
|
||||
workspace = false
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "clean"]
|
||||
args = ["+nightly", "test-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
191
README.md
191
README.md
@@ -1,17 +1,10 @@
|
||||
<picture>
|
||||
<source srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_pref_dark_RGB.svg" media="(prefers-color-scheme: dark)">
|
||||
<img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo">
|
||||
</picture>
|
||||
**Please note:** This framework is in active development. I'm keeping it in a cycle of 0.0.x releases at the moment to indicate that it’s not even ready for its 0.1.0. Active work is being done on documentation and features, and APIs should not necessarily be considered stable. At the same time, it is more than a toy project or proof of concept, and I am actively using it for my own application development.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/gbj/leptos/main/docs/logos/logo.svg" alt="Leptos Logo" style="width: 100%; height: auto; display: block; margin: auto;">
|
||||
|
||||
[](https://crates.io/crates/leptos)
|
||||
[](https://docs.rs/leptos)
|
||||

|
||||
[](https://discord.gg/YdRAhS7eQB)
|
||||
[](https://matrix.to/#/#leptos:matrix.org)
|
||||
|
||||
[Website](https://leptos.dev) | [Book](https://leptos-rs.github.io/leptos/) | [Docs.rs](https://docs.rs/leptos/latest/leptos/) | [Playground](https://codesandbox.io/p/sandbox/leptos-rtfggt?file=%2Fsrc%2Fmain.rs%3A1%2C1) | [Discord](https://discord.gg/YdRAhS7eQB)
|
||||
|
||||
You can find a list of useful libraries and example projects at [`awesome-leptos`](https://github.com/leptos-rs/awesome-leptos).
|
||||
|
||||
# Leptos
|
||||
|
||||
@@ -19,9 +12,9 @@ You can find a list of useful libraries and example projects at [`awesome-leptos
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
|
||||
pub fn SimpleCounter(cx: Scope, initial_value: i32) -> Element {
|
||||
// create a reactive signal with the initial value
|
||||
let (value, set_value) = signal(initial_value);
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
|
||||
// create event handlers for our buttons
|
||||
// note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
|
||||
@@ -29,43 +22,23 @@ pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
|
||||
let decrement = move |_| set_value.update(|value| *value -= 1);
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
// create user interfaces with the declarative `view!` macro
|
||||
// this JSX is compiled to an HTML template string for performance
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<button on:click=clear>Clear</button>
|
||||
<button on:click=decrement>-1</button>
|
||||
// text nodes can be quoted or unquoted
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=increment>+1</button>
|
||||
<button on:click=clear>"Clear"</button>
|
||||
<button on:click=decrement>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<button on:click=increment>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
// we also support a builder syntax rather than the JSX-like `view` macro
|
||||
#[component]
|
||||
pub fn SimpleCounterWithBuilder(initial_value: i32) -> impl IntoView {
|
||||
use leptos::html::*;
|
||||
|
||||
let (value, set_value) = signal(initial_value);
|
||||
let clear = move |_| set_value(0);
|
||||
let decrement = move |_| set_value.update(|value| *value -= 1);
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
// the `view` macro above expands to this builder syntax
|
||||
div().child((
|
||||
button().on(ev::click, clear).child("Clear"),
|
||||
button().on(ev::click, decrement).child("-1"),
|
||||
span().child(("Value: ", value, "!")),
|
||||
button().on(ev::click, increment).child("+1")
|
||||
))
|
||||
}
|
||||
|
||||
// Easy to use with Trunk (trunkrs.dev) or with a simple wasm-bindgen setup
|
||||
pub fn main() {
|
||||
mount_to_body(|| view! {
|
||||
<SimpleCounter initial_value=3 />
|
||||
})
|
||||
mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=3 /> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## About the Framework
|
||||
@@ -74,89 +47,117 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
|
||||
|
||||
## What does that mean?
|
||||
|
||||
- **Full-stack**: Leptos can be used to build apps that run in the browser (client-side rendering), on the server (server-side rendering), or by rendering HTML on the server and then adding interactivity in the browser (server-side rendering with hydration). This includes support for HTTP streaming of both data ([`Resource`s](https://docs.rs/leptos/latest/leptos/struct.Resource.html)) and HTML (out-of-order or in-order streaming of [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) components.)
|
||||
- **Isomorphic**: Leptos provides primitives to write isomorphic [server functions](https://docs.rs/leptos_server/0.2.5/leptos_server/index.html), i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser, without needing to create and maintain a separate REST or other API.
|
||||
- **Web**: Leptos is built on the Web platform and Web standards. The [router](https://docs.rs/leptos_router/latest/leptos_router/) is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
|
||||
- **Full-stack**: Leptos can be used to build apps that run in the browser (_client-side rendering_), on the server (_server-side rendering_), or by rendering HTML on the server and then adding interactivity in the browser (_hydration_). This includes support for _HTTP streaming_ of both data (`Resource`s) and HTML (out-of-order streaming of `<Suspense/>` components.)
|
||||
- **Isomorphic**: Leptos provides primitives to write isomorphic server functions, i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser.
|
||||
- **Web**: Leptos is built on the Web platform and Web standards. The router is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
|
||||
- **Framework**: Leptos provides most of what you need to build a modern web app: a reactive system, templating library, and a router that works on both the server and client side.
|
||||
- **Fine-grained reactivity**: The entire framework is built from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signal’s value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (So, no virtual DOM overhead!)
|
||||
- **Fine-grained reactivity**: The entire framework is build from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signal’s value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (_So, no virtual DOM!_)
|
||||
- **Declarative**: Tell Leptos how you want the page to look, and let the framework tell the browser how to do it.
|
||||
|
||||
## Getting Started
|
||||
|
||||
The best way to get started with a Leptos project right now is to use the [`cargo-leptos`](https://github.com/akesson/cargo-leptos) build tool and our [starter template](https://github.com/leptos-rs/start).
|
||||
|
||||
```bash
|
||||
cargo install cargo-leptos
|
||||
cargo leptos new --git https://github.com/leptos-rs/start
|
||||
cd [your project name]
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
## Learn more
|
||||
|
||||
Here are some resources for learning more about Leptos:
|
||||
|
||||
- [Book](https://leptos-rs.github.io/leptos/) (work in progress)
|
||||
- [Examples](https://github.com/leptos-rs/leptos/tree/main/examples)
|
||||
- [Examples](https://github.com/gbj/leptos/tree/main/examples)
|
||||
- [API Documentation](https://docs.rs/leptos/latest/leptos/)
|
||||
- [Common Bugs](https://github.com/leptos-rs/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
|
||||
- [Common Bugs](https://github.com/gbj/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
|
||||
- Leptos Guide (in progress)
|
||||
|
||||
## `cargo-leptos`
|
||||
## `nightly` Note
|
||||
|
||||
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our starter templates for [Actix](https://github.com/leptos-rs/start) or [Axum](https://github.com/leptos-rs/start-axum).
|
||||
Most of the examples assume you’re using `nightly` Rust. If you’re on stable, note the following:
|
||||
|
||||
```bash
|
||||
cargo install cargo-leptos --locked
|
||||
cargo leptos new --git https://github.com/leptos-rs/start-axum
|
||||
cd [your project name]
|
||||
cargo leptos watch
|
||||
```
|
||||
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.0", features = ["stable"] }`
|
||||
2. `nightly` enables the function call syntax for accessing and setting signals. If you’re using `stable`,
|
||||
you’ll just call `.get()`, `.set()`, or `.update()` manually. Check out the
|
||||
[`counters-stable` example](https://github.com/gbj/leptos/blob/main/examples/counters-stable/src/main.rs)
|
||||
for examples of the correct API.
|
||||
|
||||
Open browser to [http://localhost:3000/](http://localhost:3000/).
|
||||
## Benchmarks
|
||||
|
||||
### Server-Side Rendering
|
||||
|
||||
I’ve created a benchmark comparing Leptos’s HTML rendering on the server to [Tera](https://github.com/Keats/tera), [Yew](https://github.com/yewstack/yew), and [Sycamore](https://github.com/sycamore-rs/sycamore). You can find the benchmark [here](https://github.com/gbj/leptos/tree/main/benchmarks) and run it yourself using `cargo bench`. Leptos renders HTML roughly as fast as Tera, and scales well as templates become larger. It's significantly faster than the server-side HTML rendering done by similar frameworks.
|
||||
|
||||
<details>
|
||||
<summary>Click to show results</summary>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><td><em>ns/iter</em></td><td>Tera</td><td>Leptos</td><td>Yew</td><td>Sycamore</td></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>3 Counters</td><td align="right">3,454</td><td align="right">5,666</td><td align="right">34,984</td><td align="right">32,412</td></tr>
|
||||
<tr><td>TodoMVC (no todos)</td><td align="right">2,396</td><td align="right">5,561</td><td align="right">38,725</td><td align="right">68,749</td></tr>
|
||||
<tr><td>TodoMVC (1000 todos)</td><td align="right">3,829,447</td><td align="right">3,077,907</td><td align="right">5,125,639</td><td align="right">19,448,900</td></tr>
|
||||
<tr><td><em>Average</em></td><td align="right">1.08</td><td align="right">1.65</td><td align="right">6.25</td><td align="right">9.36</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
|
||||
### Client-Side Rendering
|
||||
|
||||
The gold standard for testing raw rendering performance for front-end web frameworks is the [js-framework-benchmark](https://github.com/krausest/js-framework-benchmark). The official results list Leptos as the fastest Rust/Wasm framework, slightly slower than SolidJS and significantly faster than popular JS frameworks like Svelte, Preact, and React.
|
||||
|
||||
<details>
|
||||
<summary>Click to show results</summary>
|
||||
<img width="913" alt="js-framework-benchmark results" src="https://user-images.githubusercontent.com/286622/198388168-d21e938b-5d59-4000-b373-91b48f1ec4d3.png">
|
||||
</details>
|
||||
|
||||
## FAQs
|
||||
|
||||
### What’s up with the name?
|
||||
|
||||
_Leptos_ (λεπτός) is an ancient Greek word meaning “thin, light, refined, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
|
||||
|
||||
### Is it production ready?
|
||||
|
||||
People usually mean one of three things by this question.
|
||||
|
||||
1. **Are the APIs stable?** i.e., will I have to rewrite my whole app from Leptos 0.1 to 0.2 to 0.3 to 0.4, or can I write it now and benefit from new features and updates as new versions come?
|
||||
|
||||
The APIs are basically settled. We’re adding new features, but we’re very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to future releases, in terms of architecture.
|
||||
|
||||
2. **Are there bugs?**
|
||||
|
||||
Yes, I’m sure there are. You can see from the state of our issue tracker over time that there aren’t that _many_ bugs and they’re usually resolved pretty quickly. But for sure, there may be moments where you encounter something that requires a fix at the framework level, which may not be immediately resolved.
|
||||
|
||||
3. **Am I a consumer or a contributor?**
|
||||
|
||||
This may be the big one: “production ready” implies a certain orientation to a library: that you can basically use it, without any special knowledge of its internals or ability to contribute. Everyone has this at some level in their stack: for example I (@gbj) don’t have the capacity or knowledge to contribute to something like `wasm-bindgen` at this point: I simply rely on it to work.
|
||||
|
||||
There are several people in the community using Leptos right now for many websites at work, who have also become significant contributors. There may be missing features that you need, and you may end up building them! But, if you're willing to contribute a few missing pieces along the way, the framework is most definitely usable for production applications, especially given the ecosystem of libraries that have sprung up around it.
|
||||
|
||||
### Can I use this for native GUI?
|
||||
|
||||
Sure! Obviously the `view` macro is for generating DOM nodes but you can use the reactive system to drive any native GUI toolkit that uses the same kind of object-oriented, event-callback-based framework as the DOM pretty easily. The principles are the same:
|
||||
Sure! Obviously the `view` macro is for generating DOM nodes but you can use the reactive system to drive native any GUI toolkit that uses the same kind of object-oriented, event-callback-based framework as the DOM pretty easily. The principles are the same:
|
||||
|
||||
- Use signals, derived signals, and memos to create your reactive system
|
||||
- Create GUI widgets
|
||||
- Use event listeners to update signals
|
||||
- Create effects to update the UI
|
||||
|
||||
The 0.7 update originally set out to create a "generic rendering" approach that would allow us to reuse most of the same view logic to do all of the above. Unfortunately, this has had to be shelved for now due to difficulties encountered by the Rust compiler when building larger-scale applications with the number of generics spread throughout the codebase that this required. It's an approach I'm looking forward to exploring again in the future; feel free to reach out if you're interested in this kind of work.
|
||||
I've put together a [very simple GTK example](https://github.com/gbj/leptos/blob/main/examples/gtk/src/main.rs) so you can see what I mean.
|
||||
|
||||
### How is this different from Yew?
|
||||
### How is this different from Yew/Dioxus?
|
||||
|
||||
Yew is the most-used library for Rust web UI development, but there are several differences between Yew and Leptos, in philosophy, approach, and performance.
|
||||
On the surface level, these libraries may seem similar. Yew is, of course, the most mature Rust library for web UI development and has a huge ecosystem. Dioxus is similar in many ways, being heavily inspired by React. Here are some conceptual differences between Leptos and these frameworks:
|
||||
|
||||
- **VDOM vs. fine-grained:** Yew is built on the virtual DOM (VDOM) model: state changes cause components to re-render, generating a new virtual DOM tree. Yew diffs this against the previous VDOM, and applies those patches to the actual DOM. Component functions rerun whenever state changes. Leptos takes an entirely different approach. Components run once, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
|
||||
- **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is.
|
||||
- **Server integration:** Yew was created in an era in which browser-rendered single-page apps (SPAs) were the dominant paradigm. While Leptos supports client-side rendering, it also focuses on integrating with the server side of your application via server functions and multiple modes of serving HTML, including out-of-order streaming.
|
||||
|
||||
### How is this different from Dioxus?
|
||||
|
||||
Like Leptos, Dioxus is a framework for building UIs using web technologies. However, there are significant differences in approach and features.
|
||||
|
||||
- **VDOM vs. fine-grained:** While Dioxus has a performant virtual DOM (VDOM), it still uses coarse-grained/component-scoped reactivity: changing a stateful value reruns the component function and diffs the old UI against the new one. Leptos components use a different mental model, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
|
||||
- **Web vs. desktop priorities:** Dioxus uses Leptos server functions in its fullstack mode, but does not have the same `<Suspense>`-based support for things like streaming HTML rendering, or share the same focus on holistic web performance. Leptos tends to prioritize holistic web performance (streaming HTML rendering, smaller WASM binary sizes, etc.), whereas Dioxus has an unparalleled experience when building desktop apps, because your application logic runs as a native Rust binary.
|
||||
- **Performance:** This has huge performance implications: Leptos is simply _much_ faster at both creating and updating the UI than Yew is.
|
||||
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising components re-renders because there are no re-renders. Your app can be divided into components based on what makes sense for your app, because they have no performance implications.
|
||||
|
||||
### How is this different from Sycamore?
|
||||
|
||||
Sycamore and Leptos are both heavily influenced by SolidJS. At this point, Leptos has a larger community and ecosystem and is more actively developed. Other differences:
|
||||
Conceptually, these two frameworks are very similar: because both are built on fine-grained reactivity, most apps will end up looking very similar between the two, and Sycamore or Leptos apps will both look a lot like SolidJS apps, in the same way that Yew or Dioxus can look a lot like React.
|
||||
|
||||
- **Templating DSLs:** Sycamore uses a custom templating language for its views, while Leptos uses a JSX-like template format.
|
||||
- **`'static` signals:** One of Leptos’s main innovations was the creation of `Copy + 'static` signals, which have excellent ergonomics. Sycamore is in the process of adopting the same pattern, but this is not yet released.
|
||||
- **Perseus vs. server functions:** The Perseus metaframework provides an opinionated way to build Sycamore apps that include server functionality. Leptos instead provides primitives like server functions in the core of the framework.
|
||||
There are some practical differences that make a significant difference:
|
||||
|
||||
- **Maturity:** Sycamore is obviously a much more mature and stable library with a larger ecosystem.
|
||||
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
|
||||
- **Template node cloning:** Leptos's `view` macro compiles to a static HTML string and a set of instructions of how to assign its reactive values. This means that at runtime, Leptos can clone a `<template>` node rather than calling `document.createElement()` to create DOM nodes. This is a _significantly_ faster way of rendering components.
|
||||
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use `create_rw_signal` to give a unified read/write signal.)_
|
||||
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0); // a signal
|
||||
let double_count = move || count() * 2; // a derived signal
|
||||
let memoized_count = create_memo(cx, move |_| count() * 3); // a memo
|
||||
// all are accessed by calling them
|
||||
assert_eq!(count(), 0);
|
||||
assert_eq!(double_count(), 0);
|
||||
assert_eq!(memoized_count(), 0);
|
||||
|
||||
// this function can accept any of those signals
|
||||
fn do_work_on_signal(my_signal: impl Fn() -> i32) { ... }
|
||||
```
|
||||
|
||||
- **Signals and scopes are `'static`:** Both Leptos and Sycamore ease the pain of moving signals in closures (in particular, event listeners) by making them `Copy`, to avoid the `{ let count = count.clone(); move |_| ... }` that's very familiar in Rust UI code. Sycamore does this by using bump allocation to tie the lifetimes of its signals to its scopes: since references are `Copy`, `&'a Signal<T>` can be moved into a closure. Leptos does this by using arena allocation and passing around indices: types like `ReadSignal<T>`, `WriteSignal<T>`, and `Memo<T>` are actually wrapper for indices into an arena. This means that both scopes and signals are both `Copy` and `'static` in Leptos, which means that they can be moved easily into closures without adding lifetime complexity.
|
||||
|
||||
13
SECURITY.md
13
SECURITY.md
@@ -1,13 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a suspected security issue, please contact security@leptos.dev rather than opening
|
||||
a public issue.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
The most-recently-released version of the library is supported with security updates.
|
||||
For example, if a security issue is discovered that affects 0.3.2 and all later releases,
|
||||
a 0.4.x patch will be released but a new 0.3.x patch release will not be made. You should
|
||||
plan to update to the latest version to receive any new features or bugfixes of any kind.
|
||||
40
TODO.md
40
TODO.md
@@ -1,40 +0,0 @@
|
||||
- core examples
|
||||
- [x] counter
|
||||
- [x] counters
|
||||
- [x] fetch
|
||||
- [x] todomvc
|
||||
- [x] error_boundary
|
||||
- [x] parent\_child
|
||||
- [x] on: on components
|
||||
- [ ] router
|
||||
- [ ] slots
|
||||
- [ ] hackernews
|
||||
- [ ] counter\_isomorphic
|
||||
- [ ] todo\_app\_sqlite
|
||||
- other ssr examples
|
||||
- [ ] error boundary SSR
|
||||
- reactivity
|
||||
- Signal wrappers
|
||||
- SignalDispose implementations on all Copy types
|
||||
- untracked access warnings
|
||||
- ErrorBoundary
|
||||
- [ ] RenderHtml implementation
|
||||
- [ ] Separate component?
|
||||
- Suspense/Transition components?
|
||||
- callbacks
|
||||
- unsync StoredValue
|
||||
- SSR
|
||||
- escaping HTML correctly (attributes + text nodes)
|
||||
- router
|
||||
- nested routes
|
||||
- trailing slashes
|
||||
- \_meta package (and use in hackernews)
|
||||
- integrations
|
||||
- update tests
|
||||
- hackernews example
|
||||
- TODOs
|
||||
- Suspense/Transition/Await components
|
||||
- nicer routing components
|
||||
- async routing (waiting for data to load before navigation)
|
||||
- `<A>` component
|
||||
- figure out rebuilding issues: list (needs new signal IDs) vs. regular rebuild
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "throw_error"
|
||||
version = "0.3.1"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Utilities for wrapping, throwing, and catching errors."
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pin-project-lite = { workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow.workspace = true
|
||||
@@ -1 +0,0 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
@@ -1,2 +0,0 @@
|
||||
A utility library for wrapping arbitrary errors, and for “throwing” errors in a way
|
||||
that can be caught by user-defined error hooks.
|
||||
@@ -1,189 +0,0 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
//! A utility library for wrapping arbitrary errors, and for “throwing” errors in a way
|
||||
//! that can be caught by user-defined error hooks.
|
||||
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
error,
|
||||
fmt::{self, Display},
|
||||
future::Future,
|
||||
ops,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
/* Wrapper Types */
|
||||
|
||||
/// A generic wrapper for any error.
|
||||
#[derive(Debug, Clone)]
|
||||
#[repr(transparent)]
|
||||
pub struct Error(Arc<dyn error::Error + Send + Sync>);
|
||||
|
||||
impl Error {
|
||||
/// Converts the wrapper into the inner reference-counted error.
|
||||
pub fn into_inner(self) -> Arc<dyn error::Error + Send + Sync> {
|
||||
Arc::clone(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for Error {
|
||||
type Target = Arc<dyn error::Error + Send + Sync>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Error
|
||||
where
|
||||
T: Into<Box<dyn error::Error + Send + Sync + 'static>>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Error(Arc::from(value.into()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements behavior that allows for global or scoped error handling.
|
||||
///
|
||||
/// This allows for both "throwing" errors to register them, and "clearing" errors when they are no
|
||||
/// longer valid. This is useful for something like a user interface, in which an error can be
|
||||
/// "thrown" on some invalid user input, and later "cleared" if the user corrects the input.
|
||||
/// Keeping a unique identifier for each error allows the UI to be updated accordingly.
|
||||
pub trait ErrorHook: Send + Sync {
|
||||
/// Handles the given error, returning a unique identifier.
|
||||
fn throw(&self, error: Error) -> ErrorId;
|
||||
|
||||
/// Clears the error associated with the given identifier.
|
||||
fn clear(&self, id: &ErrorId);
|
||||
}
|
||||
|
||||
/// A unique identifier for an error. This is returned when you call [`throw`], which calls a
|
||||
/// global error handler.
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Default)]
|
||||
pub struct ErrorId(usize);
|
||||
|
||||
impl Display for ErrorId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for ErrorId {
|
||||
fn from(value: usize) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static ERROR_HOOK: RefCell<Option<Arc<dyn ErrorHook>>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
/// Resets the error hook to its previous state when dropped.
|
||||
pub struct ResetErrorHookOnDrop(Option<Arc<dyn ErrorHook>>);
|
||||
|
||||
impl Drop for ResetErrorHookOnDrop {
|
||||
fn drop(&mut self) {
|
||||
ERROR_HOOK.with_borrow_mut(|this| *this = self.0.take())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current error hook.
|
||||
pub fn get_error_hook() -> Option<Arc<dyn ErrorHook>> {
|
||||
ERROR_HOOK.with_borrow(Clone::clone)
|
||||
}
|
||||
|
||||
/// Sets the current thread-local error hook, which will be invoked when [`throw`] is called.
|
||||
pub fn set_error_hook(hook: Arc<dyn ErrorHook>) -> ResetErrorHookOnDrop {
|
||||
ResetErrorHookOnDrop(
|
||||
ERROR_HOOK.with_borrow_mut(|this| Option::replace(this, hook)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Invokes the error hook set by [`set_error_hook`] with the given error.
|
||||
pub fn throw(error: impl Into<Error>) -> ErrorId {
|
||||
ERROR_HOOK
|
||||
.with_borrow(|hook| hook.as_ref().map(|hook| hook.throw(error.into())))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Clears the given error from the current error hook.
|
||||
pub fn clear(id: &ErrorId) {
|
||||
ERROR_HOOK
|
||||
.with_borrow(|hook| hook.as_ref().map(|hook| hook.clear(id)))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pin_project_lite::pin_project! {
|
||||
/// A [`Future`] that reads the error hook that is set when it is created, and sets this as the
|
||||
/// current error hook whenever it is polled.
|
||||
pub struct ErrorHookFuture<Fut> {
|
||||
hook: Option<Arc<dyn ErrorHook>>,
|
||||
#[pin]
|
||||
inner: Fut
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut> ErrorHookFuture<Fut> {
|
||||
/// Reads the current hook and wraps the given [`Future`], returning a new `Future` that will
|
||||
/// set the error hook whenever it is polled.
|
||||
pub fn new(inner: Fut) -> Self {
|
||||
Self {
|
||||
hook: ERROR_HOOK.with_borrow(Clone::clone),
|
||||
inner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut> Future for ErrorHookFuture<Fut>
|
||||
where
|
||||
Fut: Future,
|
||||
{
|
||||
type Output = Fut::Output;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
let _hook = this
|
||||
.hook
|
||||
.as_ref()
|
||||
.map(|hook| set_error_hook(Arc::clone(hook)));
|
||||
this.inner.poll(cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::error::Error as StdError;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MyError;
|
||||
|
||||
impl Display for MyError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "MyError")
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for MyError {}
|
||||
|
||||
#[test]
|
||||
fn test_from() {
|
||||
let e = MyError;
|
||||
let _le = Error::from(e);
|
||||
|
||||
let e = "some error".to_string();
|
||||
let _le = Error::from(e);
|
||||
|
||||
let e = anyhow::anyhow!("anyhow error");
|
||||
let _le = Error::from(e);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
[package]
|
||||
name = "any_spawner"
|
||||
version = "0.3.0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Spawn asynchronous tasks in an executor-independent way."
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-executor = { optional = true , workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
glib = { optional = true , workspace = true, default-features = true }
|
||||
thiserror = { workspace = true , default-features = true }
|
||||
tokio = { optional = true, default-features = false, features = [
|
||||
"rt",
|
||||
] , workspace = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
wasm-bindgen-futures = { optional = true , workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
futures-lite = { default-features = false , workspace = true }
|
||||
tokio = { default-features = false, features = [
|
||||
"rt",
|
||||
"macros",
|
||||
"time",
|
||||
] , workspace = true }
|
||||
wasm-bindgen-test = { workspace = true, default-features = true }
|
||||
serial_test = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
async-executor = ["dep:async-executor"]
|
||||
tracing = ["dep:tracing"]
|
||||
tokio = ["dep:tokio"]
|
||||
glib = ["dep:glib"]
|
||||
wasm-bindgen = ["dep:wasm-bindgen-futures"]
|
||||
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"]
|
||||
max_combination_size = 2
|
||||
@@ -1,4 +0,0 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
]
|
||||
@@ -1,26 +0,0 @@
|
||||
This crate makes it easier to write asynchronous code that is executor-agnostic, by providing a
|
||||
utility that can be used to spawn tasks in a variety of executors.
|
||||
|
||||
It only supports single executor per program, but that executor can be set at runtime, anywhere
|
||||
in your crate (or an application that depends on it).
|
||||
|
||||
This can be extended to support any executor or runtime that supports spawning [`Future`]s.
|
||||
|
||||
This is a least common denominator implementation in many ways. Limitations include:
|
||||
|
||||
- setting an executor is a one-time, global action
|
||||
- no "join handle" or other result is returned from the spawn
|
||||
- the `Future` must output `()`
|
||||
|
||||
```rust
|
||||
use any_spawner::Executor;
|
||||
|
||||
Executor::init_futures_executor()
|
||||
.expect("executor should only be initialized once");
|
||||
|
||||
// spawn a thread-safe Future
|
||||
Executor::spawn(async { /* ... */ });
|
||||
|
||||
// spawn a Future that is !Send
|
||||
Executor::spawn_local(async { /* ... */ });
|
||||
```
|
||||
@@ -1,514 +0,0 @@
|
||||
//! This crate makes it easier to write asynchronous code that is executor-agnostic, by providing a
|
||||
//! utility that can be used to spawn tasks in a variety of executors.
|
||||
//!
|
||||
//! It only supports single executor per program, but that executor can be set at runtime, anywhere
|
||||
//! in your crate (or an application that depends on it).
|
||||
//!
|
||||
//! This can be extended to support any executor or runtime that supports spawning [`Future`]s.
|
||||
//!
|
||||
//! This is a least common denominator implementation in many ways. Limitations include:
|
||||
//! - setting an executor is a one-time, global action
|
||||
//! - no "join handle" or other result is returned from the spawn
|
||||
//! - the `Future` must output `()`
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use any_spawner::Executor;
|
||||
//!
|
||||
//! // make sure an Executor has been initialized with one of the init_ functions
|
||||
//!
|
||||
//! // spawn a thread-safe Future
|
||||
//! Executor::spawn(async { /* ... */ });
|
||||
//!
|
||||
//! // spawn a Future that is !Send
|
||||
//! Executor::spawn_local(async { /* ... */ });
|
||||
//! ```
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
use std::{future::Future, pin::Pin, sync::OnceLock};
|
||||
use thiserror::Error;
|
||||
|
||||
/// A future that has been pinned.
|
||||
pub type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
|
||||
/// A future that has been pinned.
|
||||
pub type PinnedLocalFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
||||
|
||||
// Type alias for the spawn function pointer.
|
||||
type SpawnFn = fn(PinnedFuture<()>);
|
||||
// Type alias for the spawn_local function pointer.
|
||||
type SpawnLocalFn = fn(PinnedLocalFuture<()>);
|
||||
// Type alias for the poll_local function pointer.
|
||||
type PollLocalFn = fn();
|
||||
|
||||
/// Holds the function pointers for the current global executor.
|
||||
#[derive(Clone, Copy)]
|
||||
struct ExecutorFns {
|
||||
spawn: SpawnFn,
|
||||
spawn_local: SpawnLocalFn,
|
||||
poll_local: PollLocalFn,
|
||||
}
|
||||
|
||||
// Use a single OnceLock to ensure atomic initialization of all functions.
|
||||
static EXECUTOR_FNS: OnceLock<ExecutorFns> = OnceLock::new();
|
||||
|
||||
// No-op functions to use when an executor doesn't support a specific operation.
|
||||
#[cfg(any(feature = "tokio", feature = "wasm-bindgen", feature = "glib"))]
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn no_op_poll() {}
|
||||
|
||||
#[cfg(all(not(feature = "wasm-bindgen"), not(debug_assertions)))]
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn no_op_spawn(_: PinnedFuture<()>) {
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!(
|
||||
"Warning: Executor::spawn called, but no global 'spawn' function is \
|
||||
configured (perhaps only spawn_local is supported, e.g., on wasm \
|
||||
without threading?)."
|
||||
);
|
||||
}
|
||||
|
||||
// Wasm panics if you spawn without an executor
|
||||
#[cfg(feature = "wasm-bindgen")]
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn no_op_spawn(_: PinnedFuture<()>) {
|
||||
panic!(
|
||||
"Executor::spawn called, but no global 'spawn' function is configured."
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn no_op_spawn_local(_: PinnedLocalFuture<()>) {
|
||||
panic!(
|
||||
"Executor::spawn_local called, but no global 'spawn_local' function \
|
||||
is configured."
|
||||
);
|
||||
}
|
||||
|
||||
/// Errors that can occur when using the executor.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ExecutorError {
|
||||
/// The executor has already been set.
|
||||
#[error("Global executor has already been set.")]
|
||||
AlreadySet,
|
||||
}
|
||||
|
||||
/// A global async executor that can spawn tasks.
|
||||
pub struct Executor;
|
||||
|
||||
impl Executor {
|
||||
/// Spawns a thread-safe [`Future`].
|
||||
///
|
||||
/// Uses the globally configured executor.
|
||||
/// Panics if no global executor has been initialized.
|
||||
#[inline(always)]
|
||||
#[track_caller]
|
||||
pub fn spawn(fut: impl Future<Output = ()> + Send + 'static) {
|
||||
let pinned_fut = Box::pin(fut);
|
||||
|
||||
if let Some(fns) = EXECUTOR_FNS.get() {
|
||||
(fns.spawn)(pinned_fut)
|
||||
} else {
|
||||
// No global executor set.
|
||||
handle_uninitialized_spawn(pinned_fut);
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns a [`Future`] that cannot be sent across threads.
|
||||
///
|
||||
/// Uses the globally configured executor.
|
||||
/// Panics if no global executor has been initialized.
|
||||
#[inline(always)]
|
||||
#[track_caller]
|
||||
pub fn spawn_local(fut: impl Future<Output = ()> + 'static) {
|
||||
let pinned_fut = Box::pin(fut);
|
||||
|
||||
if let Some(fns) = EXECUTOR_FNS.get() {
|
||||
(fns.spawn_local)(pinned_fut)
|
||||
} else {
|
||||
// No global executor set.
|
||||
handle_uninitialized_spawn_local(pinned_fut);
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits until the next "tick" of the current async executor.
|
||||
/// Respects the global executor.
|
||||
#[inline(always)]
|
||||
pub async fn tick() {
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
#[cfg(not(all(feature = "wasm-bindgen", target_family = "wasm")))]
|
||||
Executor::spawn(async move {
|
||||
_ = tx.send(());
|
||||
});
|
||||
#[cfg(all(feature = "wasm-bindgen", target_family = "wasm"))]
|
||||
Executor::spawn_local(async move {
|
||||
_ = tx.send(());
|
||||
});
|
||||
|
||||
_ = rx.await;
|
||||
}
|
||||
|
||||
/// Polls the global async executor.
|
||||
///
|
||||
/// Uses the globally configured executor.
|
||||
/// Does nothing if the global executor does not support polling.
|
||||
#[inline(always)]
|
||||
pub fn poll_local() {
|
||||
if let Some(fns) = EXECUTOR_FNS.get() {
|
||||
(fns.poll_local)()
|
||||
}
|
||||
// If not initialized or doesn't support polling, do nothing gracefully.
|
||||
}
|
||||
}
|
||||
|
||||
impl Executor {
|
||||
/// Globally sets the [`tokio`] runtime as the executor used to spawn tasks.
|
||||
///
|
||||
/// Returns `Err(_)` if a global executor has already been set.
|
||||
///
|
||||
/// Requires the `tokio` feature to be activated on this crate.
|
||||
#[cfg(feature = "tokio")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
|
||||
pub fn init_tokio() -> Result<(), ExecutorError> {
|
||||
let executor_impl = ExecutorFns {
|
||||
spawn: |fut| {
|
||||
tokio::spawn(fut);
|
||||
},
|
||||
spawn_local: |fut| {
|
||||
tokio::task::spawn_local(fut);
|
||||
},
|
||||
// Tokio doesn't have an explicit global poll function like LocalPool::run_until_stalled
|
||||
poll_local: no_op_poll,
|
||||
};
|
||||
EXECUTOR_FNS
|
||||
.set(executor_impl)
|
||||
.map_err(|_| ExecutorError::AlreadySet)
|
||||
}
|
||||
|
||||
/// Globally sets the [`wasm-bindgen-futures`] runtime as the executor used to spawn tasks.
|
||||
///
|
||||
/// Returns `Err(_)` if a global executor has already been set.
|
||||
///
|
||||
/// Requires the `wasm-bindgen` feature to be activated on this crate.
|
||||
#[cfg(feature = "wasm-bindgen")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "wasm-bindgen")))]
|
||||
pub fn init_wasm_bindgen() -> Result<(), ExecutorError> {
|
||||
let executor_impl = ExecutorFns {
|
||||
// wasm-bindgen-futures only supports spawn_local
|
||||
spawn: no_op_spawn,
|
||||
spawn_local: |fut| {
|
||||
wasm_bindgen_futures::spawn_local(fut);
|
||||
},
|
||||
poll_local: no_op_poll,
|
||||
};
|
||||
EXECUTOR_FNS
|
||||
.set(executor_impl)
|
||||
.map_err(|_| ExecutorError::AlreadySet)
|
||||
}
|
||||
|
||||
/// Globally sets the [`glib`] runtime as the executor used to spawn tasks.
|
||||
///
|
||||
/// Returns `Err(_)` if a global executor has already been set.
|
||||
///
|
||||
/// Requires the `glib` feature to be activated on this crate.
|
||||
#[cfg(feature = "glib")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "glib")))]
|
||||
pub fn init_glib() -> Result<(), ExecutorError> {
|
||||
let executor_impl = ExecutorFns {
|
||||
spawn: |fut| {
|
||||
let main_context = glib::MainContext::default();
|
||||
main_context.spawn(fut);
|
||||
},
|
||||
spawn_local: |fut| {
|
||||
let main_context = glib::MainContext::default();
|
||||
main_context.spawn_local(fut);
|
||||
},
|
||||
// Glib needs event loop integration, explicit polling isn't the standard model here.
|
||||
poll_local: no_op_poll,
|
||||
};
|
||||
EXECUTOR_FNS
|
||||
.set(executor_impl)
|
||||
.map_err(|_| ExecutorError::AlreadySet)
|
||||
}
|
||||
|
||||
/// Globally sets the [`futures`] executor as the executor used to spawn tasks,
|
||||
/// lazily creating a thread pool to spawn tasks into.
|
||||
///
|
||||
/// Returns `Err(_)` if a global executor has already been set.
|
||||
///
|
||||
/// Requires the `futures-executor` feature to be activated on this crate.
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "futures-executor")))]
|
||||
pub fn init_futures_executor() -> Result<(), ExecutorError> {
|
||||
use futures::{
|
||||
executor::{LocalPool, LocalSpawner, ThreadPool},
|
||||
task::{LocalSpawnExt, SpawnExt},
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
|
||||
// Keep the lazy-init ThreadPool and thread-local LocalPool for spawn_local impl
|
||||
static THREAD_POOL: OnceLock<ThreadPool> = OnceLock::new();
|
||||
thread_local! {
|
||||
static LOCAL_POOL: RefCell<LocalPool> = RefCell::new(LocalPool::new());
|
||||
// SPAWNER is derived from LOCAL_POOL, keep it for efficiency inside the closure
|
||||
static SPAWNER: LocalSpawner = LOCAL_POOL.with(|pool| pool.borrow().spawner());
|
||||
}
|
||||
|
||||
fn get_thread_pool() -> &'static ThreadPool {
|
||||
THREAD_POOL.get_or_init(|| {
|
||||
ThreadPool::new()
|
||||
.expect("could not create futures executor ThreadPool")
|
||||
})
|
||||
}
|
||||
|
||||
let executor_impl = ExecutorFns {
|
||||
spawn: |fut| {
|
||||
get_thread_pool()
|
||||
.spawn(fut)
|
||||
.expect("failed to spawn future on ThreadPool");
|
||||
},
|
||||
spawn_local: |fut| {
|
||||
// Use the thread_local SPAWNER derived from LOCAL_POOL
|
||||
SPAWNER.with(|spawner| {
|
||||
spawner
|
||||
.spawn_local(fut)
|
||||
.expect("failed to spawn local future");
|
||||
});
|
||||
},
|
||||
poll_local: || {
|
||||
// Use the thread_local LOCAL_POOL
|
||||
LOCAL_POOL.with(|pool| {
|
||||
// Use try_borrow_mut to prevent panic during re-entrant calls
|
||||
if let Ok(mut pool) = pool.try_borrow_mut() {
|
||||
pool.run_until_stalled();
|
||||
}
|
||||
// If already borrowed, we're likely in a nested poll, so do nothing.
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
EXECUTOR_FNS
|
||||
.set(executor_impl)
|
||||
.map_err(|_| ExecutorError::AlreadySet)
|
||||
}
|
||||
|
||||
/// Globally sets the [`async_executor`] executor as the executor used to spawn tasks,
|
||||
/// lazily creating a thread pool to spawn tasks into.
|
||||
///
|
||||
/// Returns `Err(_)` if a global executor has already been set.
|
||||
///
|
||||
/// Requires the `async-executor` feature to be activated on this crate.
|
||||
#[cfg(feature = "async-executor")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "async-executor")))]
|
||||
pub fn init_async_executor() -> Result<(), ExecutorError> {
|
||||
use async_executor::{Executor as AsyncExecutor, LocalExecutor};
|
||||
|
||||
// Keep the lazy-init global Executor and thread-local LocalExecutor for spawn_local impl
|
||||
static ASYNC_EXECUTOR: OnceLock<AsyncExecutor<'static>> =
|
||||
OnceLock::new();
|
||||
thread_local! {
|
||||
static LOCAL_EXECUTOR_POOL: LocalExecutor<'static> = const { LocalExecutor::new() };
|
||||
}
|
||||
|
||||
fn get_async_executor() -> &'static AsyncExecutor<'static> {
|
||||
ASYNC_EXECUTOR.get_or_init(AsyncExecutor::new)
|
||||
}
|
||||
|
||||
let executor_impl = ExecutorFns {
|
||||
spawn: |fut| {
|
||||
get_async_executor().spawn(fut).detach();
|
||||
},
|
||||
spawn_local: |fut| {
|
||||
LOCAL_EXECUTOR_POOL.with(|pool| pool.spawn(fut).detach());
|
||||
},
|
||||
poll_local: || {
|
||||
LOCAL_EXECUTOR_POOL.with(|pool| {
|
||||
// try_tick polls the local executor without blocking
|
||||
// This prevents issues if called recursively or from within a task.
|
||||
pool.try_tick();
|
||||
});
|
||||
},
|
||||
};
|
||||
EXECUTOR_FNS
|
||||
.set(executor_impl)
|
||||
.map_err(|_| ExecutorError::AlreadySet)
|
||||
}
|
||||
|
||||
/// Globally sets a custom executor as the executor used to spawn tasks.
|
||||
///
|
||||
/// Requires the custom executor to be `Send + Sync` as it will be stored statically.
|
||||
///
|
||||
/// Returns `Err(_)` if a global executor has already been set.
|
||||
pub fn init_custom_executor(
|
||||
custom_executor: impl CustomExecutor + Send + Sync + 'static,
|
||||
) -> Result<(), ExecutorError> {
|
||||
// Store the custom executor instance itself to call its methods.
|
||||
// Use Box for dynamic dispatch.
|
||||
static CUSTOM_EXECUTOR_INSTANCE: OnceLock<
|
||||
Box<dyn CustomExecutor + Send + Sync>,
|
||||
> = OnceLock::new();
|
||||
|
||||
CUSTOM_EXECUTOR_INSTANCE
|
||||
.set(Box::new(custom_executor))
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
|
||||
// Now set the ExecutorFns using the stored instance
|
||||
let executor_impl = ExecutorFns {
|
||||
spawn: |fut| {
|
||||
// Unwrap is safe because we just set it successfully or returned Err.
|
||||
CUSTOM_EXECUTOR_INSTANCE.get().unwrap().spawn(fut);
|
||||
},
|
||||
spawn_local: |fut| {
|
||||
CUSTOM_EXECUTOR_INSTANCE.get().unwrap().spawn_local(fut);
|
||||
},
|
||||
poll_local: || {
|
||||
CUSTOM_EXECUTOR_INSTANCE.get().unwrap().poll_local();
|
||||
},
|
||||
};
|
||||
|
||||
EXECUTOR_FNS
|
||||
.set(executor_impl)
|
||||
.map_err(|_| ExecutorError::AlreadySet)
|
||||
// If setting EXECUTOR_FNS fails (extremely unlikely race if called *concurrently*
|
||||
// with another init_* after CUSTOM_EXECUTOR_INSTANCE was set), we technically
|
||||
// leave CUSTOM_EXECUTOR_INSTANCE set but EXECUTOR_FNS not. This is an edge case,
|
||||
// but the primary race condition is solved.
|
||||
}
|
||||
|
||||
/// Sets a custom executor *for the current thread only*.
|
||||
///
|
||||
/// This overrides the global executor for calls to `spawn`, `spawn_local`, and `poll_local`
|
||||
/// made *from the current thread*. It does not affect other threads or the global state.
|
||||
///
|
||||
/// The provided `custom_executor` must implement [`CustomExecutor`] and `'static`, but does
|
||||
/// **not** need to be `Send` or `Sync`.
|
||||
///
|
||||
/// Returns `Err(ExecutorError::AlreadySet)` if a *local* executor has already been set
|
||||
/// *for this thread*.
|
||||
pub fn init_local_custom_executor(
|
||||
custom_executor: impl CustomExecutor + 'static,
|
||||
) -> Result<(), ExecutorError> {
|
||||
// Store the custom executor instance itself to call its methods.
|
||||
// Use Box for dynamic dispatch.
|
||||
thread_local! {
|
||||
static CUSTOM_EXECUTOR_INSTANCE: OnceLock<
|
||||
Box<dyn CustomExecutor>,
|
||||
> = OnceLock::new();
|
||||
};
|
||||
|
||||
CUSTOM_EXECUTOR_INSTANCE.with(|this| {
|
||||
this.set(Box::new(custom_executor))
|
||||
.map_err(|_| ExecutorError::AlreadySet)
|
||||
})?;
|
||||
|
||||
// Now set the ExecutorFns using the stored instance
|
||||
let executor_impl = ExecutorFns {
|
||||
spawn: |fut| {
|
||||
// Unwrap is safe because we just set it successfully or returned Err.
|
||||
CUSTOM_EXECUTOR_INSTANCE
|
||||
.with(|this| this.get().unwrap().spawn(fut));
|
||||
},
|
||||
spawn_local: |fut| {
|
||||
CUSTOM_EXECUTOR_INSTANCE
|
||||
.with(|this| this.get().unwrap().spawn_local(fut));
|
||||
},
|
||||
poll_local: || {
|
||||
CUSTOM_EXECUTOR_INSTANCE
|
||||
.with(|this| this.get().unwrap().poll_local());
|
||||
},
|
||||
};
|
||||
|
||||
EXECUTOR_FNS
|
||||
.set(executor_impl)
|
||||
.map_err(|_| ExecutorError::AlreadySet)
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for custom executors.
|
||||
/// Custom executors can be used to integrate with any executor that supports spawning futures.
|
||||
///
|
||||
/// If used with `init_custom_executor`, the implementation must be `Send + Sync + 'static`.
|
||||
///
|
||||
/// All methods can be called recursively. Implementors should be mindful of potential
|
||||
/// deadlocks or excessive resource consumption if recursive calls are not handled carefully
|
||||
/// (e.g., using `try_borrow_mut` or non-blocking polls within implementations).
|
||||
pub trait CustomExecutor {
|
||||
/// Spawns a future, usually on a thread pool.
|
||||
fn spawn(&self, fut: PinnedFuture<()>);
|
||||
/// Spawns a local future. May require calling `poll_local` to make progress.
|
||||
fn spawn_local(&self, fut: PinnedLocalFuture<()>);
|
||||
/// Polls the executor, if it supports polling. Implementations should ideally be
|
||||
/// non-blocking or use mechanisms like `try_tick` or `try_borrow_mut` to handle
|
||||
/// re-entrant calls safely.
|
||||
fn poll_local(&self);
|
||||
}
|
||||
|
||||
// Ensure CustomExecutor is object-safe
|
||||
#[allow(dead_code)]
|
||||
fn test_object_safety(_: Box<dyn CustomExecutor + Send + Sync>) {} // Added Send + Sync constraint here for global usage
|
||||
|
||||
/// Handles the case where `Executor::spawn` is called without an initialized executor.
|
||||
#[cold] // Less likely path
|
||||
#[inline(never)]
|
||||
#[track_caller]
|
||||
fn handle_uninitialized_spawn(_fut: PinnedFuture<()>) {
|
||||
let caller = std::panic::Location::caller();
|
||||
#[cfg(all(debug_assertions, feature = "tracing"))]
|
||||
{
|
||||
tracing::error!(
|
||||
target: "any_spawner",
|
||||
spawn_caller=%caller,
|
||||
"Executor::spawn called before a global executor was initialized. Task dropped."
|
||||
);
|
||||
// Drop the future implicitly after logging
|
||||
drop(_fut);
|
||||
}
|
||||
#[cfg(all(debug_assertions, not(feature = "tracing")))]
|
||||
{
|
||||
panic!(
|
||||
"At {caller}, tried to spawn a Future with Executor::spawn() \
|
||||
before a global executor was initialized."
|
||||
);
|
||||
}
|
||||
// In release builds (without tracing), call the specific no-op function.
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
no_op_spawn(_fut);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the case where `Executor::spawn_local` is called without an initialized executor.
|
||||
#[cold] // Less likely path
|
||||
#[inline(never)]
|
||||
#[track_caller]
|
||||
fn handle_uninitialized_spawn_local(_fut: PinnedLocalFuture<()>) {
|
||||
let caller = std::panic::Location::caller();
|
||||
#[cfg(all(debug_assertions, feature = "tracing"))]
|
||||
{
|
||||
tracing::error!(
|
||||
target: "any_spawner",
|
||||
spawn_caller=%caller,
|
||||
"Executor::spawn_local called before a global executor was initialized. \
|
||||
Task likely dropped or panicked."
|
||||
);
|
||||
// Fall through to panic or no-op depending on build/target
|
||||
}
|
||||
#[cfg(all(debug_assertions, not(feature = "tracing")))]
|
||||
{
|
||||
panic!(
|
||||
"At {caller}, tried to spawn a Future with \
|
||||
Executor::spawn_local() before a global executor was initialized."
|
||||
);
|
||||
}
|
||||
// In release builds (without tracing), call the specific no-op function (which usually panics).
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
no_op_spawn_local(_fut);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
use any_spawner::{Executor, ExecutorError};
|
||||
|
||||
#[test]
|
||||
fn test_already_set_error() {
|
||||
struct SimpleExecutor;
|
||||
|
||||
impl any_spawner::CustomExecutor for SimpleExecutor {
|
||||
fn spawn(&self, _fut: any_spawner::PinnedFuture<()>) {}
|
||||
fn spawn_local(&self, _fut: any_spawner::PinnedLocalFuture<()>) {}
|
||||
fn poll_local(&self) {}
|
||||
}
|
||||
|
||||
// First initialization should succeed
|
||||
Executor::init_custom_executor(SimpleExecutor)
|
||||
.expect("First initialization failed");
|
||||
|
||||
// Second initialization should fail with AlreadySet error
|
||||
let result = Executor::init_custom_executor(SimpleExecutor);
|
||||
assert!(matches!(result, Err(ExecutorError::AlreadySet)));
|
||||
|
||||
// First local initialization should fail
|
||||
let result = Executor::init_local_custom_executor(SimpleExecutor);
|
||||
assert!(matches!(result, Err(ExecutorError::AlreadySet)));
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
#![cfg(feature = "async-executor")]
|
||||
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
// A simple async executor for testing
|
||||
struct TestExecutor {
|
||||
tasks: Mutex<Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>>,
|
||||
}
|
||||
|
||||
impl TestExecutor {
|
||||
fn new() -> Self {
|
||||
TestExecutor {
|
||||
tasks: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn<F>(&self, future: F)
|
||||
where
|
||||
F: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
self.tasks.lock().unwrap().push(Box::pin(future));
|
||||
}
|
||||
|
||||
fn run_all(&self) {
|
||||
// Take all tasks out to process them
|
||||
let tasks = self.tasks.lock().unwrap().drain(..).collect::<Vec<_>>();
|
||||
|
||||
// Use a basic future executor to run each task to completion
|
||||
for mut task in tasks {
|
||||
// Use futures-lite's block_on to complete the future
|
||||
futures::executor::block_on(async {
|
||||
unsafe {
|
||||
let task_mut = Pin::new_unchecked(&mut task);
|
||||
let _ = std::future::Future::poll(
|
||||
task_mut,
|
||||
&mut std::task::Context::from_waker(
|
||||
futures::task::noop_waker_ref(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_executor() {
|
||||
let executor = Arc::new(TestExecutor::new());
|
||||
let executor_clone = executor.clone();
|
||||
|
||||
// Create a spawner function that will use our test executor
|
||||
let spawner = move |future| {
|
||||
executor_clone.spawn(future);
|
||||
};
|
||||
|
||||
// Prepare test data
|
||||
let counter = Arc::new(Mutex::new(0));
|
||||
let counter_clone = counter.clone();
|
||||
|
||||
// Use the spawner to spawn a task
|
||||
spawner(async move {
|
||||
*counter_clone.lock().unwrap() += 1;
|
||||
});
|
||||
|
||||
// Run all tasks
|
||||
executor.run_all();
|
||||
|
||||
// Check if the task completed correctly
|
||||
assert_eq!(*counter.lock().unwrap(), 1);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
use any_spawner::Executor;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_custom_executor() {
|
||||
// Define a simple custom executor
|
||||
struct TestExecutor {
|
||||
spawn_called: Arc<AtomicBool>,
|
||||
spawn_local_called: Arc<AtomicBool>,
|
||||
poll_local_called: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl any_spawner::CustomExecutor for TestExecutor {
|
||||
fn spawn(&self, fut: any_spawner::PinnedFuture<()>) {
|
||||
self.spawn_called.store(true, Ordering::SeqCst);
|
||||
// Execute the future immediately (this works for simple test futures)
|
||||
futures::executor::block_on(fut);
|
||||
}
|
||||
|
||||
fn spawn_local(&self, fut: any_spawner::PinnedLocalFuture<()>) {
|
||||
self.spawn_local_called.store(true, Ordering::SeqCst);
|
||||
// Execute the future immediately
|
||||
futures::executor::block_on(fut);
|
||||
}
|
||||
|
||||
fn poll_local(&self) {
|
||||
self.poll_local_called.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
let spawn_called = Arc::new(AtomicBool::new(false));
|
||||
let spawn_local_called = Arc::new(AtomicBool::new(false));
|
||||
let poll_local_called = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let executor = TestExecutor {
|
||||
spawn_called: spawn_called.clone(),
|
||||
spawn_local_called: spawn_local_called.clone(),
|
||||
poll_local_called: poll_local_called.clone(),
|
||||
};
|
||||
|
||||
// Initialize with our custom executor
|
||||
Executor::init_custom_executor(executor)
|
||||
.expect("Failed to initialize custom executor");
|
||||
|
||||
// Test spawn
|
||||
Executor::spawn(async {
|
||||
// Simple task
|
||||
});
|
||||
assert!(spawn_called.load(Ordering::SeqCst));
|
||||
|
||||
// Test spawn_local
|
||||
Executor::spawn_local(async {
|
||||
// Simple local task
|
||||
});
|
||||
assert!(spawn_local_called.load(Ordering::SeqCst));
|
||||
|
||||
// Test poll_local
|
||||
Executor::poll_local();
|
||||
assert!(poll_local_called.load(Ordering::SeqCst));
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
#![cfg(feature = "futures-executor")]
|
||||
|
||||
use any_spawner::{CustomExecutor, Executor, PinnedFuture, PinnedLocalFuture};
|
||||
|
||||
#[test]
|
||||
fn can_create_custom_executor() {
|
||||
use futures::{
|
||||
executor::{LocalPool, LocalSpawner},
|
||||
task::LocalSpawnExt,
|
||||
};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
static LOCAL_POOL: RefCell<LocalPool> = RefCell::new(LocalPool::new());
|
||||
static SPAWNER: LocalSpawner = LOCAL_POOL.with(|pool| pool.borrow().spawner());
|
||||
}
|
||||
|
||||
struct CustomFutureExecutor;
|
||||
impl CustomExecutor for CustomFutureExecutor {
|
||||
fn spawn(&self, _fut: PinnedFuture<()>) {
|
||||
panic!("not supported in this test");
|
||||
}
|
||||
|
||||
fn spawn_local(&self, fut: PinnedLocalFuture<()>) {
|
||||
SPAWNER.with(|spawner| {
|
||||
spawner.spawn_local(fut).expect("failed to spawn future");
|
||||
});
|
||||
}
|
||||
|
||||
fn poll_local(&self) {
|
||||
LOCAL_POOL.with(|pool| {
|
||||
if let Ok(mut pool) = pool.try_borrow_mut() {
|
||||
pool.run_until_stalled();
|
||||
}
|
||||
// If we couldn't borrow_mut, we're in a nested call to poll, so we don't need to do anything.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Executor::init_custom_executor(CustomFutureExecutor)
|
||||
.expect("couldn't set executor");
|
||||
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
let counter_clone = Arc::clone(&counter);
|
||||
Executor::spawn_local(async move {
|
||||
counter_clone.store(1, Ordering::Release);
|
||||
});
|
||||
Executor::poll_local();
|
||||
assert_eq!(counter.load(Ordering::Acquire), 1);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
#![cfg(feature = "tokio")]
|
||||
|
||||
use any_spawner::Executor;
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_executor_tick() {
|
||||
// Initialize the tokio executor
|
||||
Executor::init_tokio().expect("Failed to initialize tokio executor");
|
||||
|
||||
let value = Arc::new(Mutex::new(false));
|
||||
let value_clone = value.clone();
|
||||
|
||||
// Spawn a task that sets the value after a tick
|
||||
Executor::spawn(async move {
|
||||
Executor::tick().await;
|
||||
*value_clone.lock().unwrap() = true;
|
||||
});
|
||||
|
||||
// Allow some time for the task to complete
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
|
||||
// Check that the value was set
|
||||
assert!(*value.lock().unwrap());
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
#![cfg(feature = "futures-executor")]
|
||||
|
||||
use any_spawner::Executor;
|
||||
use futures::channel::oneshot;
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_futures_executor() {
|
||||
// Initialize the futures executor
|
||||
Executor::init_futures_executor()
|
||||
.expect("Failed to initialize futures executor");
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let result = Arc::new(Mutex::new(None));
|
||||
let result_clone = result.clone();
|
||||
|
||||
// Spawn a task
|
||||
Executor::spawn(async move {
|
||||
tx.send(84).expect("Failed to send value");
|
||||
});
|
||||
|
||||
// Spawn a task that waits for the result
|
||||
Executor::spawn(async move {
|
||||
match rx.await {
|
||||
Ok(val) => *result_clone.lock().unwrap() = Some(val),
|
||||
Err(_) => panic!("Failed to receive value"),
|
||||
}
|
||||
});
|
||||
|
||||
// Poll a few times to ensure the task completes
|
||||
for _ in 0..10 {
|
||||
Executor::poll_local();
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
|
||||
if result.lock().unwrap().is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(*result.lock().unwrap(), Some(84));
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#![cfg(feature = "futures-executor")]
|
||||
|
||||
use any_spawner::Executor;
|
||||
// All tests in this file use the same executor.
|
||||
|
||||
#[test]
|
||||
fn can_spawn_local_future() {
|
||||
use std::rc::Rc;
|
||||
|
||||
let _ = Executor::init_futures_executor();
|
||||
let rc = Rc::new(());
|
||||
Executor::spawn_local(async {
|
||||
_ = rc;
|
||||
});
|
||||
Executor::spawn(async {});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_make_local_progress() {
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
let _ = Executor::init_futures_executor();
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
Executor::spawn_local({
|
||||
let counter = Arc::clone(&counter);
|
||||
async move {
|
||||
assert_eq!(counter.fetch_add(1, Ordering::AcqRel), 0);
|
||||
Executor::spawn_local(async {
|
||||
// Should not crash
|
||||
});
|
||||
}
|
||||
});
|
||||
Executor::poll_local();
|
||||
assert_eq!(counter.load(Ordering::Acquire), 1);
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
#![cfg(feature = "glib")]
|
||||
|
||||
use any_spawner::Executor;
|
||||
use glib::{MainContext, MainLoop};
|
||||
use serial_test::serial;
|
||||
use std::{
|
||||
cell::Cell,
|
||||
future::Future,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
// Helper to run a future to completion on a dedicated glib MainContext.
|
||||
// Returns true if the future completed within the timeout, false otherwise.
|
||||
fn run_on_glib_context<F>(fut: F)
|
||||
where
|
||||
F: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
let _ = Executor::init_glib();
|
||||
|
||||
let context = MainContext::default();
|
||||
let main_loop = MainLoop::new(Some(&context), false);
|
||||
let main_loop_clone = main_loop.clone();
|
||||
|
||||
Executor::spawn(async move {
|
||||
fut.await;
|
||||
main_loop_clone.quit();
|
||||
});
|
||||
|
||||
main_loop.run();
|
||||
}
|
||||
|
||||
// Helper to run a local (!Send) future on the glib context.
|
||||
fn run_local_on_glib_context<F>(fut: F)
|
||||
where
|
||||
F: Future<Output = ()> + 'static,
|
||||
{
|
||||
let _ = Executor::init_glib();
|
||||
|
||||
let context = MainContext::default();
|
||||
let main_loop = MainLoop::new(Some(&context), false);
|
||||
let main_loop_clone = main_loop.clone();
|
||||
|
||||
Executor::spawn_local(async move {
|
||||
fut.await;
|
||||
main_loop_clone.quit();
|
||||
});
|
||||
|
||||
main_loop.run();
|
||||
}
|
||||
|
||||
// This test must run after a test that successfully initializes glib,
|
||||
// or within its own process.
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_glib_spawn() {
|
||||
let success_flag = Arc::new(AtomicBool::new(false));
|
||||
let flag_clone = success_flag.clone();
|
||||
|
||||
run_on_glib_context(async move {
|
||||
// Simulate async work
|
||||
futures_lite::future::yield_now().await;
|
||||
flag_clone.store(true, Ordering::SeqCst);
|
||||
|
||||
// We need to give the spawned task time to run.
|
||||
// The run_on_glib_context handles the main loop.
|
||||
// We just need to ensure spawn happened correctly.
|
||||
// Let's wait a tiny bit within the driving future to ensure spawn gets processed.
|
||||
glib::timeout_future(Duration::from_millis(10)).await;
|
||||
});
|
||||
|
||||
assert!(
|
||||
success_flag.load(Ordering::SeqCst),
|
||||
"Spawned future did not complete successfully"
|
||||
);
|
||||
}
|
||||
|
||||
// Similar conditions as test_glib_spawn regarding initialization state.
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_glib_spawn_local() {
|
||||
let success_flag = Rc::new(Cell::new(false));
|
||||
let flag_clone = success_flag.clone();
|
||||
|
||||
run_local_on_glib_context(async move {
|
||||
// Use Rc to make the future !Send
|
||||
let non_send_data = Rc::new(Cell::new(10));
|
||||
|
||||
let data = non_send_data.get();
|
||||
assert_eq!(data, 10, "Rc data should be accessible");
|
||||
non_send_data.set(20); // Modify non-Send data
|
||||
|
||||
// Simulate async work
|
||||
futures_lite::future::yield_now().await;
|
||||
|
||||
assert_eq!(
|
||||
non_send_data.get(),
|
||||
20,
|
||||
"Rc data should persist modification"
|
||||
);
|
||||
flag_clone.set(true);
|
||||
|
||||
// Wait a tiny bit
|
||||
glib::timeout_future(Duration::from_millis(10)).await;
|
||||
});
|
||||
|
||||
assert!(
|
||||
success_flag.get(),
|
||||
"Spawned local future did not complete successfully"
|
||||
);
|
||||
}
|
||||
|
||||
// Test Executor::tick with glib backend
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_glib_tick() {
|
||||
run_on_glib_context(async {
|
||||
let value = Arc::new(Mutex::new(false));
|
||||
let value_clone = value.clone();
|
||||
|
||||
// Spawn a task that sets the value after a tick
|
||||
Executor::spawn(async move {
|
||||
Executor::tick().await;
|
||||
*value_clone.lock().unwrap() = true;
|
||||
});
|
||||
|
||||
// Allow some time for the task to complete
|
||||
glib::timeout_future(Duration::from_millis(10)).await;
|
||||
|
||||
// Check that the value was set
|
||||
assert!(*value.lock().unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
// Test Executor::poll_local with glib backend (should be a no-op)
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_glib_poll_local_is_no_op() {
|
||||
// Ensure glib executor is initialized
|
||||
let _ = Executor::init_glib();
|
||||
// poll_local for glib is configured as a no-op
|
||||
// Calling it should not panic or cause issues.
|
||||
Executor::poll_local();
|
||||
Executor::poll_local();
|
||||
|
||||
println!("Executor::poll_local called successfully (expected no-op).");
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
use any_spawner::Executor;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_local_custom_executor() {
|
||||
// Define a thread-local custom executor
|
||||
struct LocalTestExecutor {
|
||||
spawn_called: Arc<AtomicBool>,
|
||||
spawn_local_called: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl any_spawner::CustomExecutor for LocalTestExecutor {
|
||||
fn spawn(&self, fut: any_spawner::PinnedFuture<()>) {
|
||||
self.spawn_called.store(true, Ordering::SeqCst);
|
||||
futures::executor::block_on(fut);
|
||||
}
|
||||
|
||||
fn spawn_local(&self, fut: any_spawner::PinnedLocalFuture<()>) {
|
||||
self.spawn_local_called.store(true, Ordering::SeqCst);
|
||||
futures::executor::block_on(fut);
|
||||
}
|
||||
|
||||
fn poll_local(&self) {
|
||||
// No-op for this test
|
||||
}
|
||||
}
|
||||
|
||||
let local_spawn_called = Arc::new(AtomicBool::new(false));
|
||||
let local_spawn_local_called = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let local_executor = LocalTestExecutor {
|
||||
spawn_called: local_spawn_called.clone(),
|
||||
spawn_local_called: local_spawn_local_called.clone(),
|
||||
};
|
||||
|
||||
// Initialize a thread-local executor
|
||||
Executor::init_local_custom_executor(local_executor)
|
||||
.expect("Failed to initialize local custom executor");
|
||||
|
||||
// Test spawn - should use the thread-local executor
|
||||
Executor::spawn(async {
|
||||
// Simple task
|
||||
});
|
||||
assert!(local_spawn_called.load(Ordering::SeqCst));
|
||||
|
||||
// Test spawn_local - should use the thread-local executor
|
||||
Executor::spawn_local(async {
|
||||
// Simple local task
|
||||
});
|
||||
assert!(local_spawn_local_called.load(Ordering::SeqCst));
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
#![cfg(feature = "tokio")]
|
||||
|
||||
use any_spawner::Executor;
|
||||
use futures::channel::oneshot;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_tasks() {
|
||||
Executor::init_tokio().expect("Failed to initialize tokio executor");
|
||||
|
||||
let counter = Arc::new(Mutex::new(0));
|
||||
let tasks = 10;
|
||||
let mut handles = Vec::new();
|
||||
|
||||
// Spawn multiple tasks that increment the counter
|
||||
for _ in 0..tasks {
|
||||
let counter_clone = counter.clone();
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
Executor::spawn(async move {
|
||||
*counter_clone.lock().unwrap() += 1;
|
||||
tx.send(()).expect("Failed to send completion signal");
|
||||
});
|
||||
|
||||
handles.push(rx);
|
||||
}
|
||||
|
||||
// Wait for all tasks to complete
|
||||
for handle in handles {
|
||||
handle.await.expect("Task failed");
|
||||
}
|
||||
|
||||
// Verify that all tasks incremented the counter
|
||||
assert_eq!(*counter.lock().unwrap(), tasks);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
#![cfg(feature = "tokio")]
|
||||
|
||||
use any_spawner::Executor;
|
||||
use futures::channel::oneshot;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tokio_executor() {
|
||||
// Initialize the tokio executor
|
||||
Executor::init_tokio().expect("Failed to initialize tokio executor");
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
// Spawn a task that sends a value
|
||||
Executor::spawn(async move {
|
||||
tx.send(42).expect("Failed to send value");
|
||||
});
|
||||
|
||||
// Wait for the spawned task to complete
|
||||
assert_eq!(rx.await.unwrap(), 42);
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
#![cfg(all(feature = "wasm-bindgen", target_family = "wasm"))]
|
||||
|
||||
use any_spawner::Executor;
|
||||
use futures::channel::oneshot;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_wasm_bindgen_spawn_local() {
|
||||
// Initialize the wasm-bindgen executor
|
||||
let _ = Executor::init_wasm_bindgen();
|
||||
|
||||
// Create a channel to verify the task completes
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
// Spawn a local task (wasm doesn't support sending futures between threads)
|
||||
Executor::spawn_local(async move {
|
||||
// Simulate some async work
|
||||
Executor::tick().await;
|
||||
tx.send(42).expect("Failed to send result");
|
||||
});
|
||||
|
||||
// Wait for the task to complete
|
||||
let result = rx.await.expect("Failed to receive result");
|
||||
assert_eq!(result, 42);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_wasm_bindgen_tick() {
|
||||
// Initialize the wasm-bindgen executor if not already initialized
|
||||
let _ = Executor::init_wasm_bindgen();
|
||||
|
||||
let flag = Arc::new(AtomicBool::new(false));
|
||||
let flag_clone = flag.clone();
|
||||
|
||||
// Spawn a task that will set the flag
|
||||
Executor::spawn_local(async move {
|
||||
flag_clone.store(true, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
// Wait for a tick, which should allow the spawned task to run
|
||||
Executor::tick().await;
|
||||
|
||||
// Verify the flag was set
|
||||
assert!(flag.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_multiple_wasm_bindgen_tasks() {
|
||||
// Initialize once for all tests
|
||||
let _ = Executor::init_wasm_bindgen();
|
||||
|
||||
// Create channels for multiple tasks
|
||||
let (tx1, rx1) = oneshot::channel();
|
||||
let (tx2, rx2) = oneshot::channel();
|
||||
|
||||
// Spawn multiple tasks
|
||||
Executor::spawn_local(async move {
|
||||
tx1.send("task1").expect("Failed to send from task1");
|
||||
});
|
||||
|
||||
Executor::spawn_local(async move {
|
||||
tx2.send("task2").expect("Failed to send from task2");
|
||||
});
|
||||
|
||||
// Wait for both tasks to complete
|
||||
let (result1, result2) = futures::join!(rx1, rx2);
|
||||
|
||||
assert_eq!(result1.unwrap(), "task1");
|
||||
assert_eq!(result2.unwrap(), "task2");
|
||||
}
|
||||
|
||||
// This test verifies that spawn (not local) fails on wasm as expected
|
||||
#[wasm_bindgen_test]
|
||||
#[should_panic]
|
||||
fn test_wasm_bindgen_spawn_errors() {
|
||||
let _ = Executor::init_wasm_bindgen();
|
||||
|
||||
// Using should_panic to test that Executor::spawn panics in wasm
|
||||
Executor::spawn(async {
|
||||
// This should panic since wasm-bindgen doesn't support Send futures
|
||||
});
|
||||
}
|
||||
@@ -4,32 +4,27 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
l0410 = { package = "leptos", version = "0.4.10", features = [
|
||||
"nightly",
|
||||
"ssr",
|
||||
] }
|
||||
leptos = { path = "../leptos", features = ["ssr", "nightly"] }
|
||||
leptos_reactive = { path = "../leptos_reactive", features = ["ssr", "nightly"] }
|
||||
tachydom = { git = "https://github.com/gbj/tachys", features = [
|
||||
"nightly",
|
||||
"leptos",
|
||||
] }
|
||||
tachy_maccy = { git = "https://github.com/gbj/tachys", features = ["nightly"] }
|
||||
sycamore = { version = "0.8.0", features = ["ssr"] }
|
||||
yew = { version = "0.20.0", features = ["ssr"] }
|
||||
tokio-test = "0.4.0"
|
||||
miniserde = "0.1.0"
|
||||
gloo = "0.8.0"
|
||||
uuid = { version = "1.0", features = ["serde", "v4", "wasm-bindgen"] }
|
||||
wasm-bindgen = "0.2.100"
|
||||
lazy_static = "1.0"
|
||||
log = "0.4.0"
|
||||
strum = "0.24.0"
|
||||
strum_macros = "0.24.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_json = "1.0"
|
||||
tera = "1.0"
|
||||
leptos = { path = "../leptos", default-features = false, features = ["ssr"] }
|
||||
sycamore = { version = "0.8", features = ["ssr"] }
|
||||
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
|
||||
tokio-test = "0.4"
|
||||
miniserde = "0.1"
|
||||
gloo = "0.8"
|
||||
uuid = { version = "1", features = ["serde", "v4", "wasm-bindgen"] }
|
||||
wasm-bindgen = "0.2"
|
||||
lazy_static = "1"
|
||||
log = "0.4"
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
serde = { version = "1", features = ["derive", "rc"]}
|
||||
serde_json = "1"
|
||||
tera = "1"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.0"
|
||||
features = ["Window", "Document", "HtmlElement", "HtmlInputElement"]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"Window",
|
||||
"Document",
|
||||
"HtmlElement",
|
||||
"HtmlInputElement"
|
||||
]
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
extern crate test;
|
||||
|
||||
mod reactive;
|
||||
//mod reactive;
|
||||
mod ssr;
|
||||
mod todomvc;
|
||||
|
||||
@@ -1,227 +1,40 @@
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn leptos_deep_creation(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
let signal = create_rw_signal(0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for _ in 0..1000usize {
|
||||
let prev = memos.last().copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
#[bench]
|
||||
fn leptos_deep_update(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
fn leptos_create_1000_signals(b: &mut Bencher) {
|
||||
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
|
||||
|
||||
b.iter(|| {
|
||||
let signal = create_rw_signal(0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_narrowing_down(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
let sigs = (0..1000).map(|n| create_signal(n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo =
|
||||
create_memo(move |_| reads.iter().map(|r| r.get()).sum::<i32>());
|
||||
assert_eq!(memo(), 499500);
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_fanning_out(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
let sig = create_rw_signal(0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_narrowing_update(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = (0..1000).map(|n| create_signal(n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo =
|
||||
create_memo(move |_| reads.iter().map(|r| r.get()).sum::<i32>());
|
||||
assert_eq!(memo(), 499500);
|
||||
create_isomorphic_effect({
|
||||
let acc = Rc::clone(&acc);
|
||||
move |_| {
|
||||
acc.set(memo());
|
||||
}
|
||||
});
|
||||
assert_eq!(acc.get(), 499500);
|
||||
|
||||
writes[1].update(|n| *n += 1);
|
||||
writes[10].update(|n| *n += 1);
|
||||
writes[100].update(|n| *n += 1);
|
||||
|
||||
assert_eq!(acc.get(), 499503);
|
||||
assert_eq!(memo(), 499503);
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l0410_deep_creation(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l0410_deep_update(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l0410_narrowing_down(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
|
||||
assert_eq!(memo(), 499500);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l0410_fanning_out(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
fn leptos_create_and_update_1000_signals(b: &mut Bencher) {
|
||||
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sig = create_rw_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
#[bench]
|
||||
fn l0410_narrowing_update(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo.get(), 499500);
|
||||
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
|
||||
assert_eq!(memo(), 499500);
|
||||
create_isomorphic_effect(cx, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |_| {
|
||||
acc.set(memo.get());
|
||||
acc.set(memo());
|
||||
}
|
||||
});
|
||||
assert_eq!(acc.get(), 499500);
|
||||
@@ -231,30 +44,27 @@ fn l0410_narrowing_update(b: &mut Bencher) {
|
||||
writes[100].update(|n| *n += 1);
|
||||
|
||||
assert_eq!(acc.get(), 499503);
|
||||
assert_eq!(memo.get(), 499503);
|
||||
assert_eq!(memo(), 499503);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l0410_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
fn leptos_create_and_dispose_1000_scopes(b: &mut Bencher) {
|
||||
use leptos::{create_isomorphic_effect, create_scope, create_signal};
|
||||
|
||||
b.iter(|| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let disposers = (0..1000)
|
||||
.map(|_| {
|
||||
create_scope(runtime, {
|
||||
create_scope({
|
||||
let acc = Rc::clone(&acc);
|
||||
move |cx| {
|
||||
let (r, w) = create_signal(cx, 0);
|
||||
create_isomorphic_effect(cx, {
|
||||
move |_| {
|
||||
acc.set(r.get());
|
||||
acc.set(r());
|
||||
}
|
||||
});
|
||||
w.update(|n| *n += 1);
|
||||
@@ -266,22 +76,16 @@ fn l0410_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
disposer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_narrowing_down(b: &mut Bencher) {
|
||||
use sycamore::reactive::{
|
||||
create_effect, create_memo, create_scope, create_signal,
|
||||
};
|
||||
fn sycamore_create_1000_signals(b: &mut Bencher) {
|
||||
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = Rc::new(
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
|
||||
);
|
||||
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
|
||||
let memo = create_memo(cx, {
|
||||
let sigs = Rc::clone(&sigs);
|
||||
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
|
||||
@@ -293,78 +97,13 @@ fn sycamore_narrowing_down(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_fanning_out(b: &mut Bencher) {
|
||||
use sycamore::reactive::{
|
||||
create_effect, create_memo, create_scope, create_signal,
|
||||
};
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let sig = create_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move || sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 1000);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_deep_creation(b: &mut Bencher) {
|
||||
use sycamore::reactive::*;
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
}
|
||||
}
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_deep_update(b: &mut Bencher) {
|
||||
use sycamore::reactive::*;
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(*memos[999].get(), 1001);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
}
|
||||
#[bench]
|
||||
fn sycamore_narrowing_update(b: &mut Bencher) {
|
||||
use sycamore::reactive::{
|
||||
create_effect, create_memo, create_scope, create_signal,
|
||||
};
|
||||
fn sycamore_create_and_update_1000_signals(b: &mut Bencher) {
|
||||
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = Rc::new(
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
|
||||
);
|
||||
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
|
||||
let memo = create_memo(cx, {
|
||||
let sigs = Rc::clone(&sigs);
|
||||
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
|
||||
@@ -390,7 +129,7 @@ fn sycamore_narrowing_update(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
fn sycamore_create_and_dispose_1000_scopes(b: &mut Bencher) {
|
||||
use sycamore::reactive::{create_effect, create_scope, create_signal};
|
||||
|
||||
b.iter(|| {
|
||||
|
||||
@@ -3,13 +3,14 @@ use test::Bencher;
|
||||
#[bench]
|
||||
fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let r = create_runtime();
|
||||
b.iter(|| {
|
||||
leptos::leptos_dom::HydrationCtx::reset_id();
|
||||
|
||||
b.iter(|| {
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
#[component]
|
||||
fn Counter(initial: i32) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(initial);
|
||||
fn Counter(cx: Scope, initial: i32) -> Element {
|
||||
let (value, set_value) = create_signal(cx, initial);
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
@@ -18,7 +19,8 @@ fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
}
|
||||
}
|
||||
|
||||
let rendered = view! {
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<main>
|
||||
<h1>"Welcome to our benchmark page."</h1>
|
||||
<p>"Here's some introductory text."</p>
|
||||
@@ -26,61 +28,22 @@ fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
<Counter initial=2/>
|
||||
<Counter initial=3/>
|
||||
</main>
|
||||
}.into_view().render_to_string();
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"<main data-hk=\"0-0-0-1\"><h1 data-hk=\"0-0-0-2\">Welcome to our benchmark page.</h1><p data-hk=\"0-0-0-3\">Here's some introductory text.</p><div data-hk=\"0-0-0-5\"><button data-hk=\"0-0-0-6\">-1</button><span data-hk=\"0-0-0-7\">Value: <!>1<!--hk=0-0-0-8-->!</span><button data-hk=\"0-0-0-9\">+1</button></div><!--hk=0-0-0-4--><div data-hk=\"0-0-0-11\"><button data-hk=\"0-0-0-12\">-1</button><span data-hk=\"0-0-0-13\">Value: <!>2<!--hk=0-0-0-14-->!</span><button data-hk=\"0-0-0-15\">+1</button></div><!--hk=0-0-0-10--><div data-hk=\"0-0-0-17\"><button data-hk=\"0-0-0-18\">-1</button><span data-hk=\"0-0-0-19\">Value: <!>3<!--hk=0-0-0-20-->!</span><button data-hk=\"0-0-0-21\">+1</button></div><!--hk=0-0-0-16--></main>" );
|
||||
"<main data-hk=\"0-0\"><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><!--#--><div data-hk=\"0-2-0\"><button>-1</button><span>Value: <!--#-->1<!--/-->!</span><button>+1</button></div><!--/--><!--#--><div data-hk=\"0-3-0\"><button>-1</button><span>Value: <!--#-->2<!--/-->!</span><button>+1</button></div><!--/--><!--#--><div data-hk=\"0-4-0\"><button>-1</button><span>Value: <!--#-->3<!--/-->!</span><button>+1</button></div><!--/--></main>"
|
||||
);
|
||||
});
|
||||
});
|
||||
r.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn tachys_ssr_bench(b: &mut Bencher) {
|
||||
use leptos::{create_runtime, create_signal, SignalGet, SignalUpdate};
|
||||
use tachy_maccy::view;
|
||||
use tachydom::view::{Render, RenderHtml};
|
||||
use tachydom::html::element::ElementChild;
|
||||
use tachydom::html::attribute::global::ClassAttribute;
|
||||
use tachydom::html::attribute::global::GlobalAttributes;
|
||||
use tachydom::html::attribute::global::OnAttribute;
|
||||
use tachydom::renderer::dom::Dom;
|
||||
let rt = create_runtime();
|
||||
b.iter(|| {
|
||||
fn counter(initial: i32) -> impl Render<Dom> + RenderHtml<Dom> {
|
||||
let (value, set_value) = create_signal(initial);
|
||||
view! {
|
||||
<div>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<button on:click=move |_| set_value.update(|value| *value += 1)>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
let rendered = view! {
|
||||
<main>
|
||||
<h1>"Welcome to our benchmark page."</h1>
|
||||
<p>"Here's some introductory text."</p>
|
||||
{counter(1)}
|
||||
{counter(2)}
|
||||
{counter(3)}
|
||||
</main>
|
||||
}.to_html();
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"<main><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><div><button>-1</button><span>Value: <!>1<!>!</span><button>+1</button></div><div><button>-1</button><span>Value: <!>2<!>!</span><button>+1</button></div><div><button>-1</button><span>Value: <!>3<!>!</span><button>+1</button></div></main>"
|
||||
);
|
||||
});
|
||||
rt.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn tera_ssr_bench(b: &mut Bencher) {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::*;
|
||||
use tera::*;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
static TEMPLATE: &str = r#"<main>
|
||||
static TEMPLATE: &str = r#"<main>
|
||||
<h1>Welcome to our benchmark page.</h1>
|
||||
<p>Here's some introductory text.</p>
|
||||
{% for counter in counters %}
|
||||
@@ -92,40 +55,37 @@ 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 {
|
||||
value: i32
|
||||
}
|
||||
|
||||
b.iter(|| {
|
||||
let mut ctx = Context::new();
|
||||
ctx.insert("counters", &vec![
|
||||
Counter { value: 0 },
|
||||
Counter { value: 1},
|
||||
Counter { value: 2 }
|
||||
]);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Counter {
|
||||
value: i32,
|
||||
}
|
||||
|
||||
b.iter(|| {
|
||||
let mut ctx = Context::new();
|
||||
ctx.insert(
|
||||
"counters",
|
||||
&vec![
|
||||
Counter { value: 0 },
|
||||
Counter { value: 1 },
|
||||
Counter { value: 2 },
|
||||
],
|
||||
);
|
||||
|
||||
let _ = TERA.render("template.html", &ctx).unwrap();
|
||||
});
|
||||
let _ = TERA.render("template.html", &ctx).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_ssr_bench(b: &mut Bencher) {
|
||||
use sycamore::prelude::*;
|
||||
use sycamore::*;
|
||||
use sycamore::*;
|
||||
use sycamore::prelude::*;
|
||||
|
||||
b.iter(|| {
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
#[derive(Prop)]
|
||||
struct CounterProps {
|
||||
@@ -179,10 +139,10 @@ fn sycamore_ssr_bench(b: &mut Bencher) {
|
||||
|
||||
#[bench]
|
||||
fn yew_ssr_bench(b: &mut Bencher) {
|
||||
use yew::prelude::*;
|
||||
use yew::ServerRenderer;
|
||||
use yew::prelude::*;
|
||||
use yew::ServerRenderer;
|
||||
|
||||
b.iter(|| {
|
||||
b.iter(|| {
|
||||
#[derive(Properties, PartialEq, Eq, Debug)]
|
||||
struct CounterProps {
|
||||
initial: i32
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
pub use leptos::*;
|
||||
use leptos::*;
|
||||
use miniserde::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::HtmlInputElement;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -9,13 +8,13 @@ pub struct Todos(pub Vec<Todo>);
|
||||
const STORAGE_KEY: &str = "todos-leptos";
|
||||
|
||||
impl Todos {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
Self(vec![])
|
||||
}
|
||||
|
||||
pub fn new_with_1000() -> Self {
|
||||
pub fn new_with_1000(cx: Scope) -> Self {
|
||||
let todos = (0..1000)
|
||||
.map(|id| Todo::new(id, format!("Todo #{id}")))
|
||||
.map(|id| Todo::new(cx, id, format!("Todo #{id}")))
|
||||
.collect();
|
||||
Self(todos)
|
||||
}
|
||||
@@ -72,17 +71,13 @@ pub struct Todo {
|
||||
}
|
||||
|
||||
impl Todo {
|
||||
pub fn new(id: usize, title: String) -> Self {
|
||||
Self::new_with_completed(id, title, false)
|
||||
pub fn new(cx: Scope, id: usize, title: String) -> Self {
|
||||
Self::new_with_completed(cx, id, title, false)
|
||||
}
|
||||
|
||||
pub fn new_with_completed(
|
||||
id: usize,
|
||||
title: String,
|
||||
completed: bool,
|
||||
) -> Self {
|
||||
let (title, set_title) = create_signal(title);
|
||||
let (completed, set_completed) = create_signal(completed);
|
||||
pub fn new_with_completed(cx: Scope, id: usize, title: String, completed: bool) -> Self {
|
||||
let (title, set_title) = create_signal(cx, title);
|
||||
let (completed, set_completed) = create_signal(cx, completed);
|
||||
Self {
|
||||
id,
|
||||
title,
|
||||
@@ -102,7 +97,7 @@ const ESCAPE_KEY: u32 = 27;
|
||||
const ENTER_KEY: u32 = 13;
|
||||
|
||||
#[component]
|
||||
pub fn TodoMVC(todos: Todos) -> impl IntoView {
|
||||
pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
|
||||
let mut next_id = todos
|
||||
.0
|
||||
.iter()
|
||||
@@ -111,10 +106,14 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
|
||||
.map(|last| last + 1)
|
||||
.unwrap_or(0);
|
||||
|
||||
let (todos, set_todos) = create_signal(todos);
|
||||
provide_context(set_todos);
|
||||
let (todos, set_todos) = create_signal(cx, todos);
|
||||
provide_context(cx, set_todos);
|
||||
|
||||
let (mode, set_mode) = create_signal(Mode::All);
|
||||
let (mode, set_mode) = create_signal(cx, Mode::All);
|
||||
window_event_listener("hashchange", move |_| {
|
||||
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
|
||||
set_mode(new_mode);
|
||||
});
|
||||
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let target = event_target::<HtmlInputElement>(&ev);
|
||||
@@ -124,7 +123,7 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
|
||||
let title = event_target_value(&ev);
|
||||
let title = title.trim();
|
||||
if !title.is_empty() {
|
||||
let new = Todo::new(next_id, title.to_string());
|
||||
let new = Todo::new(cx, next_id, title.to_string());
|
||||
set_todos.update(|t| t.add(new));
|
||||
next_id += 1;
|
||||
target.set_value("");
|
||||
@@ -132,7 +131,7 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
|
||||
}
|
||||
};
|
||||
|
||||
let filtered_todos = create_memo::<Vec<Todo>>(move |_| {
|
||||
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
|
||||
todos.with(|todos| match mode.get() {
|
||||
Mode::All => todos.0.to_vec(),
|
||||
Mode::Active => todos
|
||||
@@ -152,7 +151,7 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
|
||||
|
||||
// effect to serialize to JSON
|
||||
// this does reactive reads, so it will automatically serialize on any relevant change
|
||||
create_effect(move |_| {
|
||||
create_effect(cx, move |_| {
|
||||
if let Ok(Some(storage)) = window().local_storage() {
|
||||
let objs = todos
|
||||
.get()
|
||||
@@ -167,67 +166,43 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
view! { cx,
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
<input
|
||||
class="new-todo"
|
||||
placeholder="What needs to be done?"
|
||||
autofocus=""
|
||||
on:keydown=add_todo
|
||||
/>
|
||||
<input class="new-todo" placeholder="What needs to be done?" autofocus on:keydown=add_todo />
|
||||
</header>
|
||||
<section class="main" class:hidden=move || todos.with(|t| t.is_empty())>
|
||||
<input
|
||||
id="toggle-all"
|
||||
class="toggle-all"
|
||||
type="checkbox"
|
||||
prop:checked=move || todos.with(|t| t.remaining() > 0)
|
||||
<section class="main" class:hidden={move || todos.with(|t| t.is_empty())}>
|
||||
<input id="toggle-all" class="toggle-all" type="checkbox"
|
||||
prop:checked={move || todos.with(|t| t.remaining() > 0)}
|
||||
on:input=move |_| set_todos.update(|t| t.toggle_all())
|
||||
/>
|
||||
<label for="toggle-all">"Mark all as complete"</label>
|
||||
<ul class="todo-list">
|
||||
<For
|
||||
each=filtered_todos
|
||||
key=|todo| todo.id
|
||||
children=move |todo: Todo| {
|
||||
view! { <Todo todo=todo.clone()/> }
|
||||
}
|
||||
/>
|
||||
<For each=filtered_todos key=|todo| todo.id>
|
||||
{move |cx, todo: &Todo| view! { cx, <Todo todo=todo.clone() /> }}
|
||||
</For>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" class:hidden=move || todos.with(|t| t.is_empty())>
|
||||
<footer class="footer" class:hidden={move || todos.with(|t| t.is_empty())}>
|
||||
<span class="todo-count">
|
||||
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
|
||||
{move || if todos.with(|t| t.remaining()) == 1 { " item" } else { " items" }}
|
||||
{move || if todos.with(|t| t.remaining()) == 1 {
|
||||
" item"
|
||||
} else {
|
||||
" items"
|
||||
}}
|
||||
" left"
|
||||
</span>
|
||||
<ul class="filters">
|
||||
<li>
|
||||
<a
|
||||
href="#/"
|
||||
class="selected"
|
||||
class:selected=move || mode() == Mode::All
|
||||
>
|
||||
"All"
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/active" class:selected=move || mode() == Mode::Active>
|
||||
"Active"
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/completed" class:selected=move || mode() == Mode::Completed>
|
||||
"Completed"
|
||||
</a>
|
||||
</li>
|
||||
<li><a href="#/" class="selected" class:selected={move || mode() == Mode::All}>"All"</a></li>
|
||||
<li><a href="#/active" class:selected={move || mode() == Mode::Active}>"Active"</a></li>
|
||||
<li><a href="#/completed" class:selected={move || mode() == Mode::Completed}>"Completed"</a></li>
|
||||
</ul>
|
||||
<button
|
||||
class="clear-completed hidden"
|
||||
class:hidden=move || todos.with(|t| t.completed() == 0)
|
||||
class:hidden={move || todos.with(|t| t.completed() == 0)}
|
||||
on:click=move |_| set_todos.update(|t| t.clear_completed())
|
||||
>
|
||||
"Clear completed"
|
||||
@@ -236,18 +211,18 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p>"Double-click to edit a todo"</p>
|
||||
<p>"Created by " <a href="http://todomvc.com">"Greg Johnston"</a></p>
|
||||
<p>"Part of " <a href="http://todomvc.com">"TodoMVC"</a></p>
|
||||
<p>"Created by "<a href="http://todomvc.com">"Greg Johnston"</a></p>
|
||||
<p>"Part of "<a href="http://todomvc.com">"TodoMVC"</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
}.into_view()
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Todo(todo: Todo) -> impl IntoView {
|
||||
let (editing, set_editing) = create_signal(false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>().unwrap();
|
||||
//let input = NodeRef::new();
|
||||
pub fn Todo(cx: Scope, todo: Todo) -> Element {
|
||||
let (editing, set_editing) = create_signal(cx, false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
|
||||
let input = NodeRef::new(cx);
|
||||
|
||||
let save = move |value: &str| {
|
||||
let value = value.trim();
|
||||
@@ -259,39 +234,46 @@ pub fn Todo(todo: Todo) -> impl IntoView {
|
||||
set_editing(false);
|
||||
};
|
||||
|
||||
view! {
|
||||
<li class="todo" class:editing=editing class:completed=move || (todo.completed)()>
|
||||
let tpl = view! { cx,
|
||||
<li
|
||||
class="todo"
|
||||
class:editing={editing}
|
||||
class:completed={move || (todo.completed)()}
|
||||
_ref=input
|
||||
>
|
||||
<div class="view">
|
||||
<input class="toggle" type="checkbox" prop:checked=move || (todo.completed)()/>
|
||||
<label on:dblclick=move |_| set_editing(true)>{move || todo.title.get()}</label>
|
||||
<button
|
||||
class="destroy"
|
||||
on:click=move |_| set_todos.update(|t| t.remove(todo.id))
|
||||
></button>
|
||||
<input
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
prop:checked={move || (todo.completed)()}
|
||||
|
||||
/>
|
||||
<label on:dblclick=move |_| set_editing(true)>
|
||||
{move || todo.title.get()}
|
||||
</label>
|
||||
<button class="destroy" on:click=move |_| set_todos.update(|t| t.remove(todo.id))/>
|
||||
</div>
|
||||
{move || {
|
||||
editing()
|
||||
.then(|| {
|
||||
view! {
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden=move || !(editing)()
|
||||
prop:value=move || todo.title.get()
|
||||
on:focusout=move |ev| save(&event_target_value(&ev))
|
||||
on:keyup=move |ev| {
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
set_editing(false);
|
||||
}
|
||||
}
|
||||
/>
|
||||
{move || editing().then(|| view! { cx,
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden={move || !(editing)()}
|
||||
prop:value={move || todo.title.get()}
|
||||
on:focusout=move |ev| save(&event_target_value(&ev))
|
||||
on:keyup={move |ev| {
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
set_editing(false);
|
||||
}
|
||||
})
|
||||
}}
|
||||
}}
|
||||
/>
|
||||
})
|
||||
}
|
||||
</li>
|
||||
}
|
||||
};
|
||||
|
||||
tpl
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -323,8 +305,8 @@ pub struct TodoSerialized {
|
||||
}
|
||||
|
||||
impl TodoSerialized {
|
||||
pub fn into_todo(self, ) -> Todo {
|
||||
Todo::new_with_completed(self.id, self.title, self.completed)
|
||||
pub fn into_todo(self, cx: Scope) -> Todo {
|
||||
Todo::new_with_completed(cx, self.id, self.title, self.completed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,45 +2,31 @@ use test::Bencher;
|
||||
|
||||
mod leptos;
|
||||
mod sycamore;
|
||||
mod tachys;
|
||||
mod tera;
|
||||
mod yew;
|
||||
|
||||
#[bench]
|
||||
fn leptos_todomvc_ssr(b: &mut Bencher) {
|
||||
use self::leptos::*;
|
||||
use ::leptos::*;
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
use crate::todomvc::leptos::*;
|
||||
|
||||
let html = ::leptos::ssr::render_to_string(|| {
|
||||
view! { <TodoMVC todos=Todos::new()/> }
|
||||
b.iter(|| {
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new(cx)/>
|
||||
};
|
||||
|
||||
assert!(rendered.len() > 1);
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn tachys_todomvc_ssr(b: &mut Bencher) {
|
||||
use ::leptos::*;
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
use crate::todomvc::tachys::*;
|
||||
use tachydom::view::{Render, RenderHtml};
|
||||
|
||||
let rendered = TodoMVC(Todos::new()).to_html();
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"<main><section class=\"todoapp\"><header class=\"header\"><h1>todos</h1><input placeholder=\"What needs to be done?\" autofocus class=\"new-todo\"></header><section class=\"main hidden\"><input id=\"toggle-all\" type=\"checkbox\" class=\"toggle-all\"><label for=\"toggle-all\">Mark all as complete</label><ul class=\"todo-list\"></ul></section><footer class=\"footer hidden\"><span class=\"todo-count\"><strong>0</strong><!> items<!> left</span><ul class=\"filters\"><li><a href=\"#/\" class=\"selected selected\">All</a></li><li><a href=\"#/active\" class=\"\">Active</a></li><li><a href=\"#/completed\" class=\"\">Completed</a></li></ul><button class=\"clear-completed hidden hidden\">Clear completed</button></footer></section><footer class=\"info\"><p>Double-click to edit a todo</p><p>Created by <a href=\"http://todomvc.com\">Greg Johnston</a></p><p>Part of <a href=\"http://todomvc.com\">TodoMVC</a></p></footer></main>" );
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_todomvc_ssr(b: &mut Bencher) {
|
||||
use self::sycamore::*;
|
||||
use ::sycamore::{prelude::*, *};
|
||||
use ::sycamore::prelude::*;
|
||||
use ::sycamore::*;
|
||||
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
@@ -59,7 +45,8 @@ fn sycamore_todomvc_ssr(b: &mut Bencher) {
|
||||
#[bench]
|
||||
fn yew_todomvc_ssr(b: &mut Bencher) {
|
||||
use self::yew::*;
|
||||
use ::yew::{prelude::*, ServerRenderer};
|
||||
use ::yew::prelude::*;
|
||||
use ::yew::ServerRenderer;
|
||||
|
||||
b.iter(|| {
|
||||
tokio_test::block_on(async {
|
||||
@@ -72,37 +59,26 @@ fn yew_todomvc_ssr(b: &mut Bencher) {
|
||||
|
||||
#[bench]
|
||||
fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
b.iter(|| {
|
||||
use self::leptos::*;
|
||||
use ::leptos::*;
|
||||
|
||||
let html = ::leptos::ssr::render_to_string(|| {
|
||||
view! {
|
||||
<TodoMVC todos=Todos::new_with_1000()/>
|
||||
}
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn tachys_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
use self::leptos::*;
|
||||
use ::leptos::*;
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
use crate::todomvc::tachys::*;
|
||||
use tachydom::view::{Render, RenderHtml};
|
||||
|
||||
let rendered = TodoMVC(Todos::new_with_1000()).to_html();
|
||||
assert!(rendered.len() > 20_000)
|
||||
b.iter(|| {
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new_with_1000(cx)/>
|
||||
};
|
||||
|
||||
assert!(rendered.len() > 1);
|
||||
});
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
use self::sycamore::*;
|
||||
use ::sycamore::{prelude::*, *};
|
||||
use ::sycamore::prelude::*;
|
||||
use ::sycamore::*;
|
||||
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
@@ -121,7 +97,8 @@ fn sycamore_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
#[bench]
|
||||
fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
use self::yew::*;
|
||||
use ::yew::{prelude::*, ServerRenderer};
|
||||
use ::yew::prelude::*;
|
||||
use ::yew::ServerRenderer;
|
||||
|
||||
b.iter(|| {
|
||||
tokio_test::block_on(async {
|
||||
@@ -131,18 +108,3 @@ fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn tera_todomvc_ssr(b: &mut Bencher) {
|
||||
use ::leptos::*;
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
use crate::todomvc::leptos::*;
|
||||
|
||||
let html = ::leptos::ssr::render_to_string(|| {
|
||||
view! { <TodoMVC todos=Todos::new()/> }
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
@@ -1,333 +0,0 @@
|
||||
pub use leptos_reactive::*;
|
||||
use miniserde::*;
|
||||
use tachy_maccy::view;
|
||||
use tachydom::{
|
||||
html::{
|
||||
attribute::global::{ClassAttribute, GlobalAttributes, OnAttribute},
|
||||
element::ElementChild,
|
||||
},
|
||||
renderer::dom::Dom,
|
||||
view::{keyed::keyed, Render, RenderHtml},
|
||||
};
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::HtmlInputElement;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Todos(pub Vec<Todo>);
|
||||
|
||||
const STORAGE_KEY: &str = "todos-leptos";
|
||||
|
||||
impl Todos {
|
||||
pub fn new() -> Self {
|
||||
Self(vec![])
|
||||
}
|
||||
|
||||
pub fn new_with_1000() -> Self {
|
||||
let todos = (0..1000)
|
||||
.map(|id| Todo::new(id, format!("Todo #{id}")))
|
||||
.collect();
|
||||
Self(todos)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn add(&mut self, todo: Todo) {
|
||||
self.0.push(todo);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: usize) {
|
||||
self.0.retain(|todo| todo.id != id);
|
||||
}
|
||||
|
||||
pub fn remaining(&self) -> usize {
|
||||
self.0.iter().filter(|todo| !(todo.completed)()).count()
|
||||
}
|
||||
|
||||
pub fn completed(&self) -> usize {
|
||||
self.0.iter().filter(|todo| (todo.completed)()).count()
|
||||
}
|
||||
|
||||
pub fn toggle_all(&self) {
|
||||
// if all are complete, mark them all active instead
|
||||
if self.remaining() == 0 {
|
||||
for todo in &self.0 {
|
||||
if todo.completed.get() {
|
||||
(todo.set_completed)(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
// otherwise, mark them all complete
|
||||
else {
|
||||
for todo in &self.0 {
|
||||
(todo.set_completed)(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_completed(&mut self) {
|
||||
self.0.retain(|todo| !todo.completed.get());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Todo {
|
||||
pub id: usize,
|
||||
pub title: ReadSignal<String>,
|
||||
pub set_title: WriteSignal<String>,
|
||||
pub completed: ReadSignal<bool>,
|
||||
pub set_completed: WriteSignal<bool>,
|
||||
}
|
||||
|
||||
impl Todo {
|
||||
pub fn new(id: usize, title: String) -> Self {
|
||||
Self::new_with_completed(id, title, false)
|
||||
}
|
||||
|
||||
pub fn new_with_completed(
|
||||
id: usize,
|
||||
title: String,
|
||||
completed: bool,
|
||||
) -> Self {
|
||||
let (title, set_title) = create_signal(title);
|
||||
let (completed, set_completed) = create_signal(completed);
|
||||
Self {
|
||||
id,
|
||||
title,
|
||||
set_title,
|
||||
completed,
|
||||
set_completed,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(&self) {
|
||||
self.set_completed
|
||||
.update(|completed| *completed = !*completed);
|
||||
}
|
||||
}
|
||||
|
||||
const ESCAPE_KEY: u32 = 27;
|
||||
const ENTER_KEY: u32 = 13;
|
||||
|
||||
pub fn TodoMVC(todos: Todos) -> impl Render<Dom> + RenderHtml<Dom> {
|
||||
let mut next_id = todos
|
||||
.0
|
||||
.iter()
|
||||
.map(|todo| todo.id)
|
||||
.max()
|
||||
.map(|last| last + 1)
|
||||
.unwrap_or(0);
|
||||
|
||||
let (todos, set_todos) = create_signal(todos);
|
||||
provide_context(set_todos);
|
||||
|
||||
let (mode, set_mode) = create_signal(Mode::All);
|
||||
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
todo!()
|
||||
/* let target = event_target::<HtmlInputElement>(&ev);
|
||||
ev.stop_propagation();
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
let title = event_target_value(&ev);
|
||||
let title = title.trim();
|
||||
if !title.is_empty() {
|
||||
let new = Todo::new(next_id, title.to_string());
|
||||
set_todos.update(|t| t.add(new));
|
||||
next_id += 1;
|
||||
target.set_value("");
|
||||
}
|
||||
} */
|
||||
};
|
||||
|
||||
let filtered_todos = create_memo::<Vec<Todo>>(move |_| {
|
||||
todos.with(|todos| match mode.get() {
|
||||
Mode::All => todos.0.to_vec(),
|
||||
Mode::Active => todos
|
||||
.0
|
||||
.iter()
|
||||
.filter(|todo| !todo.completed.get())
|
||||
.cloned()
|
||||
.collect(),
|
||||
Mode::Completed => todos
|
||||
.0
|
||||
.iter()
|
||||
.filter(|todo| todo.completed.get())
|
||||
.cloned()
|
||||
.collect(),
|
||||
})
|
||||
});
|
||||
|
||||
// effect to serialize to JSON
|
||||
// this does reactive reads, so it will automatically serialize on any relevant change
|
||||
create_effect(move |_| {
|
||||
()
|
||||
/* if let Ok(Some(storage)) = window().local_storage() {
|
||||
let objs = todos
|
||||
.get()
|
||||
.0
|
||||
.iter()
|
||||
.map(TodoSerialized::from)
|
||||
.collect::<Vec<_>>();
|
||||
let json = json::to_string(&objs);
|
||||
if storage.set_item(STORAGE_KEY, &json).is_err() {
|
||||
log::error!("error while trying to set item in localStorage");
|
||||
}
|
||||
} */
|
||||
});
|
||||
|
||||
view! {
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
<input
|
||||
class="new-todo"
|
||||
placeholder="What needs to be done?"
|
||||
autofocus
|
||||
/>
|
||||
</header>
|
||||
<section class="main" class:hidden=move || todos.with(|t| t.is_empty())>
|
||||
<input
|
||||
id="toggle-all"
|
||||
class="toggle-all"
|
||||
r#type="checkbox"
|
||||
//prop:checked=move || todos.with(|t| t.remaining() > 0)
|
||||
on:input=move |_| set_todos.update(|t| t.toggle_all())
|
||||
/>
|
||||
<label r#for="toggle-all">"Mark all as complete"</label>
|
||||
<ul class="todo-list">
|
||||
{move || {
|
||||
keyed(filtered_todos.get(), |todo| todo.id, Todo)
|
||||
}}
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" class:hidden=move || todos.with(|t| t.is_empty())>
|
||||
<span class="todo-count">
|
||||
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
|
||||
{move || if todos.with(|t| t.remaining()) == 1 { " item" } else { " items" }}
|
||||
" left"
|
||||
</span>
|
||||
<ul class="filters">
|
||||
<li>
|
||||
<a
|
||||
href="#/"
|
||||
class="selected"
|
||||
class:selected=move || mode() == Mode::All
|
||||
>
|
||||
"All"
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/active" class:selected=move || mode() == Mode::Active>
|
||||
"Active"
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/completed" class:selected=move || mode() == Mode::Completed>
|
||||
"Completed"
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
class="clear-completed hidden"
|
||||
class:hidden=move || todos.with(|t| t.completed() == 0)
|
||||
on:click=move |_| set_todos.update(|t| t.clear_completed())
|
||||
>
|
||||
"Clear completed"
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p>"Double-click to edit a todo"</p>
|
||||
<p>"Created by " <a href="http://todomvc.com">"Greg Johnston"</a></p>
|
||||
<p>"Part of " <a href="http://todomvc.com">"TodoMVC"</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
pub fn Todo(todo: Todo) -> impl Render<Dom> + RenderHtml<Dom> {
|
||||
let (editing, set_editing) = create_signal(false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>().unwrap();
|
||||
//let input = NodeRef::new();
|
||||
|
||||
let save = move |value: &str| {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
set_todos.update(|t| t.remove(todo.id));
|
||||
} else {
|
||||
(todo.set_title)(value.to_string());
|
||||
}
|
||||
set_editing(false);
|
||||
};
|
||||
|
||||
view! {
|
||||
<li class="todo" class:editing=editing class:completed=move || (todo.completed)()>
|
||||
/* <div class="view">
|
||||
<input class="toggle" r#type="checkbox"/>
|
||||
<label on:dblclick=move |_| set_editing(true)>{move || todo.title.get()}</label>
|
||||
<button
|
||||
class="destroy"
|
||||
on:click=move |_| set_todos.update(|t| t.remove(todo.id))
|
||||
></button>
|
||||
</div>
|
||||
{move || {
|
||||
editing()
|
||||
.then(|| {
|
||||
view! {
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden=move || !(editing)()
|
||||
/>
|
||||
}
|
||||
})
|
||||
}} */
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
Active,
|
||||
Completed,
|
||||
All,
|
||||
}
|
||||
|
||||
impl Default for Mode {
|
||||
fn default() -> Self {
|
||||
Mode::All
|
||||
}
|
||||
}
|
||||
|
||||
pub fn route(hash: &str) -> Mode {
|
||||
match hash {
|
||||
"/active" => Mode::Active,
|
||||
"/completed" => Mode::Completed,
|
||||
_ => Mode::All,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TodoSerialized {
|
||||
pub id: usize,
|
||||
pub title: String,
|
||||
pub completed: bool,
|
||||
}
|
||||
|
||||
impl TodoSerialized {
|
||||
pub fn into_todo(self) -> Todo {
|
||||
Todo::new_with_completed(self.id, self.title, self.completed)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Todo> for TodoSerialized {
|
||||
fn from(todo: &Todo) -> Self {
|
||||
Self {
|
||||
id: todo.id,
|
||||
title: todo.title.get(),
|
||||
completed: (todo.completed)(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 %}
|
||||
@@ -87,17 +87,17 @@ static TEMPLATE: &str = r#"<main>
|
||||
</main>"#;
|
||||
|
||||
#[bench]
|
||||
fn tera_todomvc_ssr(b: &mut Bencher) {
|
||||
fn tera_todomvc(b: &mut Bencher) {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::*;
|
||||
|
||||
|
||||
static LazyLock<TERA>: Tera = LazyLock( || {
|
||||
lazy_static::lazy_static! {
|
||||
static ref TERA: Tera = {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
|
||||
tera
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Todo {
|
||||
@@ -127,17 +127,17 @@ fn tera_todomvc_ssr(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn tera_todomvc_ssr_1000(b: &mut Bencher) {
|
||||
fn tera_todomvc_1000(b: &mut Bencher) {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::*;
|
||||
|
||||
|
||||
static TERA: LazyLock<Tera> = LazyLock::new(|| {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
|
||||
tera
|
||||
});
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref TERA: Tera = {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
|
||||
tera
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Todo {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
[tasks.check-minimal-versions]
|
||||
condition = { channels = ["nightly"] }
|
||||
command = "cargo"
|
||||
args = [
|
||||
"all-features",
|
||||
"minimal-versions",
|
||||
"check",
|
||||
"--ignore-private",
|
||||
"--detach-path-deps",
|
||||
"--direct",
|
||||
]
|
||||
install_script = '''
|
||||
cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
'''
|
||||
@@ -1,20 +0,0 @@
|
||||
[tasks.lint]
|
||||
dependencies = ["check-format-flow", "clippy-each-feature"]
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../" }
|
||||
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]
|
||||
|
||||
[tasks.clippy-each-feature]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"all-features",
|
||||
"clippy",
|
||||
"--no-deps",
|
||||
"--",
|
||||
"-D",
|
||||
"clippy::print_stdout",
|
||||
]
|
||||
install_script = '''
|
||||
cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
'''
|
||||
@@ -1,15 +0,0 @@
|
||||
extend = [
|
||||
{ path = "./lint.toml" },
|
||||
{ path = "./test.toml" },
|
||||
{ path = "./check-minimal-versions.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
RUSTFLAGS = ""
|
||||
LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
|
||||
|
||||
[env.github-actions]
|
||||
RUSTFLAGS = "-D warnings"
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = ["lint", "test-each-feature", "doctests"]
|
||||
@@ -1,16 +0,0 @@
|
||||
[tasks.test-each-feature]
|
||||
env = { "NEXTEST_NO_TESTS" = "warn" }
|
||||
command = "cargo"
|
||||
args = ["all-features", "nextest", "run", "--all-targets"]
|
||||
install_script = '''
|
||||
cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
'''
|
||||
|
||||
# This can be removed once doctests is supported in nextest
|
||||
# https://github.com/nextest-rs/nextest/issues/16
|
||||
[tasks.doctests]
|
||||
command = "cargo"
|
||||
args = ["all-features", "test", "--doc"]
|
||||
install_script = '''
|
||||
cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
'''
|
||||
@@ -1,7 +0,0 @@
|
||||
[tasks.post-test]
|
||||
dependencies = ["test-wasm"]
|
||||
|
||||
[tasks.test-wasm]
|
||||
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome --features=wasm-bindgen" }
|
||||
command = "cargo"
|
||||
args = ["make", "wasm-pack-test"]
|
||||
@@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "const_str_slice_concat"
|
||||
version = "0.1.0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Utilities for const concatenation of string slices."
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
@@ -1 +0,0 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
@@ -1,139 +0,0 @@
|
||||
#![no_std]
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
//! Utilities for const concatenation of string slices.
|
||||
|
||||
pub(crate) const MAX_TEMPLATE_SIZE: usize = 4096;
|
||||
|
||||
/// Converts a zero-terminated buffer of bytes into a UTF-8 string.
|
||||
pub const fn str_from_buffer(buf: &[u8; MAX_TEMPLATE_SIZE]) -> &str {
|
||||
match core::ffi::CStr::from_bytes_until_nul(buf) {
|
||||
Ok(cstr) => match cstr.to_str() {
|
||||
Ok(str) => str,
|
||||
Err(_) => panic!("TEMPLATE FAILURE"),
|
||||
},
|
||||
Err(_) => panic!("TEMPLATE FAILURE"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Concatenates any number of static strings into a single array.
|
||||
// credit to Rainer Stropek, "Constant fun," Rust Linz, June 2022
|
||||
pub const fn const_concat(
|
||||
strs: &'static [&'static str],
|
||||
) -> [u8; MAX_TEMPLATE_SIZE] {
|
||||
let mut buffer = [0; MAX_TEMPLATE_SIZE];
|
||||
let mut position = 0;
|
||||
let mut remaining = strs;
|
||||
|
||||
while let [current, tail @ ..] = remaining {
|
||||
let x = current.as_bytes();
|
||||
let mut i = 0;
|
||||
|
||||
// have it iterate over bytes manually, because, again,
|
||||
// no mutable references in const fns
|
||||
while i < x.len() {
|
||||
buffer[position] = x[i];
|
||||
position += 1;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
remaining = tail;
|
||||
}
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
/// Converts a zero-terminated buffer of bytes into a UTF-8 string with the given prefix.
|
||||
pub const fn const_concat_with_prefix(
|
||||
strs: &'static [&'static str],
|
||||
prefix: &'static str,
|
||||
suffix: &'static str,
|
||||
) -> [u8; MAX_TEMPLATE_SIZE] {
|
||||
let mut buffer = [0; MAX_TEMPLATE_SIZE];
|
||||
let mut position = 0;
|
||||
let mut remaining = strs;
|
||||
|
||||
while let [current, tail @ ..] = remaining {
|
||||
let x = current.as_bytes();
|
||||
let mut i = 0;
|
||||
|
||||
// have it iterate over bytes manually, because, again,
|
||||
// no mutable references in const fns
|
||||
while i < x.len() {
|
||||
buffer[position] = x[i];
|
||||
position += 1;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
remaining = tail;
|
||||
}
|
||||
|
||||
if buffer[0] == 0 {
|
||||
buffer
|
||||
} else {
|
||||
let mut new_buf = [0; MAX_TEMPLATE_SIZE];
|
||||
let prefix = prefix.as_bytes();
|
||||
let suffix = suffix.as_bytes();
|
||||
let mut position = 0;
|
||||
let mut i = 0;
|
||||
while i < prefix.len() {
|
||||
new_buf[position] = prefix[i];
|
||||
position += 1;
|
||||
i += 1;
|
||||
}
|
||||
i = 0;
|
||||
while i < buffer.len() {
|
||||
if buffer[i] == 0 {
|
||||
break;
|
||||
}
|
||||
new_buf[position] = buffer[i];
|
||||
position += 1;
|
||||
i += 1;
|
||||
}
|
||||
i = 0;
|
||||
while i < suffix.len() {
|
||||
new_buf[position] = suffix[i];
|
||||
position += 1;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
new_buf
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts any number of strings into a UTF-8 string, separated by the given string.
|
||||
pub const fn const_concat_with_separator(
|
||||
strs: &[&str],
|
||||
separator: &'static str,
|
||||
) -> [u8; MAX_TEMPLATE_SIZE] {
|
||||
let mut buffer = [0; MAX_TEMPLATE_SIZE];
|
||||
let mut position = 0;
|
||||
let mut remaining = strs;
|
||||
|
||||
while let [current, tail @ ..] = remaining {
|
||||
let x = current.as_bytes();
|
||||
let mut i = 0;
|
||||
|
||||
// have it iterate over bytes manually, because, again,
|
||||
// no mutable references in const fns
|
||||
while i < x.len() {
|
||||
buffer[position] = x[i];
|
||||
position += 1;
|
||||
i += 1;
|
||||
}
|
||||
if !x.is_empty() {
|
||||
let mut position = 0;
|
||||
let separator = separator.as_bytes();
|
||||
while i < separator.len() {
|
||||
buffer[position] = separator[i];
|
||||
position += 1;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
remaining = tail;
|
||||
}
|
||||
|
||||
buffer
|
||||
}
|
||||
@@ -9,10 +9,10 @@ This document is intended as a running list of common issues, with example code
|
||||
**Issue**: Sometimes you want to update a reactive signal in a way that depends on another signal.
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(0);
|
||||
let (b, set_b) = create_signal(false);
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (b, set_a) = create_signal(cx, false);
|
||||
|
||||
create_effect(move |_| {
|
||||
create_effect(cx, move |_| {
|
||||
if a() > 5 {
|
||||
set_b(true);
|
||||
}
|
||||
@@ -24,56 +24,10 @@ This creates an inefficient chain of updates, and can easily lead to infinite lo
|
||||
**Solution**: Follow the rule, _What can be derived, should be derived._ In this case, this has the benefit of massively reducing the code size, too!
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(0);
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let b = move || a () > 5;
|
||||
```
|
||||
|
||||
### Nested signal updates/reads triggering panic
|
||||
|
||||
Sometimes you have nested signals: for example, hash-map that can change over time, each of whose values can also change over time:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let resources = create_rw_signal(HashMap::new());
|
||||
|
||||
let update = move |id: usize| {
|
||||
resources.update(|resources| {
|
||||
resources
|
||||
.entry(id)
|
||||
.or_insert_with(|| create_rw_signal(0))
|
||||
.update(|amount| *amount += 1)
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<pre>{move || format!("{:#?}", resources.get().into_iter().map(|(id, resource)| (id, resource.get())).collect::<Vec<_>>())}</pre>
|
||||
<button on:click=move |_| update(1)>"+"</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Clicking the button twice will cause a panic, because of the nested signal _read_. Calling the `update` function on `resources` immediately takes out a mutable borrow on `resources`, then updates the `resource` signal—which re-runs the effect that reads from the signals, which tries to immutably access `resources` and panics. It's the nested update here which causes a problem, because the inner update triggers and effect that tries to read both signals while the outer is still updating.
|
||||
|
||||
You can fix this fairly easily by using the [`batch()`](https://docs.rs/leptos/latest/leptos/fn.batch.html) method:
|
||||
|
||||
```rust
|
||||
let update = move |id: usize| {
|
||||
batch(move || {
|
||||
resources.update(|resources| {
|
||||
resources
|
||||
.entry(id)
|
||||
.or_insert_with(|| create_rw_signal(0))
|
||||
.update(|amount| *amount += 1)
|
||||
})
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
This delays running any effects until after both updates are made, preventing the conflict entirely without requiring any other restructuring.
|
||||
|
||||
## Templates and the DOM
|
||||
|
||||
### `<input value=...>` doesn't update or stops updating
|
||||
@@ -83,11 +37,11 @@ Many DOM attributes can be updated either by setting an attribute on the DOM nod
|
||||
This means that in practice, attributes like `value` or `checked` on an `<input/>` element only update the _default_ value for the `<input/>`. If you want to reactively update the value, you should use `prop:value` instead to set the `value` property.
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal("Starting value".to_string());
|
||||
let (a, set_a) = create_signal(cx, "Starting value".to_string());
|
||||
let on_input = move |ev| set_a(event_target_value(&ev));
|
||||
|
||||
view! {
|
||||
|
||||
cx,
|
||||
// ❌ reactivity doesn't work as expected: typing only updates the default
|
||||
// of each input, so if you start typing in the second input, it won't
|
||||
// update the first one
|
||||
@@ -97,29 +51,13 @@ view! {
|
||||
```
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal("Starting value".to_string());
|
||||
let (a, set_a) = create_signal(cx, "Starting value".to_string());
|
||||
let on_input = move |ev| set_a(event_target_value(&ev));
|
||||
|
||||
view! {
|
||||
|
||||
cx,
|
||||
// ✅ works as intended by setting the value *property*
|
||||
<input prop:value=a on:input=on_input />
|
||||
<input prop:value=a on:input=on_input />
|
||||
}
|
||||
```
|
||||
|
||||
## Build configuration
|
||||
|
||||
### Cargo feature resolution in workspaces
|
||||
|
||||
A new [version](https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions) of Cargo's feature resolver was introduced for the 2021 edition of Rust.
|
||||
For single crate projects it will select a resolver version based on the Rust edition in `Cargo.toml`. As there is no Rust edition present for `Cargo.toml` in a workspace, Cargo will default to the pre 2021 edition resolver.
|
||||
This can cause issues resulting in non WASM compatible code being built for a WASM target. Seeing `mio` failing to build is often a sign that none WASM compatible code is being included in the build.
|
||||
|
||||
The resolver version can be set in the workspace `Cargo.toml` to remedy this issue.
|
||||
|
||||
```toml
|
||||
[workspace]
|
||||
members = ["member1", "member2"]
|
||||
resolver = "2"
|
||||
```
|
||||
|
||||
2
docs/book/.gitignore
vendored
2
docs/book/.gitignore
vendored
@@ -1 +1 @@
|
||||
book
|
||||
book
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
The Leptos book is now available at [https://book.leptos.dev](https://book.leptos.dev).
|
||||
|
||||
The source code for the book has moved to [https://github.com/leptos-rs/book](https://github.com/leptos-rs/book). Please open issues or make PRs in that repository.
|
||||
@@ -1,10 +1,16 @@
|
||||
[output.html]
|
||||
additional-css = ["./mdbook-admonish.css"]
|
||||
[output.html.playground]
|
||||
runnable = false
|
||||
[book]
|
||||
authors = ["Greg Johnston"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "The Leptos Guide"
|
||||
|
||||
[preprocessor]
|
||||
|
||||
[preprocessor.admonish]
|
||||
command = "mdbook-admonish"
|
||||
assets_version = "3.0.1" # do not edit: managed by `mdbook-admonish install`
|
||||
[preprocessor.mermaid]
|
||||
command = "mdbook-mermaid"
|
||||
|
||||
[output]
|
||||
|
||||
[output.html]
|
||||
additional-js = ["mermaid.min.js", "mermaid-init.js"]
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
@charset "UTF-8";
|
||||
:root {
|
||||
--md-admonition-icon--admonish-note: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z'/></svg>");
|
||||
--md-admonition-icon--admonish-abstract: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'/></svg>");
|
||||
--md-admonition-icon--admonish-info: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z'/></svg>");
|
||||
--md-admonition-icon--admonish-tip: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17.66 11.2c-.23-.3-.51-.56-.77-.82-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32-2.59 2.08-3.61 5.75-2.39 8.9.04.1.08.2.08.33 0 .22-.15.42-.35.5-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5.14.6.41 1.2.71 1.73 1.08 1.73 2.95 2.97 4.96 3.22 2.14.27 4.43-.12 6.07-1.6 1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6-1.12.4-2.24-.16-2.9-.82 1.19-.28 1.9-1.16 2.11-2.05.17-.8-.15-1.46-.28-2.23-.12-.74-.1-1.37.17-2.06.19.38.39.76.63 1.06.77 1 1.98 1.44 2.24 2.8.04.14.06.28.06.43.03.82-.33 1.72-.93 2.27z'/></svg>");
|
||||
--md-admonition-icon--admonish-success: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m9 20.42-6.21-6.21 2.83-2.83L9 14.77l9.88-9.89 2.83 2.83L9 20.42z'/></svg>");
|
||||
--md-admonition-icon--admonish-question: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m15.07 11.25-.9.92C13.45 12.89 13 13.5 13 15h-2v-.5c0-1.11.45-2.11 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41a2 2 0 0 0-2-2 2 2 0 0 0-2 2H8a4 4 0 0 1 4-4 4 4 0 0 1 4 4 3.2 3.2 0 0 1-.93 2.25M13 19h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10c0-5.53-4.5-10-10-10z'/></svg>");
|
||||
--md-admonition-icon--admonish-warning: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 14h-2V9h2m0 9h-2v-2h2M1 21h22L12 2 1 21z'/></svg>");
|
||||
--md-admonition-icon--admonish-failure: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20 6.91 17.09 4 12 9.09 6.91 4 4 6.91 9.09 12 4 17.09 6.91 20 12 14.91 17.09 20 20 17.09 14.91 12 20 6.91z'/></svg>");
|
||||
--md-admonition-icon--admonish-danger: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M11 15H6l7-14v8h5l-7 14v-8z'/></svg>");
|
||||
--md-admonition-icon--admonish-bug: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 0 0-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z'/></svg>");
|
||||
--md-admonition-icon--admonish-example: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M7 13v-2h14v2H7m0 6v-2h14v2H7M7 7V5h14v2H7M3 8V5H2V4h2v4H3m-1 9v-1h3v4H2v-1h2v-.5H3v-1h1V17H2m2.25-7a.75.75 0 0 1 .75.75c0 .2-.08.39-.21.52L3.12 13H5v1H2v-.92L4 11H2v-1h2.25z'/></svg>");
|
||||
--md-admonition-icon--admonish-quote: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 17h3l2-4V7h-6v6h3M6 17h3l2-4V7H5v6h3l-2 4z'/></svg>");
|
||||
--md-details-icon: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42Z'/></svg>");
|
||||
}
|
||||
|
||||
:is(.admonition) {
|
||||
display: flow-root;
|
||||
margin: 1.5625em 0;
|
||||
padding: 0 1.2rem;
|
||||
color: var(--fg);
|
||||
page-break-inside: avoid;
|
||||
background-color: var(--bg);
|
||||
border: 0 solid black;
|
||||
border-inline-start-width: 0.4rem;
|
||||
border-radius: 0.2rem;
|
||||
box-shadow: 0 0.2rem 1rem rgba(0, 0, 0, 0.05), 0 0 0.1rem rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
@media print {
|
||||
:is(.admonition) {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
:is(.admonition) > * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:is(.admonition) :is(.admonition) {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
:is(.admonition) > .tabbed-set:only-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
html :is(.admonition) > :last-child {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
a.admonition-anchor-link {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: -1.2rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
a.admonition-anchor-link:link, a.admonition-anchor-link:visited {
|
||||
color: var(--fg);
|
||||
}
|
||||
a.admonition-anchor-link:link:hover, a.admonition-anchor-link:visited:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
a.admonition-anchor-link::before {
|
||||
content: "§";
|
||||
}
|
||||
|
||||
:is(.admonition-title, summary.admonition-title) {
|
||||
position: relative;
|
||||
min-height: 4rem;
|
||||
margin-block: 0;
|
||||
margin-inline: -1.6rem -1.2rem;
|
||||
padding-block: 0.8rem;
|
||||
padding-inline: 4.4rem 1.2rem;
|
||||
font-weight: 700;
|
||||
background-color: rgba(68, 138, 255, 0.1);
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
display: flex;
|
||||
}
|
||||
:is(.admonition-title, summary.admonition-title) p {
|
||||
margin: 0;
|
||||
}
|
||||
html :is(.admonition-title, summary.admonition-title):last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
:is(.admonition-title, summary.admonition-title)::before {
|
||||
position: absolute;
|
||||
top: 0.625em;
|
||||
inset-inline-start: 1.6rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background-color: #448aff;
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"></svg>');
|
||||
-webkit-mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"></svg>');
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-size: contain;
|
||||
content: "";
|
||||
}
|
||||
:is(.admonition-title, summary.admonition-title):hover a.admonition-anchor-link {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
details.admonition > summary.admonition-title::after {
|
||||
position: absolute;
|
||||
top: 0.625em;
|
||||
inset-inline-end: 1.6rem;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
background-color: currentcolor;
|
||||
mask-image: var(--md-details-icon);
|
||||
-webkit-mask-image: var(--md-details-icon);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-size: contain;
|
||||
content: "";
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.25s;
|
||||
}
|
||||
details[open].admonition > summary.admonition-title::after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-note) {
|
||||
border-color: #448aff;
|
||||
}
|
||||
|
||||
:is(.admonish-note) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(68, 138, 255, 0.1);
|
||||
}
|
||||
:is(.admonish-note) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #448aff;
|
||||
mask-image: var(--md-admonition-icon--admonish-note);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-note);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-abstract, .admonish-summary, .admonish-tldr) {
|
||||
border-color: #00b0ff;
|
||||
}
|
||||
|
||||
:is(.admonish-abstract, .admonish-summary, .admonish-tldr) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 176, 255, 0.1);
|
||||
}
|
||||
:is(.admonish-abstract, .admonish-summary, .admonish-tldr) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00b0ff;
|
||||
mask-image: var(--md-admonition-icon--admonish-abstract);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-abstract);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-info, .admonish-todo) {
|
||||
border-color: #00b8d4;
|
||||
}
|
||||
|
||||
:is(.admonish-info, .admonish-todo) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 184, 212, 0.1);
|
||||
}
|
||||
:is(.admonish-info, .admonish-todo) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00b8d4;
|
||||
mask-image: var(--md-admonition-icon--admonish-info);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-info);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-tip, .admonish-hint, .admonish-important) {
|
||||
border-color: #00bfa5;
|
||||
}
|
||||
|
||||
:is(.admonish-tip, .admonish-hint, .admonish-important) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 191, 165, 0.1);
|
||||
}
|
||||
:is(.admonish-tip, .admonish-hint, .admonish-important) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00bfa5;
|
||||
mask-image: var(--md-admonition-icon--admonish-tip);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-tip);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-success, .admonish-check, .admonish-done) {
|
||||
border-color: #00c853;
|
||||
}
|
||||
|
||||
:is(.admonish-success, .admonish-check, .admonish-done) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 200, 83, 0.1);
|
||||
}
|
||||
:is(.admonish-success, .admonish-check, .admonish-done) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00c853;
|
||||
mask-image: var(--md-admonition-icon--admonish-success);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-success);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-question, .admonish-help, .admonish-faq) {
|
||||
border-color: #64dd17;
|
||||
}
|
||||
|
||||
:is(.admonish-question, .admonish-help, .admonish-faq) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(100, 221, 23, 0.1);
|
||||
}
|
||||
:is(.admonish-question, .admonish-help, .admonish-faq) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #64dd17;
|
||||
mask-image: var(--md-admonition-icon--admonish-question);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-question);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-warning, .admonish-caution, .admonish-attention) {
|
||||
border-color: #ff9100;
|
||||
}
|
||||
|
||||
:is(.admonish-warning, .admonish-caution, .admonish-attention) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(255, 145, 0, 0.1);
|
||||
}
|
||||
:is(.admonish-warning, .admonish-caution, .admonish-attention) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #ff9100;
|
||||
mask-image: var(--md-admonition-icon--admonish-warning);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-warning);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-failure, .admonish-fail, .admonish-missing) {
|
||||
border-color: #ff5252;
|
||||
}
|
||||
|
||||
:is(.admonish-failure, .admonish-fail, .admonish-missing) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(255, 82, 82, 0.1);
|
||||
}
|
||||
:is(.admonish-failure, .admonish-fail, .admonish-missing) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #ff5252;
|
||||
mask-image: var(--md-admonition-icon--admonish-failure);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-failure);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-danger, .admonish-error) {
|
||||
border-color: #ff1744;
|
||||
}
|
||||
|
||||
:is(.admonish-danger, .admonish-error) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(255, 23, 68, 0.1);
|
||||
}
|
||||
:is(.admonish-danger, .admonish-error) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #ff1744;
|
||||
mask-image: var(--md-admonition-icon--admonish-danger);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-danger);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-bug) {
|
||||
border-color: #f50057;
|
||||
}
|
||||
|
||||
:is(.admonish-bug) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(245, 0, 87, 0.1);
|
||||
}
|
||||
:is(.admonish-bug) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #f50057;
|
||||
mask-image: var(--md-admonition-icon--admonish-bug);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-bug);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-example) {
|
||||
border-color: #7c4dff;
|
||||
}
|
||||
|
||||
:is(.admonish-example) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(124, 77, 255, 0.1);
|
||||
}
|
||||
:is(.admonish-example) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #7c4dff;
|
||||
mask-image: var(--md-admonition-icon--admonish-example);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-example);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-quote, .admonish-cite) {
|
||||
border-color: #9e9e9e;
|
||||
}
|
||||
|
||||
:is(.admonish-quote, .admonish-cite) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(158, 158, 158, 0.1);
|
||||
}
|
||||
:is(.admonish-quote, .admonish-cite) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #9e9e9e;
|
||||
mask-image: var(--md-admonition-icon--admonish-quote);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-quote);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.navy :is(.admonition) {
|
||||
background-color: var(--sidebar-bg);
|
||||
}
|
||||
|
||||
.ayu :is(.admonition),
|
||||
.coal :is(.admonition) {
|
||||
background-color: var(--theme-hover);
|
||||
}
|
||||
|
||||
.rust :is(.admonition) {
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
.rust .admonition-anchor-link:link, .rust .admonition-anchor-link:visited {
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
1
docs/book/mermaid-init.js
Normal file
1
docs/book/mermaid-init.js
Normal file
@@ -0,0 +1 @@
|
||||
mermaid.initialize({startOnLoad:true});
|
||||
4
docs/book/mermaid.min.js
vendored
Normal file
4
docs/book/mermaid.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
docs/book/project/ch02_getting_started/Cargo.toml
Normal file
7
docs/book/project/ch02_getting_started/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "ch02_getting_started"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../../../leptos" }
|
||||
14
docs/book/project/ch02_getting_started/index.html
Normal file
14
docs/book/project/ch02_getting_started/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Leptos • Todos</title>
|
||||
|
||||
<!-- This custom link tag with `data-trunk` tells Trunk to insert code here to load our Rust/Wasm code -->
|
||||
<!-- `data-wasm-opt=z` tells the compiler to optimize for binary size in a release build -->
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" />
|
||||
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
5
docs/book/project/ch02_getting_started/src/main.rs
Normal file
5
docs/book/project/ch02_getting_started/src/main.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
mount_to_body(|_cx| view! { cx, <p>"Hello, world!"</p> })
|
||||
}
|
||||
7
docs/book/project/ch03_building_ui/Cargo.toml
Normal file
7
docs/book/project/ch03_building_ui/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "ch03_building_ui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../../../leptos" }
|
||||
14
docs/book/project/ch03_building_ui/index.html
Normal file
14
docs/book/project/ch03_building_ui/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Leptos • Todos</title>
|
||||
|
||||
<!-- This custom link tag with `data-trunk` tells Trunk to insert code here to load our Rust/Wasm code -->
|
||||
<!-- `data-wasm-opt=z` tells the compiler to optimize for binary size in a release build -->
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" />
|
||||
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
39
docs/book/project/ch03_building_ui/src/main.rs
Normal file
39
docs/book/project/ch03_building_ui/src/main.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
mount_to_body(|cx| {
|
||||
let name = "gbj";
|
||||
let userid = 0;
|
||||
let _input_element = NodeRef::new(cx);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<main>
|
||||
<h1>"My Tasks"</h1> // text nodes are wrapped in quotation marks
|
||||
<h2>"by " {name}</h2>
|
||||
<input
|
||||
type="text" // attributes work just like they do in HTML
|
||||
name="new-todo"
|
||||
prop:value="todo" // `prop:` lets you set a property on a DOM node
|
||||
value="initial" // side note: the DOM `value` attribute only sets *initial* value
|
||||
// this is very important when working with forms!
|
||||
_ref=_input_element // `_ref` stores tis element in a variable
|
||||
/>
|
||||
<ul data-user=userid> // attributes can take expressions as values
|
||||
<li class="todo my-todo" // here we set the `class` attribute
|
||||
class:completed=true // `class:` also lets you toggle individual classes
|
||||
on:click=|_| todo!() // `on:` adds an event listener
|
||||
>
|
||||
"Buy milk."
|
||||
</li>
|
||||
<li class="todo my-todo" class:completed=false>
|
||||
"???"
|
||||
</li>
|
||||
<li class="todo my-todo" class:completed=false>
|
||||
"Profit!!!"
|
||||
</li>
|
||||
</ul>
|
||||
</main>
|
||||
}
|
||||
})
|
||||
}
|
||||
7
docs/book/project/ch04_reactivity/Cargo.toml
Normal file
7
docs/book/project/ch04_reactivity/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "ch04_reactivity"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../../../leptos" }
|
||||
28
docs/book/project/ch04_reactivity/src/main.rs
Normal file
28
docs/book/project/ch04_reactivity/src/main.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
run_scope(create_runtime(), |cx| {
|
||||
// signal
|
||||
let (count, set_count) = create_signal(cx, 1);
|
||||
|
||||
// derived signal
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
// memo
|
||||
let memoized_square = create_memo(cx, move |_| count() * count());
|
||||
|
||||
// effect
|
||||
create_effect(cx, move |_| {
|
||||
println!(
|
||||
"count =\t\t{} \ndouble_count = \t{}, \nsquare = \t{}",
|
||||
count(),
|
||||
double_count(),
|
||||
memoized_square()
|
||||
);
|
||||
});
|
||||
|
||||
set_count(1);
|
||||
set_count(2);
|
||||
set_count(3);
|
||||
});
|
||||
}
|
||||
@@ -1,2 +1,11 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/">
|
||||
<link rel="canonical" href="https://book.leptos.dev/">
|
||||
# Introduction
|
||||
|
||||
This book is intended as an introduction to the [Leptos](https://github.com/gbj/leptos) Web framework. Together, we’ll build a simple todo app—first as a client-side app, then as a full-stack app.
|
||||
|
||||
The guide doesn’t assume you know anything about fine-grained reactivity or the details of modern Web frameworks. It does assume you are familiar with the Rust programming language, HTML, CSS, and the DOM and other Web APIs.
|
||||
|
||||
Leptos is most similar to frameworks like [Solid](https://www.solidjs.com) (JavaScript) and [Sycamore](https://sycamore-rs.netlify.app/) (Rust). There are some similarities to other frameworks like React (JavaScript), Yew (Rust), and Dioxus (Rust), so knowledge of one of those frameworks may also make it easier to understand Leptos.
|
||||
|
||||
You can find more detailed docs for each part of the API at [Docs.rs](https://docs.rs/leptos/latest/leptos/).
|
||||
|
||||
**The guide is a work in progress.**
|
||||
|
||||
37
docs/book/src/02_getting_started.md
Normal file
37
docs/book/src/02_getting_started.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Getting Started
|
||||
|
||||
> The code for this chapter can be found [here](https://github.com/gbj/leptos/tree/main/docs/book/project/ch02_getting_started).
|
||||
|
||||
The easiest way to get started using Leptos is to use [Trunk](https://trunkrs.dev/), as many of our [examples](https://github.com/gbj/leptos/tree/main/examples) do. (Trunk is a simple build tool that includes a dev server.)
|
||||
|
||||
If you don’t already have it installed, you can install Trunk by running
|
||||
|
||||
```bash
|
||||
cargo install --lock trunk
|
||||
```
|
||||
|
||||
Create a basic Rust binary project
|
||||
|
||||
```bash
|
||||
cargo init leptos-todo
|
||||
```
|
||||
|
||||
Add `leptos` as a dependency to your `Cargo.toml` with the `csr` featured enabled. (That stands for “client-side rendering.” We’ll talk more about Leptos’s support for server-side rendering and hydration later.)
|
||||
|
||||
```toml
|
||||
leptos = "0.0"
|
||||
```
|
||||
|
||||
You’ll want to set up a basic `index.html` with the following content:
|
||||
|
||||
```html
|
||||
{{#include ../project/ch02_getting_started/index.html}}
|
||||
```
|
||||
|
||||
Let’s start with a very simple `main.rs`
|
||||
|
||||
```rust
|
||||
{{#include ../project/ch02_getting_started/src/main.rs}}
|
||||
```
|
||||
|
||||
Now run `trunk serve --open`. Trunk should automatically compile your app and open it in your default browser. If you make edits to `main.rs`, Trunk will recompile your source code and live-reload the page.
|
||||
49
docs/book/src/03_building_ui.md
Normal file
49
docs/book/src/03_building_ui.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Templating: Building User Interfaces
|
||||
|
||||
> The code for this chapter can be found [here](https://github.com/gbj/leptos/tree/main/docs/book/project/ch03_building_ui).
|
||||
|
||||
## RSX and the `view!` macro
|
||||
|
||||
Okay, that “Hello, world!” was a little boring. We’re going to be building a todo app, so let’s look at something a little more complicated.
|
||||
|
||||
As you noticed in the first example, Leptos lets you describe your user interface with a declarative `view!` macro. It looks something like this:
|
||||
|
||||
```
|
||||
view! {
|
||||
cx, // this is the "reactive scope": more on that in the next chapter
|
||||
<p>"..."</p> // this is some HTML-ish stuff
|
||||
}
|
||||
```
|
||||
|
||||
The “HTML-ish stuff” is what we call “RSX”: XML in Rust. (You may recognize the similarity to JSX, which is the mixed JavaScript/XML syntax used by frameworks like React.)
|
||||
|
||||
Here’s a more in-depth example:
|
||||
|
||||
```rust
|
||||
{{#include ../project/ch03_building_ui/src/main.rs}}
|
||||
```
|
||||
|
||||
You’ll probably notice a few things right away:
|
||||
|
||||
1. Elements without children need to be explicit closed with a `/` (`<input/>`, not `<input>`)
|
||||
2. Text nodes are formatted as strings, i.e., wrapped in quotation marks (`"My Tasks"`)
|
||||
3. Dynamic blocks can be inserted as children of elements, if wrapped in curly braces (`<h2>"by " {name}</h2>`)
|
||||
4. Attributes can be given Rust expressions as values. This could be a string literal as in HTML (`<input type="text" .../>)` or a variable or block (`data-user=userid` or `on:click=move |_| { ... }`)
|
||||
5. Unlike in HTML, whitespace is ignored and should be manually added (it’s `<h2>"by " {name}</h2>`, not `<h2>"by" {name}</h2>`; the space between `"by"` and `{name}` is ignored.)
|
||||
6. Normal attributes work exactly like you'd think they would.
|
||||
7. There are also special, prefixed attributes.
|
||||
|
||||
- `class:` lets you make targeted updates to a single class
|
||||
- `on:` lets you add an event listener
|
||||
- `prop:` lets you set a property on a DOM element
|
||||
- `_ref` stores the DOM element you’re creating in a variable
|
||||
|
||||
> You can find more information in the [reference docs for the `view!` macro](https://docs.rs/leptos/0.0.15/leptos/macro.view.html).
|
||||
|
||||
## But, wait...
|
||||
|
||||
This example shows some parts of the Leptos templating syntax. But it’s completely static.
|
||||
|
||||
How do you actually make the user interface interactive?
|
||||
|
||||
In the next chapter, we’ll talk about “fine-grained reactivity,” which is the core of the Leptos framework.
|
||||
240
docs/book/src/04_reactivity.md
Normal file
240
docs/book/src/04_reactivity.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# Reactivity
|
||||
|
||||
## What is reactivity?
|
||||
|
||||
A few months ago, I completely baffled a friend by trying to explain what I was working on. “You have two variables, right? Call them `a` and `b`. And then you have a third variable, `c`. And when you update `a` or `b`, the value of `c` just _automatically changes_. And it changes _on the screen_! Automatically!”
|
||||
|
||||
“Isn’t that just... how computers work?” she asked me, puzzled. If your programming experience is limited to something like spreadsheets, it’s a reasonable enough assumption. This is, after all, how math works.
|
||||
|
||||
But you know this isn't how ordinary imperative programming works.
|
||||
|
||||
```rust,should_panic
|
||||
let mut a = 0;
|
||||
let mut b = 0;
|
||||
let c = a + b;
|
||||
|
||||
assert_eq!(c, 0); // sanity check
|
||||
|
||||
a = 2;
|
||||
b = 2;
|
||||
|
||||
// now c = 4, right?
|
||||
assert_eq!(c, 4); // nope. we all know this is wrong!
|
||||
```
|
||||
|
||||
But that’s _exactly_ how reactive programming works.
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
|
||||
run_scope(create_runtime(), |cx| {
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (b, set_b) = create_signal(cx, 0);
|
||||
let c = move || a() + b();
|
||||
|
||||
assert_eq!(c(), 0); // yep, still true
|
||||
|
||||
set_a(2);
|
||||
set_b(2);
|
||||
|
||||
assert_eq!(c(), 4); // ohhhhh yeah.
|
||||
});
|
||||
```
|
||||
|
||||
Hopefully, this makes some intuitive sense. After all, `c` is a closure. Calling it again causes it to access its values a second time. This isn’t _that_ cool.
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
|
||||
run_scope(create_runtime(), |cx| {
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (b, set_b) = create_signal(cx, 0);
|
||||
let c = move || a() + b();
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
println!("c = {}", c()); // prints "c = 0"
|
||||
});
|
||||
|
||||
set_a(2); // prints "c = 2"
|
||||
set_b(2); // prints "c = 4"
|
||||
});
|
||||
```
|
||||
|
||||
This example’s a little different. [`create_effect`](https://docs.rs/leptos/latest/leptos/fn.create_effect.html) defines a “side effect,” a bridge between the reactive system of signals and the outside world. Effects synchronize the reactive system with everything else: the console, the filesystem, an HTTP request, whatever.
|
||||
|
||||
Because the closure `c` is called within the effect and in turns calls the signals `a` and `b`, the effect automatically subscribes to the signals `a` and `b`. This means that whenever `a` or `b` is updated, the effect will re-run, logging the value again.
|
||||
|
||||
You can picture the reactive graph for this system like this:
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
A-->C;
|
||||
B-->C;
|
||||
C-->Effect;
|
||||
```
|
||||
|
||||
This is the foundation on which _everything_ else is built.
|
||||
|
||||
## Reactive Primitives
|
||||
|
||||
### Overview
|
||||
|
||||
The reactive system is built on the interaction between these two halves: **signals** and **effects**. When a signal is called inside an effect, the effect automatically subscribes to the signal. When a signal’s value is updated, it automatically notifies all its subscribers, and they re-run.
|
||||
|
||||
The following simple example contains most of the core reactive concepts:
|
||||
|
||||
```rust
|
||||
{{#include ../project/ch04_reactivity/src/main.rs}}
|
||||
```
|
||||
|
||||
This creates a reactive graph like this:
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
count-->double_count;
|
||||
count-->memoized_square;
|
||||
count-->effect;
|
||||
double_count-->effect;
|
||||
memoized_square-->effect;
|
||||
```
|
||||
|
||||
**Signals** are reactive values created using [`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html) or [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html).
|
||||
|
||||
**Derived Signals** computations in ordinary closures that rely on other signals. The computation re-runs whenever you access its value.
|
||||
|
||||
**Memos** are computations that are memoized with [create_memo](https://docs.rs/leptos/latest/leptos/fn.create_memo.html). Memos only re-run when one of their signal dependencies has changed.
|
||||
|
||||
And **effects** (created with [create_effect](<(https://docs.rs/leptos/latest/leptos/fn.create_effect.html)>) synchronize the reactive system with something outside it.
|
||||
|
||||
The rest of this chapter will walk through each of these concepts in more depth.
|
||||
|
||||
### Signals
|
||||
|
||||
A **signal** is a piece of data that may change over time, and notifies other code when it has changed. This is the core primitive of Leptos’s reactive system.
|
||||
|
||||
Creating a signal is very simple. You call `create_signal`, passing in the reactive scope and the default value, and receive a tuple containing a `ReadSignal` and a `WriteSignal`.
|
||||
|
||||
```rust
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
```
|
||||
|
||||
> If you’ve used signals in Sycamore or Solid, observables in MobX or Knockout, or a similar primitive in reactive library, you probably have a pretty good idea of how signals work in Leptos. If you’re familiar with React, Yew, or Dioxus, you may recognize a similar pattern to their `use_state` hooks.
|
||||
|
||||
#### `ReadSignal<T>`
|
||||
|
||||
The [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) half of this tuple allows you to get the current value of the signal. Reading that value in a reactive context automatically subscribes to any further changes. You can access the value by simply calling the `ReadSignal` as a function.
|
||||
|
||||
```rust
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
// calling value() with return the current value of the signal,
|
||||
// and automatically track changes if you're in a reactive context
|
||||
assert_eq!(value(), 0);
|
||||
```
|
||||
|
||||
> Here, a **reactive context** means anywhere within an `Effect`. Leptos’s templating system is built on top of its reactive system, so if you’re reading the signal’s value within the template, the template will automatically subscribe to the signal and update exactly the value that needs to change in the DOM.
|
||||
|
||||
Calling a `ReadSignal` clones the value it contains. If that’s too expensive, use [`ReadSignal::with()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#method.with) to borrow the value and do whatever you need.
|
||||
|
||||
```rust
|
||||
struct MySuperExpensiveStruct {
|
||||
a: String,
|
||||
b: StructThatsSuperExpensiveToClone
|
||||
}
|
||||
let (value, set_value) = create_signal(cx, MySuperExpensiveStruct::default());
|
||||
|
||||
// ❌ this is going to clone the `StructThatsSuperExpensiveToClone` unnecessarily!
|
||||
let lowercased = move || value().a.to_lowercase();
|
||||
// ✅ only use what we need
|
||||
let lowercased = move || value.with(|value: &MySuperExpensiveStruct| value.a.to_lowercase());
|
||||
```
|
||||
|
||||
#### `WriteSignal<T>`
|
||||
|
||||
The [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) half of this tuple allows you to update the value of the signal, which will automatically notify anything that’s listening to the value that something has changed. If you simply call the `WriteSignal` as a function, its value will be set to the argument you pass. If you want to mutate the value in place instead of replacing it, you can call [`WriteSignal::update`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#method.update) instead.
|
||||
|
||||
```rust
|
||||
// often you just want to replace the value
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
set_value(1);
|
||||
assert_eq!(value(), 1);
|
||||
|
||||
// sometimes you want to mutate something in place, like a Vec. Just call update()
|
||||
let (items, set_items) = create_signal(cx, vec![0]);
|
||||
set_items.update(|items: &mut Vec<i32>| items.push(1));
|
||||
assert_eq!(items(), vec![1]);
|
||||
```
|
||||
|
||||
> Under the hood, `set_value(1)` is just syntactic sugar for `set_value.update(|n| *n = 1)`.
|
||||
|
||||
#### `RwSignal<T>`
|
||||
|
||||
This kind of “read-write segregation,” in which the getter and the setter are stored in separate variables, may be familiar from the tuple-based ”hooks” pattern in libraries like React, Solid, Yew, or Dioxus. It encourages clear contracts between components. For example, if a child component only needs to be able to read a signal, but shouldn’t be able to update it (and therefore trigger changes in other parts of the application), you can pass it only the `ReadSignal`.
|
||||
|
||||
Sometimes, however, you may prefer to keep the getter and setter combined in one variable. For example, it’s awkward and repetitive to store both halves of a signal in another data structure:
|
||||
|
||||
```rust
|
||||
# use leptos::*;
|
||||
|
||||
// pretty repetitive
|
||||
struct AppState {
|
||||
count: ReadSignal<i32>,
|
||||
set_count: WriteSignal<i32>,
|
||||
name: ReadSignal<String>,
|
||||
set_name: WriteSignal<String>
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (name, set_name) = create_signal(cx, "Alice".to_string());
|
||||
provide_context(cx, AppState {
|
||||
count,
|
||||
set_count,
|
||||
name,
|
||||
set_name
|
||||
})
|
||||
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
Or maybe you just like to keep your getters and setters in one place.
|
||||
|
||||
In this case, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) and the [`RwSignal`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html) type. This returns a **R**ead-**w**rite Signal, which has the same [`get`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.get), [`with`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.with), [`set`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.set), and [`update`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.update) functions as the `ReadSignal` and `WriteSignal` halves.
|
||||
|
||||
```rust
|
||||
# use leptos::*;
|
||||
|
||||
// better
|
||||
struct AppState {
|
||||
count: RwSignal<i32>,
|
||||
name: RwSignal<String>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) {
|
||||
let count = create_rw_signal(cx, 0);
|
||||
let name = create_rw_signal(cx, "Alice".to_string());
|
||||
provide_context(cx, AppState {
|
||||
count,
|
||||
name,
|
||||
})
|
||||
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
If you still want to hand off read-only access to another part of the app, you can get a `ReadSignal` with [`RwSignal::read_only()`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.get).
|
||||
|
||||
### Derived Signals
|
||||
|
||||
(todo)
|
||||
|
||||
### Memos
|
||||
|
||||
(todo)
|
||||
|
||||
### Effects
|
||||
|
||||
(todo)
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/15_global_state.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/15_global_state.html">
|
||||
@@ -1,56 +1,6 @@
|
||||
# Summary
|
||||
|
||||
- [Introduction](./01_introduction.md)
|
||||
- [Getting Started](./getting_started/README.md)
|
||||
- [Leptos DX](./getting_started/leptos_dx.md)
|
||||
- [The Leptos Community and leptos-* Crates](./getting_started/community_crates.md)
|
||||
- [Part 1: Building User Interfaces](./view/README.md)
|
||||
- [A Basic Component](./view/01_basic_component.md)
|
||||
- [Dynamic Attributes](./view/02_dynamic_attributes.md)
|
||||
- [Components and Props](./view/03_components.md)
|
||||
- [Iteration](./view/04_iteration.md)
|
||||
- [Iterating over More Complex Data](./view/04b_iteration.md)
|
||||
- [Forms and Inputs](./view/05_forms.md)
|
||||
- [Control Flow](./view/06_control_flow.md)
|
||||
- [Error Handling](./view/07_errors.md)
|
||||
- [Parent-Child Communication](./view/08_parent_child.md)
|
||||
- [Passing Children to Components](./view/09_component_children.md)
|
||||
- [No Macros: The View Builder Syntax](./view/builder.md)
|
||||
- [Reactivity](./reactivity/README.md)
|
||||
- [Working with Signals](./reactivity/working_with_signals.md)
|
||||
- [Responding to Changes with `create_effect`](./reactivity/14_create_effect.md)
|
||||
- [Interlude: Reactivity and Functions](./reactivity/interlude_functions.md)
|
||||
- [Testing](./testing.md)
|
||||
- [Async](./async/README.md)
|
||||
- [Loading Data with Resources](./async/10_resources.md)
|
||||
- [Suspense](./async/11_suspense.md)
|
||||
- [Transition](./async/12_transition.md)
|
||||
- [Actions](./async/13_actions.md)
|
||||
- [Interlude: Projecting Children](./interlude_projecting_children.md)
|
||||
- [Global State Management](./15_global_state.md)
|
||||
- [Router](./router/README.md)
|
||||
- [Defining `<Routes/>`](./router/16_routes.md)
|
||||
- [Nested Routing](./router/17_nested_routing.md)
|
||||
- [Params and Queries](./router/18_params_and_queries.md)
|
||||
- [`<A/>`](./router/19_a.md)
|
||||
- [`<Form/>`](./router/20_form.md)
|
||||
- [Interlude: Styling](./interlude_styling.md)
|
||||
- [Metadata](./metadata.md)
|
||||
- [Client-Side Rendering: Wrapping Up](./csr_wrapping_up.md)
|
||||
- [Part 2: Server Side Rendering](./ssr/README.md)
|
||||
- [`cargo-leptos`](./ssr/21_cargo_leptos.md)
|
||||
- [The Life of a Page Load](./ssr/22_life_cycle.md)
|
||||
- [Async Rendering and SSR “Modes”](./ssr/23_ssr_modes.md)
|
||||
- [Hydration Bugs](./ssr/24_hydration_bugs.md)
|
||||
- [Working with the Server](./server/README.md)
|
||||
- [Server Functions](./server/25_server_functions.md)
|
||||
- [Extractors](./server/26_extractors.md)
|
||||
- [Responses and Redirects](./server/27_response.md)
|
||||
- [Progressive Enhancement and Graceful Degradation](./progressive_enhancement/README.md)
|
||||
- [`<ActionForm/>`s](./progressive_enhancement/action_form.md)
|
||||
- [Deployment](./deployment/README.md)
|
||||
- [Optimizing WASM Binary Size](./deployment/binary_size.md)
|
||||
- [Guide: Islands](./islands.md)
|
||||
|
||||
- [Appendix: How Does the Reactive System Work?](./appendix_reactive_graph.md)
|
||||
|
||||
- [Getting Started](./02_getting_started.md)
|
||||
- [Templating: Building User Interfaces](./03_building_ui.md)
|
||||
- [Reactivity: Making Things Interactive](./04_reactivity.md)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/appendix_reactive_graph.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/appendix_reactive_graph.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/async/10_resources.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/async/10_resources.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/async/11_suspense.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/async/11_suspense.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/async/12_transition.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/async/12_transition.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/async/13_action.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/async/13_action.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/async/index.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/async/index.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/csr_wrapping_up.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/csr_wrapping_up.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/deployment/index.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/deployment/index.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/deployment/binary_size.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/deployment/binary_size.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/getting_started/index.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/getting_started/index.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/getting_started/community_crates.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/getting_started/community_crates.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/getting_started/leptos_dx.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/getting_started/leptos_dx.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/interlude_projecting_children.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/interlude_projecting_children.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/interlude_styling.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/interlude_styling.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/islands.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/islands.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/metadata.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/metadata.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/progressive_enhancement/index.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/progressive_enhancement/index.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/progressive_enhancement/action_form.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/progressive_enhancement/action_form.html">
|
||||
@@ -1,2 +0,0 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/reactivity/14_create_effect.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/reactivity/14_create_effect.html">
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user