mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 11:21:55 -05:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50a1da08eb |
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -29,14 +29,11 @@ Steps to reproduce the behavior:
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**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.
|
||||
|
||||
32
.github/workflows/ci-changed-examples.yml
vendored
Normal file
32
.github/workflows/ci-changed-examples.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: CI Changed Examples
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- leptos_0.6
|
||||
- leptos_0.8
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- leptos_0.6
|
||||
- leptos_0.8
|
||||
jobs:
|
||||
get-example-changed:
|
||||
uses: ./.github/workflows/get-example-changed.yml
|
||||
get-matrix:
|
||||
needs: [get-example-changed]
|
||||
uses: ./.github/workflows/get-changed-examples-matrix.yml
|
||||
with:
|
||||
example_changed: ${{ fromJSON(needs.get-example-changed.outputs.example_changed) }}
|
||||
test:
|
||||
name: CI
|
||||
needs: [get-example-changed, get-matrix]
|
||||
if: needs.get-example-changed.outputs.example_changed == 'true'
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.get-matrix.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: stable
|
||||
29
.github/workflows/ci-examples.yml
vendored
Normal file
29
.github/workflows/ci-examples.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: CI Examples
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- leptos_0.6
|
||||
- leptos_0.8
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- leptos_0.6
|
||||
- leptos_0.8
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
get-examples-matrix:
|
||||
uses: ./.github/workflows/get-examples-matrix.yml
|
||||
test:
|
||||
name: CI
|
||||
needs: [get-leptos-changed, get-examples-matrix]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.get-examples-matrix.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: stable
|
||||
33
.github/workflows/ci-semver.yml
vendored
Normal file
33
.github/workflows/ci-semver.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: CI semver
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- leptos_0.6
|
||||
- leptos_0.8
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- leptos_0.6
|
||||
- leptos_0.8
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
test:
|
||||
needs: [get-leptos-changed]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
|
||||
name: Run semver check (nightly-2024-08-01)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Glib
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Semver Checks
|
||||
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
with:
|
||||
rust-toolchain: nightly-2024-08-01
|
||||
47
.github/workflows/ci.yml
vendored
47
.github/workflows/ci.yml
vendored
@@ -10,20 +10,13 @@ on:
|
||||
- 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)
|
||||
test:
|
||||
name: CI
|
||||
needs: [get-leptos-changed, get-leptos-matrix]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
strategy:
|
||||
@@ -32,37 +25,5 @@ jobs:
|
||||
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@v4
|
||||
- name: Semver Checks
|
||||
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly-2024-08-01
|
||||
|
||||
54
.github/workflows/get-changed-examples-matrix.yml
vendored
Normal file
54
.github/workflows/get-changed-examples-matrix.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: Changed Examples Matrix Call
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
example_changed:
|
||||
description: "Example Changed"
|
||||
required: true
|
||||
type: boolean
|
||||
outputs:
|
||||
matrix:
|
||||
description: "Matrix"
|
||||
value: ${{ jobs.get-example-changed.outputs.matrix }}
|
||||
|
||||
jobs:
|
||||
get-example-changed:
|
||||
name: Get Changed Example Matrix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get example project directories that changed
|
||||
id: changed-dirs
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
dir_names: true
|
||||
dir_names_max_depth: "2"
|
||||
files: |
|
||||
examples/**
|
||||
!examples/cargo-make/**
|
||||
!examples/gtk/**
|
||||
!examples/Makefile.toml
|
||||
!examples/*.md
|
||||
json: true
|
||||
quotepath: false
|
||||
|
||||
- name: List example project directories that changed
|
||||
run: echo '${{ steps.changed-dirs.outputs.all_changed_files }}'
|
||||
|
||||
- name: Set Matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
if [ ${{ inputs.example_changed }} == 'true' ]; then
|
||||
# Create matrix with changed directories
|
||||
echo "matrix={\"directory\":${{ steps.changed-dirs.outputs.all_changed_files }}}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Create matrix with one item to prevent an empty vector error
|
||||
echo "matrix={\"directory\":[\"NO_CHANGE\"]}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
12
.github/workflows/get-example-changed.yml
vendored
12
.github/workflows/get-example-changed.yml
vendored
@@ -5,18 +5,12 @@ on:
|
||||
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@v4
|
||||
@@ -24,7 +18,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Get example files that changed
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v46
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
examples/**
|
||||
@@ -33,11 +27,7 @@ jobs:
|
||||
!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"
|
||||
|
||||
2
.github/workflows/get-leptos-changed.yml
vendored
2
.github/workflows/get-leptos-changed.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Get source files that changed
|
||||
id: changed-source
|
||||
uses: tj-actions/changed-files@v46
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files_ignore: |
|
||||
.*/**/*
|
||||
|
||||
98
.github/workflows/run-cargo-make-task.yml
vendored
98
.github/workflows/run-cargo-make-task.yml
vendored
@@ -5,21 +5,20 @@ on:
|
||||
directory:
|
||||
required: true
|
||||
type: string
|
||||
cargo_make_task:
|
||||
required: true
|
||||
type: string
|
||||
toolchain:
|
||||
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' }})"
|
||||
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
toolchain: [stable, nightly-2025-03-05]
|
||||
erased_mode: [true, false]
|
||||
steps:
|
||||
- name: Free Disk Space
|
||||
run: |
|
||||
@@ -27,25 +26,11 @@ jobs:
|
||||
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/local/lib/android/sdk/ndk
|
||||
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
|
||||
sudo apt-get clean
|
||||
echo "Disk space after cleanup:"
|
||||
df -h
|
||||
# Setup environment
|
||||
@@ -57,46 +42,41 @@ jobs:
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
targets: wasm32-unknown-unknown
|
||||
components: clippy,rustfmt
|
||||
toolchain: ${{ inputs.toolchain }}
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Install binstall
|
||||
uses: cargo-bins/cargo-binstall@main
|
||||
- name: Install wasm-bindgen
|
||||
run: cargo binstall wasm-bindgen-cli --no-confirm
|
||||
- name: Install cargo-leptos
|
||||
run: cargo binstall cargo-leptos --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
|
||||
run: cargo binstall cargo-leptos --no-confirm
|
||||
- name: Install Trunk
|
||||
if: contains(inputs.directory, 'examples')
|
||||
run: cargo binstall trunk --no-confirm
|
||||
uses: jetli/trunk-action@v0.5.0
|
||||
with:
|
||||
version: "latest"
|
||||
- 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@v4
|
||||
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 }}
|
||||
@@ -104,9 +84,8 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Maybe install chromedriver
|
||||
if: contains(inputs.directory, 'examples')
|
||||
run: |
|
||||
project_makefile='${{inputs.directory}}/Makefile.toml'
|
||||
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
|
||||
@@ -120,9 +99,8 @@ jobs:
|
||||
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)
|
||||
for pw_path in $(find ${{inputs.directory}} -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
if [ ! -v $pw_dir ]; then
|
||||
@@ -134,35 +112,17 @@ jobs:
|
||||
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
|
||||
if [ ! -z $(echo ${{inputs.directory}} | grep gtk) ]; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev libgio2.0-cil-dev libgraphene-1.0-dev libcairo2-dev libpango1.0-dev libgtk-4-dev
|
||||
fi
|
||||
# Run Cargo Make Task
|
||||
- name: ${{ inputs.cargo_make_task }}
|
||||
run: |
|
||||
cd '${{ inputs.directory }}'
|
||||
cargo make --no-workspace --profile=github-actions ci
|
||||
- name: Clean up ${{ inputs.directory }}
|
||||
if: always()
|
||||
run: |
|
||||
cd '${{ inputs.directory }}'
|
||||
cargo clean || true
|
||||
rm -rf node_modules || true
|
||||
cd ${{ inputs.directory }}
|
||||
cargo make --profile=github-actions ${{ inputs.cargo_make_task }}
|
||||
|
||||
1286
Cargo.lock
generated
1286
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
53
Cargo.toml
53
Cargo.toml
@@ -40,44 +40,36 @@ members = [
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.8.0-rc3"
|
||||
version = "0.7.5"
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
convert_case = "0.8"
|
||||
throw_error = { path = "./any_error/", version = "0.3.0" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.3.0-rc3" }
|
||||
throw_error = { path = "./any_error/", version = "0.2.0" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.2.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
|
||||
either_of = { path = "./either_of/", version = "0.1.5" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.3.0" }
|
||||
itertools = "0.14.0"
|
||||
leptos = { path = "./leptos", version = "0.8.0-rc3" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.0-rc3" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.0-rc3" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-rc3" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-rc3" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.0-rc3" }
|
||||
leptos_router = { path = "./router", version = "0.8.0-rc3" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.0-rc3" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.0-rc3" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.0-rc3" }
|
||||
either_of = { path = "./either_of/", version = "0.1.0" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0" }
|
||||
leptos = { path = "./leptos", version = "0.7.5" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.5" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.5" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.5" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.5" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.5" }
|
||||
leptos_router = { path = "./router", version = "0.7.5" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.5" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.5" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.5" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0" }
|
||||
oco_ref = { path = "./oco", version = "0.2.0" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.0-rc3" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.2.0-rc3" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0-rc3" }
|
||||
rustversion = "1"
|
||||
serde_json = "1.0.0"
|
||||
server_fn = { path = "./server_fn", version = "0.8.0-rc3" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-rc3" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-rc3" }
|
||||
tachys = { path = "./tachys", version = "0.2.0-rc3" }
|
||||
trybuild = "1"
|
||||
typed-builder = "0.21.0"
|
||||
thiserror = "2.0.12"
|
||||
wasm-bindgen = "0.2.100"
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.5" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.3" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.5" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.5" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.5" }
|
||||
tachys = { path = "./tachys", version = "0.1.5" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
@@ -86,7 +78,6 @@ 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 = [
|
||||
|
||||
@@ -10,8 +10,8 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
workspace = false
|
||||
clear = true
|
||||
dependencies = [
|
||||
{ name = "lint", path = "examples/counter_without_macros" },
|
||||
{ name = "lint", path = "examples/counters_stable" },
|
||||
{ name = "check", path = "examples/counter_without_macros" },
|
||||
{ name = "check", path = "examples/counters_stable" },
|
||||
]
|
||||
|
||||
[tasks.ci-examples]
|
||||
|
||||
@@ -21,7 +21,7 @@ use leptos::*;
|
||||
#[component]
|
||||
pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
|
||||
// create a reactive signal with the initial value
|
||||
let (value, set_value) = signal(initial_value);
|
||||
let (value, set_value) = create_signal(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
|
||||
@@ -46,7 +46,7 @@ pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
|
||||
pub fn SimpleCounterWithBuilder(initial_value: i32) -> impl IntoView {
|
||||
use leptos::html::*;
|
||||
|
||||
let (value, set_value) = signal(initial_value);
|
||||
let (value, set_value) = create_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);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "throw_error"
|
||||
version = "0.3.0"
|
||||
version = "0.2.0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
error,
|
||||
fmt::{self, Display},
|
||||
future::Future,
|
||||
ops,
|
||||
mem, ops,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
@@ -17,6 +17,11 @@ use std::{
|
||||
|
||||
/* Wrapper Types */
|
||||
|
||||
/// This is a result type into which any error can be converted.
|
||||
///
|
||||
/// Results are stored as [`Error`].
|
||||
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
||||
|
||||
/// A generic wrapper for any error.
|
||||
#[derive(Debug, Clone)]
|
||||
#[repr(transparent)]
|
||||
@@ -104,7 +109,7 @@ pub fn get_error_hook() -> Option<Arc<dyn ErrorHook>> {
|
||||
/// 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)),
|
||||
ERROR_HOOK.with_borrow_mut(|this| mem::replace(this, Some(hook))),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "any_spawner"
|
||||
version = "0.3.0-rc3"
|
||||
version = "0.2.1"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -12,22 +12,12 @@ edition.workspace = true
|
||||
async-executor = { version = "1.13.1", optional = true }
|
||||
futures = "0.3.31"
|
||||
glib = { version = "0.20.6", optional = true }
|
||||
thiserror = { workspace = true }
|
||||
thiserror = "2.0"
|
||||
tokio = { version = "1.41", optional = true, default-features = false, features = [
|
||||
"rt",
|
||||
] }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.50", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
futures-lite = { version = "2.6.0", default-features = false }
|
||||
tokio = { version = "1.41", default-features = false, features = [
|
||||
"rt",
|
||||
"macros",
|
||||
"time",
|
||||
] }
|
||||
wasm-bindgen-test = { version = "0.3.50" }
|
||||
serial_test = "3.2.0"
|
||||
wasm-bindgen-futures = { version = "0.4.47", optional = true }
|
||||
|
||||
[features]
|
||||
async-executor = ["dep:async-executor"]
|
||||
@@ -44,4 +34,3 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing"]
|
||||
max_combination_size = 2
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
]
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
|
||||
@@ -11,16 +11,18 @@
|
||||
//! - no "join handle" or other result is returned from the spawn
|
||||
//! - the `Future` must output `()`
|
||||
//!
|
||||
//! ```no_run
|
||||
//! ```rust
|
||||
//! use any_spawner::Executor;
|
||||
//!
|
||||
//! // make sure an Executor has been initialized with one of the init_ functions
|
||||
//!
|
||||
//! # if false {
|
||||
//! // spawn a thread-safe Future
|
||||
//! Executor::spawn(async { /* ... */ });
|
||||
//!
|
||||
//! // spawn a Future that is !Send
|
||||
//! Executor::spawn_local(async { /* ... */ });
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -35,67 +37,15 @@ 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."
|
||||
);
|
||||
}
|
||||
static SPAWN: OnceLock<fn(PinnedFuture<()>)> = OnceLock::new();
|
||||
static SPAWN_LOCAL: OnceLock<fn(PinnedLocalFuture<()>)> = OnceLock::new();
|
||||
static POLL_LOCAL: OnceLock<fn()> = OnceLock::new();
|
||||
|
||||
/// 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.")]
|
||||
#[error("Executor has already been set.")]
|
||||
AlreadySet,
|
||||
}
|
||||
|
||||
@@ -104,143 +54,150 @@ 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)]
|
||||
/// ```rust
|
||||
/// use any_spawner::Executor;
|
||||
/// # if false {
|
||||
/// // spawn a thread-safe Future
|
||||
/// Executor::spawn(async { /* ... */ });
|
||||
/// # }
|
||||
/// ```
|
||||
#[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)
|
||||
if let Some(spawner) = SPAWN.get() {
|
||||
spawner(Box::pin(fut))
|
||||
} else {
|
||||
// No global executor set.
|
||||
handle_uninitialized_spawn(pinned_fut);
|
||||
#[cfg(all(debug_assertions, feature = "tracing"))]
|
||||
tracing::error!(
|
||||
"At {}, tried to spawn a Future with Executor::spawn() before \
|
||||
the Executor had been set.",
|
||||
std::panic::Location::caller()
|
||||
);
|
||||
#[cfg(all(debug_assertions, not(feature = "tracing")))]
|
||||
panic!(
|
||||
"At {}, tried to spawn a Future with Executor::spawn() before \
|
||||
the Executor had been set.",
|
||||
std::panic::Location::caller()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns a [`Future`] that cannot be sent across threads.
|
||||
/// ```rust
|
||||
/// use any_spawner::Executor;
|
||||
///
|
||||
/// Uses the globally configured executor.
|
||||
/// Panics if no global executor has been initialized.
|
||||
#[inline(always)]
|
||||
/// # if false {
|
||||
/// // spawn a thread-safe Future
|
||||
/// Executor::spawn_local(async { /* ... */ });
|
||||
/// # }
|
||||
/// ```
|
||||
#[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)
|
||||
if let Some(spawner) = SPAWN_LOCAL.get() {
|
||||
spawner(Box::pin(fut))
|
||||
} else {
|
||||
// No global executor set.
|
||||
handle_uninitialized_spawn_local(pinned_fut);
|
||||
#[cfg(all(debug_assertions, feature = "tracing"))]
|
||||
tracing::error!(
|
||||
"At {}, tried to spawn a Future with Executor::spawn_local() \
|
||||
before the Executor had been set.",
|
||||
std::panic::Location::caller()
|
||||
);
|
||||
#[cfg(all(debug_assertions, not(feature = "tracing")))]
|
||||
panic!(
|
||||
"At {}, tried to spawn a Future with Executor::spawn_local() \
|
||||
before the Executor had been set.",
|
||||
std::panic::Location::caller()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
/// Polls the current async executor.
|
||||
/// Not all async executors support polling, so this function may not do anything.
|
||||
pub fn poll_local() {
|
||||
if let Some(fns) = EXECUTOR_FNS.get() {
|
||||
(fns.poll_local)()
|
||||
if let Some(poller) = POLL_LOCAL.get() {
|
||||
poller()
|
||||
}
|
||||
// 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.
|
||||
/// Returns `Err(_)` if an 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| {
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
tokio::spawn(fut);
|
||||
},
|
||||
spawn_local: |fut| {
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|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)
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Globally sets the [`wasm-bindgen-futures`] runtime as the executor used to spawn tasks.
|
||||
///
|
||||
/// Returns `Err(_)` if a global executor has already been set.
|
||||
/// Returns `Err(_)` if an 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| {
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
wasm_bindgen_futures::spawn_local(fut);
|
||||
},
|
||||
poll_local: no_op_poll,
|
||||
};
|
||||
EXECUTOR_FNS
|
||||
.set(executor_impl)
|
||||
.map_err(|_| ExecutorError::AlreadySet)
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| {
|
||||
wasm_bindgen_futures::spawn_local(fut);
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Globally sets the [`glib`] runtime as the executor used to spawn tasks.
|
||||
///
|
||||
/// Returns `Err(_)` if a global executor has already been set.
|
||||
/// Returns `Err(_)` if an 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| {
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
let main_context = glib::MainContext::default();
|
||||
main_context.spawn(fut);
|
||||
},
|
||||
spawn_local: |fut| {
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|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)
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Returns `Err(_)` if an executor has already been set.
|
||||
///
|
||||
/// Requires the `futures-executor` feature to be activated on this crate.
|
||||
#[cfg(feature = "futures-executor")]
|
||||
@@ -252,11 +209,9 @@ impl Executor {
|
||||
};
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -267,248 +222,140 @@ impl Executor {
|
||||
})
|
||||
}
|
||||
|
||||
let executor_impl = ExecutorFns {
|
||||
spawn: |fut| {
|
||||
SPAWN
|
||||
.set(|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
|
||||
.expect("failed to spawn future");
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| {
|
||||
SPAWNER.with(|spawner| {
|
||||
spawner
|
||||
.spawn_local(fut)
|
||||
.expect("failed to spawn local future");
|
||||
spawner.spawn_local(fut).expect("failed to spawn future");
|
||||
});
|
||||
},
|
||||
poll_local: || {
|
||||
// Use the thread_local LOCAL_POOL
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
POLL_LOCAL
|
||||
.set(|| {
|
||||
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.
|
||||
// If we couldn't borrow_mut, we're in a nested call to poll, so we don't need to do anything.
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
EXECUTOR_FNS
|
||||
.set(executor_impl)
|
||||
.map_err(|_| ExecutorError::AlreadySet)
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Returns `Err(_)` if an 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};
|
||||
use async_executor::{Executor, LocalExecutor};
|
||||
|
||||
// Keep the lazy-init global Executor and thread-local LocalExecutor for spawn_local impl
|
||||
static ASYNC_EXECUTOR: OnceLock<AsyncExecutor<'static>> =
|
||||
OnceLock::new();
|
||||
static THREAD_POOL: OnceLock<Executor> = OnceLock::new();
|
||||
thread_local! {
|
||||
static LOCAL_EXECUTOR_POOL: LocalExecutor<'static> = const { LocalExecutor::new() };
|
||||
static LOCAL_POOL: LocalExecutor<'static> = const { LocalExecutor::new() };
|
||||
}
|
||||
|
||||
fn get_async_executor() -> &'static AsyncExecutor<'static> {
|
||||
ASYNC_EXECUTOR.get_or_init(AsyncExecutor::new)
|
||||
fn get_thread_pool() -> &'static Executor<'static> {
|
||||
THREAD_POOL.get_or_init(Executor::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)
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
get_thread_pool().spawn(fut).detach();
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| {
|
||||
LOCAL_POOL.with(|pool| pool.spawn(fut).detach());
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
POLL_LOCAL
|
||||
.set(|| {
|
||||
LOCAL_POOL.with(|pool| pool.try_tick());
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Returns `Err(_)` if an 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
|
||||
static EXECUTOR: OnceLock<Box<dyn CustomExecutor + Send + Sync>> =
|
||||
OnceLock::new();
|
||||
EXECUTOR
|
||||
.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.
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
EXECUTOR.get().unwrap().spawn(fut);
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| EXECUTOR.get().unwrap().spawn_local(fut))
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
POLL_LOCAL
|
||||
.set(|| EXECUTOR.get().unwrap().poll_local())
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets a custom executor *for the current thread only*.
|
||||
/// Locally sets a custom executor as the executor used to spawn tasks
|
||||
/// in the current thread.
|
||||
///
|
||||
/// 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*.
|
||||
/// Returns `Err(_)` if an executor has already been set.
|
||||
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| {
|
||||
static EXECUTOR: OnceLock<Box<dyn CustomExecutor>> = OnceLock::new();
|
||||
}
|
||||
EXECUTOR.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)
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
EXECUTOR.with(|this| this.get().unwrap().spawn(fut));
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| {
|
||||
EXECUTOR.with(|this| this.get().unwrap().spawn_local(fut));
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
POLL_LOCAL
|
||||
.set(|| {
|
||||
EXECUTOR.with(|this| this.get().unwrap().poll_local());
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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).
|
||||
///
|
||||
/// All methods can be called recursively.
|
||||
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.
|
||||
/// Polls the executor, if it supports polling.
|
||||
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,7 +1,6 @@
|
||||
#![cfg(feature = "futures-executor")]
|
||||
|
||||
#[cfg(feature = "futures-executor")]
|
||||
use any_spawner::{CustomExecutor, Executor, PinnedFuture, PinnedLocalFuture};
|
||||
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[test]
|
||||
fn can_create_custom_executor() {
|
||||
use futures::{
|
||||
|
||||
@@ -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,8 +1,8 @@
|
||||
#![cfg(feature = "futures-executor")]
|
||||
|
||||
#[cfg(feature = "futures-executor")]
|
||||
use any_spawner::Executor;
|
||||
// All tests in this file use the same executor.
|
||||
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[test]
|
||||
fn can_spawn_local_future() {
|
||||
use std::rc::Rc;
|
||||
@@ -15,6 +15,7 @@ fn can_spawn_local_future() {
|
||||
Executor::spawn(async {});
|
||||
}
|
||||
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[test]
|
||||
fn can_make_local_progress() {
|
||||
use std::sync::{
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
@@ -23,7 +23,7 @@ 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"
|
||||
wasm-bindgen = "0.2.0"
|
||||
lazy_static = "1.0"
|
||||
log = "0.4.0"
|
||||
strum = "0.24.0"
|
||||
|
||||
7
cargo-make/check.toml
Normal file
7
cargo-make/check.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[tasks.check]
|
||||
alias = "check-all"
|
||||
|
||||
[tasks.check-all]
|
||||
command = "cargo"
|
||||
args = ["check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -6,15 +6,13 @@ env = { LEPTOS_PROJECT_DIRECTORY = "../" }
|
||||
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]
|
||||
|
||||
[tasks.clippy-each-feature]
|
||||
dependencies = ["install-clippy"]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"all-features",
|
||||
"clippy",
|
||||
"--no-deps",
|
||||
"--",
|
||||
"-D",
|
||||
"clippy::print_stdout",
|
||||
"clippy",
|
||||
"--all-features",
|
||||
"--no-deps",
|
||||
"--",
|
||||
"-D",
|
||||
"clippy::print_stdout",
|
||||
]
|
||||
install_script = '''
|
||||
cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
'''
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
extend = [{ path = "./lint.toml" }, { path = "./test.toml" }]
|
||||
extend = [
|
||||
{ path = "./check.toml" },
|
||||
{ path = "./lint.toml" },
|
||||
{ path = "./test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
RUSTFLAGS = ""
|
||||
@@ -8,4 +12,4 @@ LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
|
||||
RUSTFLAGS = "-D warnings"
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = ["lint", "test-each-feature", "doctests"]
|
||||
dependencies = ["lint", "test"]
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
[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
|
||||
'''
|
||||
[tasks.test]
|
||||
alias = "test-all"
|
||||
|
||||
# This can be removed once doctests is supported in nextest
|
||||
# https://github.com/nextest-rs/nextest/issues/16
|
||||
[tasks.doctests]
|
||||
[tasks.test-all]
|
||||
command = "cargo"
|
||||
args = ["all-features", "test", "--doc"]
|
||||
install_script = '''
|
||||
cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
'''
|
||||
args = ["test-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
[package]
|
||||
name = "either_of"
|
||||
version = "0.1.6"
|
||||
version = "0.1.5"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -776,20 +776,20 @@ tuples!(EitherOf16 + EitherOf16Future + EitherOf16FutureProj {
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! either {
|
||||
($match:expr, $left_pattern:pat => $left_expression:expr, $right_pattern:pat => $right_expression:expr$(,)?) => {
|
||||
($match:expr, $left_pattern:pat => $left_expression:expr, $right_pattern:pat => $right_expression:expr,) => {
|
||||
match $match {
|
||||
$left_pattern => $crate::Either::Left($left_expression),
|
||||
$right_pattern => $crate::Either::Right($right_expression),
|
||||
}
|
||||
};
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr$(,)?) => {
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr,) => {
|
||||
match $match {
|
||||
$a_pattern => $crate::EitherOf3::A($a_expression),
|
||||
$b_pattern => $crate::EitherOf3::B($b_expression),
|
||||
$c_pattern => $crate::EitherOf3::C($c_expression),
|
||||
}
|
||||
};
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr$(,)?) => {
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr,) => {
|
||||
match $match {
|
||||
$a_pattern => $crate::EitherOf4::A($a_expression),
|
||||
$b_pattern => $crate::EitherOf4::B($b_expression),
|
||||
@@ -797,7 +797,7 @@ macro_rules! either {
|
||||
$d_pattern => $crate::EitherOf4::D($d_expression),
|
||||
}
|
||||
};
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr$(,)?) => {
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr,) => {
|
||||
match $match {
|
||||
$a_pattern => $crate::EitherOf5::A($a_expression),
|
||||
$b_pattern => $crate::EitherOf5::B($b_expression),
|
||||
@@ -806,7 +806,7 @@ macro_rules! either {
|
||||
$e_pattern => $crate::EitherOf5::E($e_expression),
|
||||
}
|
||||
};
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr$(,)?) => {
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr,) => {
|
||||
match $match {
|
||||
$a_pattern => $crate::EitherOf6::A($a_expression),
|
||||
$b_pattern => $crate::EitherOf6::B($b_expression),
|
||||
@@ -816,7 +816,7 @@ macro_rules! either {
|
||||
$f_pattern => $crate::EitherOf6::F($f_expression),
|
||||
}
|
||||
};
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr, $g_pattern:pat => $g_expression:expr$(,)?) => {
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr, $g_pattern:pat => $g_expression:expr,) => {
|
||||
match $match {
|
||||
$a_pattern => $crate::EitherOf7::A($a_expression),
|
||||
$b_pattern => $crate::EitherOf7::B($b_expression),
|
||||
@@ -827,7 +827,7 @@ macro_rules! either {
|
||||
$g_pattern => $crate::EitherOf7::G($g_expression),
|
||||
}
|
||||
};
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr, $g_pattern:pat => $g_expression:expr, $h_pattern:pat => $h_expression:expr$(,)?) => {
|
||||
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr, $g_pattern:pat => $g_expression:expr, $h_pattern:pat => $h_expression:expr,) => {
|
||||
match $match {
|
||||
$a_pattern => $crate::EitherOf8::A($a_expression),
|
||||
$b_pattern => $crate::EitherOf8::B($b_expression),
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1.0"
|
||||
gloo-utils = "0.2.0"
|
||||
@@ -20,27 +20,18 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.39", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
], optional = true }
|
||||
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "0.2.92"
|
||||
web-sys = { version = "0.3.69", features = [
|
||||
"AddEventListenerOptions",
|
||||
"Document",
|
||||
"Element",
|
||||
"Event",
|
||||
"EventListener",
|
||||
"EventTarget",
|
||||
"Performance",
|
||||
"Window",
|
||||
], optional = true }
|
||||
web-sys = { version = "0.3.69", features = [ "AddEventListenerOptions", "Document", "Element", "Event", "EventListener", "EventTarget", "Performance", "Window" ], optional = true }
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "dep:js-sys", "dep:web-sys"]
|
||||
hydrate = [
|
||||
"leptos/hydrate",
|
||||
"dep:js-sys",
|
||||
"dep:web-sys",
|
||||
]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:http-body-util",
|
||||
@@ -65,7 +56,7 @@ panic = "abort"
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
||||
skip_feature_sets = [["ssr", "hydrate"], []]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
|
||||
2
examples/axum_js_ssr/rust-toolchain.toml
Normal file
2
examples/axum_js_ssr/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "stable" # test change
|
||||
@@ -26,7 +26,8 @@ async fn main() {
|
||||
};
|
||||
use axum_js_ssr::app::*;
|
||||
use http_body_util::BodyExt;
|
||||
use leptos::{logging::log, prelude::*};
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
|
||||
latency::LATENCY.get_or_init(|| [0, 4, 40, 400].iter().cycle().into());
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
extend = [
|
||||
{ path = "./lint.toml" }
|
||||
]
|
||||
|
||||
[tasks.make-target-site-dir]
|
||||
command = "mkdir"
|
||||
args = ["-p", "target/site"]
|
||||
@@ -20,16 +24,21 @@ clear = true
|
||||
dependencies = ["check-debug", "check-release"]
|
||||
|
||||
[tasks.check-debug]
|
||||
dependencies = ["cargo-all-features"]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["all-features", "clippy"]
|
||||
args = ["check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-release]
|
||||
dependencies = ["cargo-all-features"]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["all-features", "clippy", "--release"]
|
||||
args = ["check-all-features", "--release"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.lint]
|
||||
dependencies = ["make-target-site-dir", "check-style"]
|
||||
|
||||
[tasks.start-client]
|
||||
dependencies = ["install-cargo-leptos"]
|
||||
command = "cargo"
|
||||
args = ["leptos", "watch", "--release", "-P"]
|
||||
args = ["leptos", "watch", "--release", "-P"]
|
||||
@@ -1,11 +1,5 @@
|
||||
[tasks.cargo-all-features]
|
||||
install_script = '''
|
||||
cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
'''
|
||||
|
||||
[tasks.install-cargo-leptos]
|
||||
install_crate = { crate_name = "cargo-leptos", binary = "cargo-leptos", test_arg = "--help" }
|
||||
args = ["--locked"]
|
||||
|
||||
[tasks.cargo-leptos-e2e]
|
||||
command = "cargo"
|
||||
@@ -21,14 +15,16 @@ clear = true
|
||||
dependencies = ["check-debug", "check-release"]
|
||||
|
||||
[tasks.check-debug]
|
||||
dependencies = ["cargo-all-features"]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["all-features", "clippy"]
|
||||
args = ["check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-release]
|
||||
dependencies = ["cargo-all-features"]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["all-features", "clippy", "--release"]
|
||||
args = ["check-all-features", "--release"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.start-client]
|
||||
dependencies = ["install-cargo-leptos"]
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
[tasks.cargo-all-features]
|
||||
install_script = '''
|
||||
cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
'''
|
||||
|
||||
[tasks.build]
|
||||
dependencies = ["cargo-all-features"]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["all-features", "build"]
|
||||
args = ["build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
[tasks.cargo-all-features]
|
||||
install_script = '''
|
||||
cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
'''
|
||||
|
||||
[tasks.build]
|
||||
install_crate = { crate_name = "wasm-pack", binary = "wasm-pack", test_arg = "--help" }
|
||||
clear = true
|
||||
@@ -19,11 +14,13 @@ clear = true
|
||||
dependencies = ["check-debug", "check-release"]
|
||||
|
||||
[tasks.check-debug]
|
||||
dependencies = ["cargo-all-features"]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["all-features", "clippy"]
|
||||
args = ["check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-release]
|
||||
dependencies = ["cargo-all-features"]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["all-features", "clippy", "--release"]
|
||||
args = ["check-all-features", "--release"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
[tasks.cargo-all-features]
|
||||
install_script = '''
|
||||
cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
'''
|
||||
[tasks.pre-clippy]
|
||||
env = { CARGO_MAKE_CLIPPY_ARGS = "--no-deps --all-targets --all-features -- -D warnings" }
|
||||
|
||||
[tasks.lint]
|
||||
dependencies = ["check-format-flow", "clippy-each-feature"]
|
||||
|
||||
[tasks.clippy-each-feature]
|
||||
dependencies = ["cargo-all-features"]
|
||||
command = "cargo"
|
||||
args = ["all-features", "clippy", "--no-deps", "--", "-D", "warnings"]
|
||||
[tasks.check-style]
|
||||
dependencies = ["check-format-flow", "clippy-flow"]
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
extend = [
|
||||
{ path = "./compile.toml" },
|
||||
{ path = "./clean.toml" },
|
||||
{ path = "./lint.toml" },
|
||||
{ path = "./node.toml" },
|
||||
{ path = "./process.toml" },
|
||||
{ path = "./compile.toml" },
|
||||
{ path = "./clean.toml" },
|
||||
{ path = "./lint.toml" },
|
||||
{ path = "./node.toml" },
|
||||
{ path = "./process.toml" },
|
||||
]
|
||||
|
||||
# CI Stages
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = ["prepare", "lint", "test-flow", "integration-test"]
|
||||
dependencies = ["prepare", "lint", "build", "test-flow", "integration-test"]
|
||||
|
||||
[tasks.prepare]
|
||||
dependencies = ["setup-node"]
|
||||
|
||||
[tasks.lint]
|
||||
dependencies = ["check-style"]
|
||||
|
||||
[tasks.integration-test]
|
||||
|
||||
# Support Local Runs
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/playwright.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
{ path = "../cargo-make/playwright.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
]
|
||||
|
||||
[tasks.integration-test]
|
||||
dependencies = ["build", "start-client", "test-playwright"]
|
||||
description = "Run integration test with automated start and stop of processes"
|
||||
env = { SPAWN_CLIENT_PROCESS = "1" }
|
||||
run_task = { name = ["start", "wait-test-stop"], parallel = true }
|
||||
|
||||
[tasks.wait-test-stop]
|
||||
private = true
|
||||
dependencies = ["wait-server", "test-playwright", "stop"]
|
||||
|
||||
[tasks.wait-server]
|
||||
script = '''
|
||||
for run in {1..12}; do
|
||||
echo "Waiting to ensure server is started..."
|
||||
sleep 10
|
||||
done
|
||||
echo "Times up, running tests"
|
||||
'''
|
||||
|
||||
@@ -6,6 +6,5 @@ command = "trunk"
|
||||
args = ["build"]
|
||||
|
||||
[tasks.start-client]
|
||||
script = '''
|
||||
trunk serve -q "${@}" &
|
||||
'''
|
||||
command = "trunk"
|
||||
args = ["serve", "${@}"]
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
[tasks.test]
|
||||
env = { RUN_CARGO_TEST = false }
|
||||
condition = { env_true = ["RUN_CARGO_TEST"] }
|
||||
|
||||
[tasks.post-test]
|
||||
dependencies = ["test-wasm"]
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" />
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico" />
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
channel = "stable" # test change
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use counter::*;
|
||||
use leptos::{mount::mount_to, prelude::*, task::tick};
|
||||
use leptos::mount::mount_to;
|
||||
use leptos::prelude::*;
|
||||
use leptos::task::tick;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ ssr = [
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix"]
|
||||
skip_feature_sets = [["ssr", "hydrate"], []]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
channel = "stable" # test change
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
dependencies = ["cargo-all-features"]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["all-features", "build"]
|
||||
args = ["build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
dependencies = ["cargo-all-features"]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["all-features", "clippy"]
|
||||
args = ["check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
channel = "stable" # test change
|
||||
|
||||
@@ -5,7 +5,8 @@ use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use counters::Counters;
|
||||
use leptos::{prelude::*, task::tick};
|
||||
use leptos::prelude::*;
|
||||
use leptos::task::tick;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
@@ -23,9 +24,8 @@ async fn inc() {
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: <span \
|
||||
data-testid=\"total\">0</span> from <span \
|
||||
data-testid=\"counters\">0</span> counters.</p><ul><!----></ul>"
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span data-testid=\"total\">0</span> from <span data-testid=\"counters\">0</span> counters.</p><ul><!----></ul>"
|
||||
);
|
||||
|
||||
// add 3 counters
|
||||
@@ -39,9 +39,8 @@ async fn inc() {
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: <span \
|
||||
data-testid=\"total\">0</span> from <span \
|
||||
data-testid=\"counters\">3</span> \
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span data-testid=\"total\">0</span> from <span data-testid=\"counters\">3</span> \
|
||||
counters.</p><ul><li><button>-1</button><input \
|
||||
type=\"text\"><span>0</span><button>+1</button><button>x</button></\
|
||||
li><li><button>-1</button><input \
|
||||
@@ -82,9 +81,8 @@ async fn inc() {
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: <span \
|
||||
data-testid=\"total\">6</span> from <span \
|
||||
data-testid=\"counters\">3</span> \
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span data-testid=\"total\">6</span> from <span data-testid=\"counters\">3</span> \
|
||||
counters.</p><ul><li><button>-1</button><input \
|
||||
type=\"text\"><span>1</span><button>+1</button><button>x</button></\
|
||||
li><li><button>-1</button><input \
|
||||
@@ -108,9 +106,8 @@ async fn inc() {
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: <span \
|
||||
data-testid=\"total\">5</span> from <span \
|
||||
data-testid=\"counters\">2</span> \
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span data-testid=\"total\">5</span> from <span data-testid=\"counters\">2</span> \
|
||||
counters.</p><ul><li><button>-1</button><input \
|
||||
type=\"text\"><span>2</span><button>+1</button><button>x</button></\
|
||||
li><li><button>-1</button><input \
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
channel = "stable" # test change
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
channel = "stable" # test change
|
||||
|
||||
@@ -13,7 +13,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
@@ -36,7 +36,7 @@ ssr = [
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"]
|
||||
skip_feature_sets = [["ssr", "hydrate"], []]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
channel = "stable" # test change
|
||||
|
||||
@@ -45,7 +45,7 @@ async fn main() {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/special/{id}", get(custom_handler))
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
channel = "stable" # test change
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos::tachys::html::style::style;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -15,7 +16,7 @@ pub enum CatError {
|
||||
|
||||
type CatCount = usize;
|
||||
|
||||
async fn fetch_cats(count: CatCount) -> Result<Vec<String>, Error> {
|
||||
async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
|
||||
if count > 0 {
|
||||
gloo_timers::future::TimeoutFuture::new(1000).await;
|
||||
// make the request
|
||||
@@ -41,7 +42,11 @@ async fn fetch_cats(count: CatCount) -> Result<Vec<String>, Error> {
|
||||
pub fn fetch_example() -> impl IntoView {
|
||||
let (cat_count, set_cat_count) = signal::<CatCount>(1);
|
||||
|
||||
let cats = LocalResource::new(move || fetch_cats(cat_count.get()));
|
||||
// we use new_unsync here because the reqwasm request type isn't Send
|
||||
// if we were doing SSR, then
|
||||
// 1) we'd want to use a Resource, so the data would be serialized to the client
|
||||
// 2) we'd need to make sure there was a thread-local spawner set up
|
||||
let cats = AsyncDerived::new_unsync(move || fetch_cats(cat_count.get()));
|
||||
|
||||
let fallback = move |errors: ArcRwSignal<Errors>| {
|
||||
let error_list = move || {
|
||||
@@ -61,6 +66,8 @@ pub fn fetch_example() -> impl IntoView {
|
||||
}
|
||||
};
|
||||
|
||||
let spreadable = style(("background-color", "AliceBlue"));
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<label>
|
||||
@@ -75,7 +82,7 @@ pub fn fetch_example() -> impl IntoView {
|
||||
/>
|
||||
|
||||
</label>
|
||||
<Transition fallback=|| view! { <div>"Loading..."</div> }>
|
||||
<Transition fallback=|| view! { <div>"Loading..."</div> } {..spreadable}>
|
||||
<ErrorBoundary fallback>
|
||||
<ul>
|
||||
{move || Suspend::new(async move {
|
||||
@@ -85,7 +92,7 @@ pub fn fetch_example() -> impl IntoView {
|
||||
.map(|s| {
|
||||
view! {
|
||||
<li>
|
||||
<img src=s.clone() />
|
||||
<img src=s.clone()/>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -43,7 +43,7 @@ codegen-units = 1
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"], []]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
channel = "stable" # test change
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn story(path: &str) -> String {
|
||||
format!("https://node-hnapi.herokuapp.com/{path}")
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::api;
|
||||
use leptos::{either::Either, prelude::*};
|
||||
use leptos::either::Either;
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::Meta;
|
||||
use leptos_router::{components::A, hooks::use_params_map};
|
||||
use leptos_router::components::A;
|
||||
use leptos_router::hooks::use_params_map;
|
||||
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::api::{self, User};
|
||||
use leptos::{either::Either, prelude::*, server::Resource};
|
||||
use leptos::server::Resource;
|
||||
use leptos::{either::Either, prelude::*};
|
||||
use leptos_router::hooks::use_params_map;
|
||||
|
||||
#[component]
|
||||
|
||||
@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1.40"
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
@@ -47,7 +47,7 @@ ssr = [
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "http", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"], []]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
channel = "stable" # test change
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use leptos::logging;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn story(path: &str) -> String {
|
||||
format!("https://node-hnapi.herokuapp.com/{path}")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::{routing::get, Router};
|
||||
use axum::Router;
|
||||
use hackernews_axum::{shell, App};
|
||||
use leptos::config::get_configuration;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
@@ -13,15 +13,6 @@ async fn main() {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route(
|
||||
"/favicon.ico",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "image/x-icon")],
|
||||
include_bytes!("../public/favicon.ico"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::api;
|
||||
use leptos::{either::Either, prelude::*};
|
||||
use leptos::either::Either;
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::Meta;
|
||||
use leptos_router::{components::A, hooks::use_params_map};
|
||||
use leptos_router::components::A;
|
||||
use leptos_router::hooks::use_params_map;
|
||||
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::api::{self, User};
|
||||
use leptos::{either::Either, prelude::*, server::Resource};
|
||||
use leptos::server::Resource;
|
||||
use leptos::{either::Either, prelude::*};
|
||||
use leptos_router::hooks::use_params_map;
|
||||
|
||||
#[component]
|
||||
|
||||
@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1.40"
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
axum = { version = "0.8.1", optional = true, features = ["http2"] }
|
||||
axum = { version = "0.7.5", optional = true, features = ["http2"] }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = [
|
||||
"fs",
|
||||
@@ -57,8 +57,7 @@ ssr = [
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "http", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"], []]
|
||||
max_combination_size = 2
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-compress.toml" },
|
||||
]
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = [
|
||||
"prepare",
|
||||
"make-target-site-dir",
|
||||
"lint",
|
||||
"test-flow",
|
||||
"integration-test",
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-compress.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
|
||||
CLIENT_PROCESS_NAME = "hackernews_islands"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
channel = "stable" # test change
|
||||
|
||||
@@ -3,7 +3,6 @@ use axum::{
|
||||
http::{header, Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use rust_embed::Embed;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
@@ -12,7 +11,7 @@ const DEV_MODE: bool = false;
|
||||
#[cfg(debug_assertions)]
|
||||
const DEV_MODE: bool = true;
|
||||
|
||||
#[derive(Embed)]
|
||||
#[derive(rust_embed::RustEmbed)]
|
||||
#[folder = "target/site/"]
|
||||
struct Assets;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::routing::get;
|
||||
pub use axum::Router;
|
||||
use hackernews_islands::*;
|
||||
pub use leptos::config::get_configuration;
|
||||
@@ -26,7 +25,6 @@ async fn main() {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/favicon.ico", get(fallback::file_and_error_handler))
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::api;
|
||||
use leptos::{either::Either, prelude::*};
|
||||
use leptos::either::Either;
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::Meta;
|
||||
use leptos_router::{components::A, hooks::use_params_map};
|
||||
use leptos_router::components::A;
|
||||
use leptos_router::hooks::use_params_map;
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_story(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::api;
|
||||
use leptos::{either::Either, prelude::*, server::Resource};
|
||||
use leptos::server::Resource;
|
||||
use leptos::{either::Either, prelude::*};
|
||||
use leptos_router::hooks::use_params_map;
|
||||
|
||||
#[server]
|
||||
|
||||
@@ -23,7 +23,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1.40"
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
axum = { version = "0.8.1", default-features = false, optional = true }
|
||||
axum = { version = "0.7.5", default-features = false, optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
http = { version = "1.1", optional = true }
|
||||
web-sys = { version = "0.3.70", features = [
|
||||
@@ -57,7 +57,7 @@ ssr = [
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "http", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"], []]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
channel = "stable" # test change
|
||||
|
||||
@@ -10,12 +10,15 @@ crate-type = ["cdylib", "rlib"]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
http = "1.1"
|
||||
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"tracing",
|
||||
"islands",
|
||||
] }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
@@ -41,7 +44,7 @@ panic = "abort"
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"], []]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
channel = "stable" # test change
|
||||
|
||||
@@ -10,20 +10,22 @@ crate-type = ["cdylib", "rlib"]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
http = "1.1"
|
||||
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"tracing",
|
||||
"islands",
|
||||
] }
|
||||
leptos_router = { path = "../../router" }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
|
||||
leptos_axum = { path = "../../integrations/axum", features = [
|
||||
"islands-router",
|
||||
"dont-use-islands-router",
|
||||
], optional = true }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
wasm-bindgen = "0.2.100"
|
||||
serde_json = "1.0.133"
|
||||
wasm-bindgen = "0.2.93"
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
@@ -45,7 +47,7 @@ panic = "abort"
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"], []]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
@@ -56,11 +58,11 @@ site-root = "target/site"
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "style.css"
|
||||
style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3009"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
140
examples/islands_router/public/routing.js
Normal file
140
examples/islands_router/public/routing.js
Normal file
@@ -0,0 +1,140 @@
|
||||
window.addEventListener("click", async (ev) => {
|
||||
// confirm that this is an <a> that meets our requirements
|
||||
if (
|
||||
ev.defaultPrevented ||
|
||||
ev.button !== 0 ||
|
||||
ev.metaKey ||
|
||||
ev.altKey ||
|
||||
ev.ctrlKey ||
|
||||
ev.shiftKey
|
||||
)
|
||||
return;
|
||||
|
||||
/** @type HTMLAnchorElement | undefined;*/
|
||||
const a = ev
|
||||
.composedPath()
|
||||
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
||||
|
||||
if (!a) return;
|
||||
|
||||
const svg = a.namespaceURI === "http://www.w3.org/2000/svg";
|
||||
const href = svg ? a.href.baseVal : a.href;
|
||||
const target = svg ? a.target.baseVal : a.target;
|
||||
if (target || (!href && !a.hasAttribute("state"))) return;
|
||||
|
||||
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
||||
if (a.hasAttribute("download") || (rel && rel.includes("external"))) return;
|
||||
|
||||
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
||||
if (
|
||||
url.origin !== window.location.origin // ||
|
||||
// TODO base
|
||||
//(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase()))
|
||||
)
|
||||
return;
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
// fetch the new page
|
||||
const resp = await fetch(url);
|
||||
const htmlString = await resp.text();
|
||||
|
||||
// Use DOMParser to parse the HTML string
|
||||
const parser = new DOMParser();
|
||||
// TODO parse from the request stream instead?
|
||||
const doc = parser.parseFromString(htmlString, 'text/html');
|
||||
|
||||
// The 'doc' variable now contains the parsed DOM
|
||||
const transition = async () => {
|
||||
const oldDocWalker = document.createTreeWalker(document);
|
||||
const newDocWalker = doc.createTreeWalker(doc);
|
||||
let oldNode = oldDocWalker.currentNode;
|
||||
let newNode = newDocWalker.currentNode;
|
||||
while(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
|
||||
oldNode = oldDocWalker.currentNode;
|
||||
newNode = newDocWalker.currentNode;
|
||||
// if the nodes are different, we need to replace the old with the new
|
||||
// because of the typed view tree, this should never actually happen
|
||||
if (oldNode.nodeType !== newNode.nodeType) {
|
||||
oldNode.replaceWith(newNode);
|
||||
}
|
||||
// if it's a text node, just update the text with the new text
|
||||
else if (oldNode.nodeType === Node.TEXT_NODE) {
|
||||
oldNode.textContent = newNode.textContent;
|
||||
}
|
||||
// if it's an element, replace if it's a different tag, or update attributes
|
||||
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
|
||||
/** @type Element */
|
||||
const oldEl = oldNode;
|
||||
/** @type Element */
|
||||
const newEl = newNode;
|
||||
if (oldEl.tagName !== newEl.tagName) {
|
||||
oldEl.replaceWith(newEl);
|
||||
}
|
||||
else {
|
||||
for(const attr of newEl.attributes) {
|
||||
oldEl.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
// we use comment "branch marker" nodes to distinguish between different branches in the statically-typed view tree
|
||||
// if one of these marker is hit, then there are two options
|
||||
// 1) it's the same branch, and we just keep walking until the end
|
||||
// 2) it's a different branch, in which case the old can be replaced with the new wholesale
|
||||
else if (oldNode.nodeType === Node.COMMENT_NODE) {
|
||||
const oldText = oldNode.textContent;
|
||||
const newText = newNode.textContent;
|
||||
if(oldText.startsWith("bo") && newText !== oldText) {
|
||||
oldDocWalker.nextNode();
|
||||
newDocWalker.nextNode();
|
||||
const oldRange = new Range();
|
||||
const newRange = new Range();
|
||||
let oldBranches = 1;
|
||||
let newBranches = 1;
|
||||
while(oldBranches > 0 && newBranches > 0) {
|
||||
if(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
|
||||
console.log(oldDocWalker.currentNode, newDocWalker.currentNode);
|
||||
if(oldDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
|
||||
if(oldDocWalker.currentNode.textContent.startsWith("bo")) {
|
||||
oldBranches += 1;
|
||||
} else if(oldDocWalker.currentNode.textContent.startsWith("bc")) {
|
||||
|
||||
oldBranches -= 1;
|
||||
}
|
||||
}
|
||||
if(newDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
|
||||
if(newDocWalker.currentNode.textContent.startsWith("bo")) {
|
||||
newBranches += 1;
|
||||
} else if(newDocWalker.currentNode.textContent.startsWith("bc")) {
|
||||
|
||||
newBranches -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
oldRange.setStartAfter(oldNode);
|
||||
oldRange.setEndBefore(oldDocWalker.currentNode);
|
||||
newRange.setStartAfter(newNode);
|
||||
newRange.setEndBefore(newDocWalker.currentNode);
|
||||
const newContents = newRange.extractContents();
|
||||
oldRange.deleteContents();
|
||||
oldRange.insertNode(newContents);
|
||||
oldNode.replaceWith(newNode);
|
||||
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} }
|
||||
}
|
||||
};
|
||||
// Not all browsers support startViewTransition; see https://caniuse.com/?search=startViewTransition
|
||||
if (document.startViewTransition) {
|
||||
await document.startViewTransition(transition);
|
||||
} else {
|
||||
await transition()
|
||||
}
|
||||
window.history.pushState(undefined, null, url);
|
||||
});
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
channel = "stable" # test change
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
use leptos::{
|
||||
either::{Either, EitherOf3},
|
||||
prelude::*,
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::{
|
||||
components::{Route, Router, Routes},
|
||||
hooks::{use_params_map, use_query_map},
|
||||
path,
|
||||
components::{FlatRoutes, Route, Router},
|
||||
StaticSegment,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
@@ -17,7 +12,7 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<AutoReload options=options.clone()/>
|
||||
<HydrationScripts options=options islands=true islands_router=true/>
|
||||
<HydrationScripts options=options islands=true/>
|
||||
<link rel="stylesheet" id="leptos" href="/pkg/islands.css"/>
|
||||
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
|
||||
</head>
|
||||
@@ -31,180 +26,34 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
view! {
|
||||
<script src="/routing.js"></script>
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"My Contacts"</h1>
|
||||
<h1>"My Application"</h1>
|
||||
</header>
|
||||
<nav>
|
||||
<a href="/">"Home"</a>
|
||||
<a href="/about">"About"</a>
|
||||
<a href="/">"Page A"</a>
|
||||
<a href="/b">"Page B"</a>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes fallback=|| "Not found.">
|
||||
<Route path=path!("") view=Home/>
|
||||
<Route path=path!("user/:id") view=Details/>
|
||||
<Route path=path!("about") view=About/>
|
||||
</Routes>
|
||||
<p>
|
||||
<label>"Home Checkbox" <input type="checkbox"/></label>
|
||||
</p>
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=StaticSegment("") view=PageA/>
|
||||
<Route path=StaticSegment("b") view=PageB/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn search(query: String) -> Result<Vec<User>, ServerFnError> {
|
||||
let users = tokio::fs::read_to_string("./mock_data.json").await?;
|
||||
let data: Vec<User> = serde_json::from_str(&users)?;
|
||||
let query = query.to_ascii_lowercase();
|
||||
Ok(data
|
||||
.into_iter()
|
||||
.filter(|user| {
|
||||
user.first_name.to_ascii_lowercase().contains(&query)
|
||||
|| user.last_name.to_ascii_lowercase().contains(&query)
|
||||
|| user.email.to_ascii_lowercase().contains(&query)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn delete_user(id: u32) -> Result<(), ServerFnError> {
|
||||
let users = tokio::fs::read_to_string("./mock_data.json").await?;
|
||||
let mut data: Vec<User> = serde_json::from_str(&users)?;
|
||||
data.retain(|user| user.id != id);
|
||||
let new_json = serde_json::to_string(&data)?;
|
||||
tokio::fs::write("./mock_data.json", &new_json).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct User {
|
||||
id: u32,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
email: String,
|
||||
#[component]
|
||||
pub fn PageA() -> impl IntoView {
|
||||
view! { <label>"Page A" <input type="checkbox"/></label> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Home() -> impl IntoView {
|
||||
let q = use_query_map();
|
||||
let q = move || q.read().get("q");
|
||||
let data = Resource::new(q, |q| async move {
|
||||
if let Some(q) = q {
|
||||
search(q).await
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
});
|
||||
let delete_user_action = ServerAction::<DeleteUser>::new();
|
||||
|
||||
let view = move || {
|
||||
Suspend::new(async move {
|
||||
let users = data.await.unwrap();
|
||||
if q().is_none() {
|
||||
EitherOf3::A(view! {
|
||||
<p class="note">"Enter a search to begin viewing contacts."</p>
|
||||
})
|
||||
} else if users.is_empty() {
|
||||
EitherOf3::B(view! {
|
||||
<p class="note">"No users found matching that search."</p>
|
||||
})
|
||||
} else {
|
||||
EitherOf3::C(view! {
|
||||
<table>
|
||||
<tbody>
|
||||
<For
|
||||
each=move || users.clone()
|
||||
key=|user| user.id
|
||||
let:user
|
||||
>
|
||||
<tr>
|
||||
<td>{user.first_name}</td>
|
||||
<td>{user.last_name}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>
|
||||
<a href=format!("/user/{}", user.id)>"Details"</a>
|
||||
<input type="checkbox"/>
|
||||
<ActionForm action=delete_user_action>
|
||||
<input type="hidden" name="id" value=user.id/>
|
||||
<input type="submit" value="Delete"/>
|
||||
</ActionForm>
|
||||
</td>
|
||||
</tr>
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
view! {
|
||||
<section class="page">
|
||||
<form method="GET" class="search">
|
||||
<input type="search" name="q" value=q autofocus oninput="this.form.requestSubmit()"/>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
<Suspense fallback=|| view! { <p>"Loading..."</p> }>{view}</Suspense>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Details() -> impl IntoView {
|
||||
#[server]
|
||||
pub async fn get_user(id: u32) -> Result<Option<User>, ServerFnError> {
|
||||
let users = tokio::fs::read_to_string("./mock_data.json").await?;
|
||||
let data: Vec<User> = serde_json::from_str(&users)?;
|
||||
Ok(data.iter().find(|user| user.id == id).cloned())
|
||||
}
|
||||
let params = use_params_map();
|
||||
let id = move || {
|
||||
params
|
||||
.read()
|
||||
.get("id")
|
||||
.and_then(|id| id.parse::<u32>().ok())
|
||||
};
|
||||
let user = Resource::new(id, |id| async move {
|
||||
match id {
|
||||
None => Ok(None),
|
||||
Some(id) => get_user(id).await,
|
||||
}
|
||||
});
|
||||
|
||||
move || {
|
||||
Suspend::new(async move {
|
||||
user.await.map(|user| match user {
|
||||
None => Either::Left(view! {
|
||||
<section class="page">
|
||||
<h2>"Not found."</h2>
|
||||
<p>"Sorry — we couldn’t find that user."</p>
|
||||
</section>
|
||||
}),
|
||||
Some(user) => Either::Right(view! {
|
||||
<section class="page">
|
||||
<h2>{user.first_name} " " { user.last_name}</h2>
|
||||
<p class="email">{user.email}</p>
|
||||
</section>
|
||||
}),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn About() -> impl IntoView {
|
||||
view! {
|
||||
<section class="page">
|
||||
<h2>"About"</h2>
|
||||
<p>"This demo is intended to show off an experimental “islands router” feature, which mimics the smooth transitions and user experience of client-side routing while minimizing the amount of code that actually runs in the browser."</p>
|
||||
<p>"By default, all the content in this application is only rendered on the server. But you can add client-side interactivity via islands like this one:"</p>
|
||||
<Counter/>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
#[island]
|
||||
pub fn Counter() -> impl IntoView {
|
||||
let count = RwSignal::new(0);
|
||||
view! {
|
||||
<button class="counter" on:click=move |_| *count.write() += 1>{count}</button>
|
||||
}
|
||||
pub fn PageB() -> impl IntoView {
|
||||
view! { <label>"Page B" <input type="checkbox"/></label> }
|
||||
}
|
||||
|
||||
@@ -1,52 +1,3 @@
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
background-color: #f6f6fa;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: ui-rounded, 'Hiragino Maru Gothic ProN', Quicksand, Comfortaa, Manjari, 'Arial Rounded MT', 'Arial Rounded MT Bold', Calibri, source-sans-pro, sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
nav a {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
form.search {
|
||||
display: flex;
|
||||
margin: 2rem auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
td {
|
||||
min-width: 10rem;
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
table {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
td:last-child > * {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.note, .note {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button.counter {
|
||||
display: block;
|
||||
font-size: 2rem;
|
||||
margin: auto;
|
||||
.pending {
|
||||
color: purple;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
]
|
||||
|
||||
[tasks.clippy-each-feature]
|
||||
dependencies = ["cargo-all-features"]
|
||||
[tasks.build]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"all-features",
|
||||
"clippy",
|
||||
"--target",
|
||||
"wasm32-unknown-unknown",
|
||||
"--no-deps",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings",
|
||||
]
|
||||
args = ["build-all-features", "--target", "wasm32-unknown-unknown"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["check-all-features", "--target", "wasm32-unknown-unknown"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.pre-clippy]
|
||||
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features --target wasm32-unknown-unknown -- -D warnings" }
|
||||
|
||||
2
examples/js-framework-benchmark/rust-toolchain.toml
Normal file
2
examples/js-framework-benchmark/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
@@ -149,12 +149,12 @@ pub fn App() -> impl IntoView {
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="row">
|
||||
<Button id="run" text="Create 1,000 rows" on:click=run />
|
||||
<Button id="runlots" text="Create 10,000 rows" on:click=run_lots />
|
||||
<Button id="add" text="Append 1,000 rows" on:click=add />
|
||||
<Button id="update" text="Update every 10th row" on:click=update />
|
||||
<Button id="clear" text="Clear" on:click=clear />
|
||||
<Button id="swaprows" text="Swap Rows" on:click=swap_rows />
|
||||
<Button id="run" text="Create 1,000 rows" on:click=run/>
|
||||
<Button id="runlots" text="Create 10,000 rows" on:click=run_lots/>
|
||||
<Button id="add" text="Append 1,000 rows" on:click=add/>
|
||||
<Button id="update" text="Update every 10th row" on:click=update/>
|
||||
<Button id="clear" text="Clear" on:click=clear/>
|
||||
<Button id="swaprows" text="Swap Rows" on:click=swap_rows/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,7 +169,7 @@ pub fn App() -> impl IntoView {
|
||||
let label = row.label;
|
||||
let is_selected = is_selected.clone();
|
||||
template! {
|
||||
< tr class : danger = { move || is_selected.selected(&Some(row_id)) }
|
||||
< tr class : danger = { move || is_selected.selected(Some(row_id)) }
|
||||
> < td class = "col-md-1" > { row_id.to_string() } </ td > < td
|
||||
class = "col-md-4" >< a on : click = move | _ | set_selected
|
||||
.set(Some(row_id)) > { move || label.get() } </ a ></ td > < td
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
channel = "stable" # test change
|
||||
|
||||
@@ -5,12 +5,12 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
log = "0.4.27"
|
||||
log = "0.4.22"
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
wasm-bindgen = "0.2.100"
|
||||
wasm-bindgen = "0.2.93"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.50"
|
||||
wasm-bindgen = "0.2.100"
|
||||
web-sys = "0.3.77"
|
||||
wasm-bindgen-test = "0.3.42"
|
||||
wasm-bindgen = "0.2.93"
|
||||
web-sys = "0.3.70"
|
||||
@@ -1,6 +1,5 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/webdriver.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
]
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
channel = "stable" # test change
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user