Compare commits

..

127 Commits

Author SHA1 Message Date
Greg Johnston
9e8b559e51 handle optional props correctly 2023-08-19 07:21:56 -04:00
Greg Johnston
db4ce00f98 add MaybeSignal deserialization 2023-08-19 07:21:56 -04:00
Greg Johnston
9edcc5f43b add support for island props 2023-08-19 07:21:56 -04:00
Greg Johnston
ea2cda3d17 work on serializing props 2023-08-19 07:21:56 -04:00
Greg Johnston
1d5252b97e fix iOS Safari pleeeaaassseeee stop it, Apple 2023-08-19 07:21:56 -04:00
Greg Johnston
d175249d00 fix build 2023-08-19 07:21:56 -04:00
Greg Johnston
35a9d84734 ignore rIC if not supported 2023-08-19 07:21:56 -04:00
Greg Johnston
c8a5434c9b start work on serializing props in DOM 2023-08-19 07:21:56 -04:00
Greg Johnston
ac00ea4ffe fix merge issues 2023-08-19 07:21:56 -04:00
Greg Johnston
59b708db9f support suspense with islands 2023-08-19 07:21:56 -04:00
Greg Johnston
722f6913d1 make whole JS import idle-only 2023-08-19 07:21:56 -04:00
Greg Johnston
aabcfb7ac3 switch to data-hk for all use cases 2023-08-19 07:21:56 -04:00
Greg Johnston
e691ae391f fix re-layouts on hydration (WIP) 2023-08-19 07:21:56 -04:00
Greg Johnston
7c3fd1d67d use correct name for hkc 2023-08-19 07:21:56 -04:00
Greg Johnston
88ee228389 turn fragments back off 2023-08-19 07:21:56 -04:00
Greg Johnston
02b0ac15a3 make whole hydration process idle 2023-08-19 07:21:56 -04:00
Greg Johnston
83edcfe29d restore compression 2023-08-19 07:21:56 -04:00
Greg Johnston
b288b7d838 do set fragment 2023-08-19 07:21:56 -04:00
Greg Johnston
a1a78f165a axum islands example 2023-08-19 07:21:56 -04:00
Greg Johnston
1f3453e94b rIC per island and resuming based on keys both working 2023-08-19 07:21:56 -04:00
Greg Johnston
e8b35ba284 work on resuming HK 2023-08-19 07:21:56 -04:00
Greg Johnston
02275d4fc0 requestIdleCallback per island 2023-08-19 07:21:56 -04:00
Greg Johnston
18e44be19a playing with islands hackernews 2023-08-19 07:21:56 -04:00
Greg Johnston
0bd4e5269a request idle callback before hydration 2023-08-19 07:21:56 -04:00
Greg Johnston
88405bfd10 intern id removal 2023-08-19 07:21:56 -04:00
Greg Johnston
80bc9a4e10 islands 2023-08-19 07:21:56 -04:00
Greg Johnston
b4c3068ddd got children working! 2023-08-19 07:21:56 -04:00
Greg Johnston
ac5406b8a6 fix merge issues 2023-08-19 07:21:56 -04:00
Greg Johnston
8f38a8a5f5 do the island hydration directly in JS, save a kb 2023-08-19 07:21:56 -04:00
Greg Johnston
bd7ff6a3fc support multiple islands, oops 2023-08-19 07:21:56 -04:00
Greg Johnston
c2d9696494 working super-basic islands implementation 2023-08-19 07:21:56 -04:00
Greg Johnston
35893d4eb9 release mode benchmarks 2023-08-19 07:20:10 -04:00
Greg Johnston
c3959d1de8 make hydration keys optional (to allow NoHydrate areas) 2023-08-19 07:20:10 -04:00
Greg Johnston
76c9d8233a islands 2023-08-19 07:20:10 -04:00
Greg Johnston
2988fc1c36 initial islands work 2023-08-19 07:20:09 -04:00
Greg Johnston
e7662ae940 perf: don't include unused hydration code in CSR bundles 2023-08-19 07:20:09 -04:00
Greg Johnston
674fcd2ade stash 2023-08-19 07:20:09 -04:00
Greg Johnston
5beab9f3cc fix examples 2023-08-19 07:03:54 -04:00
Greg Johnston
49be4a219d .map_ref() => .map() 2023-08-18 14:14:45 -04:00
Greg Johnston
f92aebdf01 cargo fmt example 2023-08-18 14:14:07 -04:00
Greg Johnston
6ba7a8e235 nope 2023-08-18 14:07:42 -04:00
Greg Johnston
10e3106760 removing cx/Scope from some merged docs 2023-08-18 08:41:37 -04:00
Greg Johnston
63dee1c93f closes issue #1558 2023-08-18 08:36:56 -04:00
Greg Johnston
fca4215fdb Change resource API to .get(), .with(), .map_ref(), and .and_then() 2023-08-18 08:36:56 -04:00
Greg Johnston
72cde5c355 use tuple form of .child() 2023-08-18 08:36:56 -04:00
Greg Johnston
0bf1f11638 avoid panic in use_navigate during SSR 2023-08-18 08:36:56 -04:00
Greg Johnston
2ea4531313 0.5.0-beta 2023-08-18 08:36:56 -04:00
Greg Johnston
bd2acfc530 fix example 2023-08-18 08:36:56 -04:00
Greg Johnston
42ff663622 sometimes Rust's variable scoping rules baffle me (should fix panic with overlapping BorrowMut in on_cleanup) 2023-08-18 08:36:56 -04:00
Greg Johnston
8ffd5c69ab erroneous log 2023-08-18 08:36:56 -04:00
Greg Johnston
90ce31edf8 fix unsetting of title when navigating between multiple pages 2023-08-18 08:36:56 -04:00
Greg Johnston
e7160092f6 fix Suspense-For-Suspense panic 2023-08-18 08:36:56 -04:00
Greg Johnston
c6bd7a40e7 pull in changes to view macor from main 2023-08-18 08:36:56 -04:00
Greg Johnston
d544a678b1 remove removed API 2023-08-18 08:36:56 -04:00
Greg Johnston
4b96a71da5 on_cleanup untracked (fixes #1494) 2023-08-18 08:36:56 -04:00
Greg Johnston
6ab95d8a16 remove deprecated APIs 2023-08-18 08:36:56 -04:00
Greg Johnston
db51f6d2b4 embed requestAnimationFrame into navigate() 2023-08-18 08:36:56 -04:00
Greg Johnston
964a26b5d3 BAD rebase, NO rebase. 2023-08-18 08:36:56 -04:00
Greg Johnston
1871d2bc6d impl Serialize and Deserialize for signal types 2023-08-18 08:36:56 -04:00
Greg Johnston
fc82788bcc add some useful From implementations 2023-08-18 08:36:56 -04:00
Ben Wishovich
dba5c444ae fix: render_route() path matching (#1479) 2023-08-18 08:36:56 -04:00
Greg Johnston
95f285f126 fix <For/> in SSR 2023-08-18 08:36:56 -04:00
Greg Johnston
5865ea4048 fix: restore missing fixes in <For/> (closes #1473) 2023-08-18 08:36:56 -04:00
Greg Johnston
121c312ba3 alpha 2 2023-08-18 08:36:56 -04:00
Greg Johnston
19967d1dd5 remove duplicate from merge 2023-08-18 08:36:56 -04:00
Greg Johnston
42a1395ffd clippy 2023-08-18 08:36:56 -04:00
Greg Johnston
421715f923 clean up window click listener 2023-08-18 08:36:56 -04:00
Greg Johnston
0eba690993 cancelable handle for window_event_listener 2023-08-18 08:36:56 -04:00
Greg Johnston
2a971b5fd3 remove scope 2023-08-18 08:36:56 -04:00
Greg Johnston
8701bd88bd closes #1465 2023-08-18 08:36:56 -04:00
Greg Johnston
8ccc731e91 better error messages for expect_context 2023-08-18 08:36:56 -04:00
Greg Johnston
b9bb999b6a was breaking something 2023-08-18 08:36:56 -04:00
Greg Johnston
d7080bff96 don't double-create children in nested suspense in ssr 2023-08-18 08:36:56 -04:00
Greg Johnston
52eea6222b track caller for better error messages 2023-08-18 08:36:56 -04:00
Greg Johnston
c1c469fd08 better error messages 2023-08-18 08:36:56 -04:00
Greg Johnston
42d18d4b54 clippy 2023-08-18 08:36:56 -04:00
Greg Johnston
e69d55190e fix routing progress display 2023-08-18 08:36:56 -04:00
Ben Wishovich
431f5398f9 Add render_route functions to respect SsrMode on routes when using custom handlers (#1460) 2023-08-18 08:36:56 -04:00
Greg Johnston
3223182141 add <Route data/> loaders again 2023-08-18 08:36:56 -04:00
Greg Johnston
815acee19b even even better error message 2023-08-18 08:36:56 -04:00
Greg Johnston
270622ce86 typo in doctest 2023-08-18 08:36:56 -04:00
Greg Johnston
de97e2dc12 fix ssr tests 2023-08-18 08:36:56 -04:00
Greg Johnston
3dd12bf416 fix release build SSR 2023-08-18 08:36:56 -04:00
Greg Johnston
59d3450d5b gtfo 2023-08-18 08:36:56 -04:00
Greg Johnston
02d0849a34 don't need to render EachItem markers for View::Element nodes 2023-08-18 08:36:56 -04:00
Greg Johnston
41f8d66565 restore continuing from current id 2023-08-18 08:36:56 -04:00
Greg Johnston
8da6710e44 add separate error field in hydration keys 2023-08-18 08:36:56 -04:00
IcosaHedron
38a68926ca cleanups must be called before properties are disposed (#1449) 2023-08-18 08:36:56 -04:00
Greg Johnston
895c8765ed add MaybeProp 2023-08-18 08:36:56 -04:00
Greg Johnston
ddd797f853 clean up logs 2023-08-18 08:36:56 -04:00
Greg Johnston
39dd204cda fix spawn docs examples 2023-08-18 08:36:56 -04:00
Greg Johnston
90041a9e99 fix reactive tests 2023-08-18 08:36:56 -04:00
Greg Johnston
d3a6d59970 cx in tests 2023-08-18 08:36:56 -04:00
Greg Johnston
314c803e4d v0.5.0-alpha 2023-08-18 08:36:56 -04:00
Greg Johnston
f5028e200e add some panic docs 2023-08-18 08:36:56 -04:00
Greg Johnston
fd15859ee4 clean up docs 2023-08-18 08:36:56 -04:00
Greg Johnston
d4ede63b3a clippy 2023-08-18 08:36:42 -04:00
Greg Johnston
b7c4a9d5c7 fix errorboundary 2023-08-18 08:36:42 -04:00
Greg Johnston
c780924c3d fix views in component macro docs 2023-08-18 08:36:42 -04:00
Greg Johnston
d2e8981e94 component macro 2023-08-18 08:36:42 -04:00
Greg Johnston
449a14bf16 fix issues in examples 2023-08-18 08:36:42 -04:00
Greg Johnston
5cc4c977ad tests 2023-08-18 08:36:42 -04:00
Greg Johnston
67ee6d5dbf ssr tests 2023-08-18 08:36:42 -04:00
Greg Johnston
919ab91cb0 fix 2023-08-18 08:36:42 -04:00
Greg Johnston
3f0c908479 fixing tests 2023-08-18 08:36:42 -04:00
Greg Johnston
a7cf566700 clippy 2023-08-18 08:36:42 -04:00
Greg Johnston
a30356da12 remove _ = cx; 2023-08-18 08:36:42 -04:00
Greg Johnston
c612c2c0a3 clippy 2023-08-18 08:36:42 -04:00
Greg Johnston
370d8a951a fix merge issues 2023-08-18 08:36:42 -04:00
Greg Johnston
b3d75125d4 clean up 2023-08-18 08:36:42 -04:00
Greg Johnston
b12b397f15 clean up 2023-08-18 08:36:42 -04:00
Greg Johnston
39d9b806ff remove Scope from docs 2023-08-18 08:36:42 -04:00
Greg Johnston
02c4bb31bc remove cx from docs 2023-08-18 08:36:41 -04:00
Greg Johnston
52f0ce4b48 fix Viz 2023-08-18 08:35:57 -04:00
Greg Johnston
b8d30676f8 move forbid(unsafe) to crate level 2023-08-18 08:35:57 -04:00
Greg Johnston
e7b52c0076 fix watch 2023-08-18 08:35:57 -04:00
Greg Johnston
8cc3ece052 even better error msg 2023-08-18 08:35:57 -04:00
Greg Johnston
a19e83818d better error when failing to deserialize resource JSON 2023-08-18 08:35:57 -04:00
Greg Johnston
e92029d36c fix suspense ownership chain 2023-08-18 08:35:57 -04:00
Greg Johnston
4a9d959488 unused import 2023-08-18 08:35:57 -04:00
Greg Johnston
9fd9d94783 get AnimatedOutlet working 2023-08-18 08:35:57 -04:00
Greg Johnston
241ca5e4dd restore <AnimatedRoutes/> 2023-08-18 08:35:57 -04:00
Greg Johnston
39e81136c5 fix suspense 2023-08-18 08:35:57 -04:00
Greg Johnston
0ebf63f8af cleaning up tests 2023-08-18 08:35:57 -04:00
Greg Johnston
0b532735fc default impls 2023-08-18 08:35:57 -04:00
Greg Johnston
40998f62c3 don't store runtime ID in signals 2023-08-18 08:35:57 -04:00
Greg Johnston
4df6057393 squash work on reactive ownership 2023-08-18 08:35:57 -04:00
270 changed files with 6501 additions and 6741 deletions

View File

@@ -9,21 +9,66 @@ on:
- main
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
setup:
name: Get Examples
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
source_changed: ${{ steps.set-source-changed.outputs.source_changed }}
steps:
- name: Checkout
uses: actions/checkout@v3
get-examples-matrix:
uses: ./.github/workflows/get-examples-matrix.yml
- name: Install JQ Tool
uses: mbround18/install-jq@v1
test:
- name: Set Matrix
id: set-matrix
run: |
examples=$(ls examples |
awk '{print "examples/" $0}' |
grep -v examples/README.md |
grep -v examples/Makefile.toml |
grep -v examples/cargo-make |
grep -v examples/gtk |
jq -R -s -c 'split("\n")[:-1]')
echo "Example Directories: $examples"
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v36
with:
files: |
integrations
leptos
leptos_config
leptos_dom
leptos_hot_reload
leptos_macro
leptos_reactive
leptos_server
meta
router
server_fn
server_fn_macro
- name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
- name: Set source_changed
id: set-source-changed
run: |
echo "source_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
matrix-job:
name: Check
needs: [get-leptos-changed, get-examples-matrix]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
needs: [setup]
if: needs.setup.outputs.source_changed == 'true'
strategy:
matrix: ${{ fromJSON(needs.get-examples-matrix.outputs.matrix) }}
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"
toolchain: nightly

View File

@@ -2,25 +2,82 @@ name: Check stable
on:
push:
branches:
- main
branches: [main]
pull_request:
branches:
- main
branches: [main]
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
setup:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
source_changed: ${{ steps.set-source-changed.outputs.source_changed }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v36
with:
files: |
integrations
leptos
leptos_config
leptos_dom
leptos_hot_reload
leptos_macro
leptos_reactive
leptos_server
meta
router
server_fn
server_fn_macro
- name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
- name: Set source_changed
id: set-source-changed
run: |
echo "source_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
test:
name: Check
needs: [get-leptos-changed]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
needs: [setup]
if: needs.setup.outputs.source_changed == 'true'
runs-on: ${{ matrix.os }}
strategy:
matrix:
directory: [examples/counters_stable, examples/counter_without_macros]
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"
toolchain: stable
rust:
- stable
os:
- ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
- 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: Run cargo check on all examples
run: cargo make --profile=github-actions check-stable

View File

@@ -1,32 +0,0 @@
name: CI Changed Examples
on:
push:
branches:
- main
pull_request:
branches:
- main
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: nightly

View File

@@ -9,13 +9,45 @@ on:
- main
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
setup:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
source_changed: ${{ steps.set-source-changed.outputs.source_changed }}
steps:
- name: Checkout
uses: actions/checkout@v3
test:
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v36
with:
files: |
integrations
leptos
leptos_config
leptos_dom
leptos_hot_reload
leptos_macro
leptos_reactive
leptos_server
meta
router
server_fn
server_fn_macro
- name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
- name: Set source_changed
id: set-source-changed
run: |
echo "source_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
matrix-job:
name: CI
needs: [get-leptos-changed]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
needs: [setup]
if: needs.setup.outputs.source_changed == 'true'
strategy:
matrix:
directory:
@@ -41,4 +73,3 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly

View File

@@ -1,39 +0,0 @@
name: Examples Changed Call
on:
workflow_call:
outputs:
example_changed:
description: "Example Changed"
value: ${{ jobs.get-example-changed.outputs.example_changed }}
jobs:
get-example-changed:
name: Get Example Changed
runs-on: ubuntu-latest
outputs:
example_changed: ${{ steps.set-example-changed.outputs.example_changed }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get example files that changed
id: changed-files
uses: tj-actions/changed-files@v36
with:
files: |
examples
!examples/cargo-make
!examples/gtk
!examples/Makefile.toml
!examples/README.md
- name: List example files that changed
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'
- name: Set example_changed
id: set-example-changed
run: |
echo "example_changed=${{ steps.changed-files.outputs.any_changed }}" >> "$GITHUB_OUTPUT"

View File

@@ -1,40 +0,0 @@
name: Get Examples Matrix Call
on:
workflow_call:
outputs:
matrix:
description: "Matrix"
value: ${{ jobs.create.outputs.matrix }}
jobs:
create:
name: Create Examples Matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install JQ Tool
uses: mbround18/install-jq@v1
- name: Set Matrix
id: set-matrix
run: |
examples=$(ls examples |
awk '{print "examples/" $0}' |
grep -v examples/README.md |
grep -v examples/Makefile.toml |
grep -v examples/cargo-make |
grep -v examples/gtk |
jq -R -s -c 'split("\n")[:-1]')
echo "Example Directories: $examples"
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
- name: Print Location Info
run: |
echo "Workspace: ${{ github.workspace }}"
pwd
ls | sort -u

View File

@@ -1,44 +0,0 @@
name: Get Leptos Changed Call
on:
workflow_call:
outputs:
leptos_changed:
description: "Leptos Changed"
value: ${{ jobs.create.outputs.leptos_changed }}
jobs:
create:
name: Detect Source Change
runs-on: ubuntu-latest
outputs:
leptos_changed: ${{ steps.set-source-changed.outputs.leptos_changed }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v36
with:
files: |
integrations
leptos
leptos_config
leptos_dom
leptos_hot_reload
leptos_macro
leptos_reactive
leptos_server
meta
router
server_fn
server_fn_macro
- name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
- name: Set leptos_changed
id: set-source-changed
run: |
echo "leptos_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"

View File

@@ -1,8 +1,9 @@
name: Deploy book
on:
push:
tags:
- '*-?v[0-9]+*'
paths: ['docs/book/**']
branches:
- main
jobs:
deploy:
@@ -33,4 +34,4 @@ jobs:
mv ../book/* .
git add .
git commit -m "Deploy book $GITHUB_SHA to gh-pages"
git push --force --set-upstream origin gh-pages
git push --force --set-upstream origin gh-pages

View File

@@ -9,27 +9,34 @@ on:
cargo_make_task:
required: true
type: string
toolchain:
required: true
type: string
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
jobs:
test:
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
runs-on: ubuntu-latest
name: Run ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
steps:
# Setup environment
- uses: actions/checkout@v4
- name: Install playwright browser dependencies
run: |
sudo apt-get update
sudo apt-get install libegl1 libvpx7 libevent-2.1-7 libopus0 libopengl0 libwoff1 libharfbuzz-icu0 libgstreamer-plugins-base1.0-0 libgstreamer-gl1.0-0 libhyphen0 libmanette-0.2-0 libgles2 gstreamer1.0-libav
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ inputs.toolchain }}
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
@@ -77,38 +84,17 @@ jobs:
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Maybe install chromedriver
- name: Install Chrome Webriver
run: |
project_makefile=${{inputs.directory}}/Makefile.toml
webdriver_count=$(cat $project_makefile | grep "cargo-make/webdriver.toml" | wc -l)
if [ $webdriver_count -eq 1 ]; then
if ! command -v chromedriver &>/dev/null; then
echo chromedriver required
sudo apt-get update
sudo apt-get install chromium-chromedriver
else
echo chromedriver is already installed
fi
else
echo chromedriver is not required
fi
- name: Maybe install playwright browser dependencies
run: |
for pw_path in $(find ${{inputs.directory}} -name playwright.config.ts)
do
pw_dir=$(dirname $pw_path)
if [ ! -v $pw_dir ]; then
echo "Playwright required in $pw_dir"
cd $pw_dir
pnpm dlx playwright install --with-deps
else
echo Playwright is not required
fi
done
sudo apt-get update
sudo apt-get install chromium-chromedriver
# Run Cargo Make Task
- name: ${{ inputs.cargo_make_task }}
run: |
cd ${{ inputs.directory }}
cargo make --profile=github-actions ${{ inputs.cargo_make_task }}
if [ "${{ inputs.directory }}" = "INTERNAL" ]; then
echo No verification required
else
cd ${{ inputs.directory }}
cargo make --profile=github-actions ${{ inputs.cargo_make_task }}
fi

View File

@@ -1,4 +1,4 @@
name: CI Examples
name: Verify All Examples
on:
workflow_dispatch:
@@ -10,17 +10,38 @@ on:
- cron: "0 8 * * *"
jobs:
get-examples-matrix:
uses: ./.github/workflows/get-examples-matrix.yml
setup:
name: Get Examples
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v3
test:
name: CI
needs: [get-examples-matrix]
- name: Install JQ Tool
uses: mbround18/install-jq@v1
- name: Set Matrix
id: set-matrix
run: |
examples=$(ls examples |
awk '{print "examples/" $0}' |
grep -v examples/README.md |
grep -v examples/Makefile.toml |
grep -v examples/cargo-make |
grep -v examples/gtk |
jq -R -s -c 'split("\n")[:-1]')
echo "Example Directories: $examples"
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
matrix-job:
name: Verify
needs: [setup]
strategy:
matrix: ${{ fromJSON(needs.get-examples-matrix.outputs.matrix) }}
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly
cargo_make_task: "verify-flow"

View File

@@ -1,20 +1,16 @@
name: Changed Examples Matrix Call
name: Verify Changed Examples
on:
workflow_call:
inputs:
example_changed:
description: "Example Changed"
required: true
type: boolean
outputs:
matrix:
description: "Matrix"
value: ${{ jobs.get-example-changed.outputs.matrix }}
push:
branches:
- main
pull_request:
branches:
- main
jobs:
get-example-changed:
name: Get Changed Example Matrix
setup:
name: Get Changes
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
@@ -24,6 +20,16 @@ jobs:
with:
fetch-depth: 0
- name: Get all example files that changed
id: changed-files
uses: tj-actions/changed-files@v36
with:
files: |
examples
- name: List all example files that changed
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'
- name: Get example project directories that changed
id: changed-dirs
uses: tj-actions/changed-files@v36
@@ -45,10 +51,21 @@ jobs:
- name: Set Matrix
id: set-matrix
run: |
if [ ${{ inputs.example_changed }} == 'true' ]; then
if [ ${{ steps.changed-files.outputs.any_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"
echo "matrix={\"directory\":[\"INTERNAL\"]}" >> "$GITHUB_OUTPUT"
fi
matrix-job:
name: Verify
needs: [setup]
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "verify-flow"

2
.gitignore vendored
View File

@@ -9,5 +9,3 @@ Cargo.lock
.idea
.direnv
.envrc
.vscode

View File

@@ -26,22 +26,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.5.0-beta2"
version = "0.5.0-beta"
[workspace.dependencies]
leptos = { path = "./leptos", version = "0.5.0-beta2" }
leptos_dom = { path = "./leptos_dom", version = "0.5.0-beta2" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.0-beta2" }
leptos_macro = { path = "./leptos_macro", version = "0.5.0-beta2" }
leptos_reactive = { path = "./leptos_reactive", version = "0.5.0-beta2" }
leptos_server = { path = "./leptos_server", version = "0.5.0-beta2" }
server_fn = { path = "./server_fn", version = "0.5.0-beta2" }
server_fn_macro = { path = "./server_fn_macro", version = "0.5.0-beta2" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.0-beta2" }
leptos_config = { path = "./leptos_config", version = "0.5.0-beta2" }
leptos_router = { path = "./router", version = "0.5.0-beta2" }
leptos_meta = { path = "./meta", version = "0.5.0-beta2" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.0-beta2" }
leptos = { path = "./leptos", version = "0.5.0-beta" }
leptos_dom = { path = "./leptos_dom", version = "0.5.0-beta" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.0-beta" }
leptos_macro = { path = "./leptos_macro", version = "0.5.0-beta" }
leptos_reactive = { path = "./leptos_reactive", version = "0.5.0-beta" }
leptos_server = { path = "./leptos_server", version = "0.5.0-beta" }
server_fn = { path = "./server_fn", version = "0.5.0-beta" }
server_fn_macro = { path = "./server_fn_macro", version = "0.5.0-beta" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.0-beta" }
leptos_config = { path = "./leptos_config", version = "0.5.0-beta" }
leptos_router = { path = "./router", version = "0.5.0-beta" }
leptos_meta = { path = "./meta", version = "0.5.0-beta" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.0-beta" }
[profile.release]
codegen-units = 1

View File

@@ -16,9 +16,9 @@
use leptos::*;
#[component]
pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
// create a reactive signal with the initial value
let (value, set_value) = create_signal(initial_value);
let (value, set_value) = create_signal(cx, initial_value);
// create event handlers for our buttons
// note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
@@ -27,29 +27,23 @@ pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
let increment = move |_| set_value.update(|value| *value += 1);
// create user interfaces with the declarative `view!` macro
view! {
view! { cx,
<div>
<button on:click=clear>Clear</button>
<button on:click=decrement>-1</button>
// text nodes can be quoted or unquoted
<button on:click=clear>"Clear"</button>
<button on:click=decrement>"-1"</button>
<span>"Value: " {value} "!"</span>
<button on:click=increment>+1</button>
<button on:click=increment>"+1"</button>
</div>
}
}
// Easy to use with Trunk (trunkrs.dev) or with a simple wasm-bindgen setup
pub fn main() {
mount_to_body(|| view! {
<SimpleCounter initial_value=3 />
})
mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=3 /> })
}
```
### Important Note
This example, and the entire `main` branch, now reflect the upcoming `0.5.0` release. You can use `0.5.0` with the `0.5.0-beta` release on crates.io or by a git dependency on the `main` branch of this repo. [Click here for the 0.4.9 `README`](https://crates.io/crates/leptos).
## About the Framework
Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained reactivity to build declarative user interfaces.
@@ -119,7 +113,7 @@ People usually mean one of three things by this question.
1. **Are the APIs stable?** i.e., will I have to rewrite my whole app from Leptos 0.1 to 0.2 to 0.3 to 0.4, or can I write it now and benefit from new features and updates as new versions come?
The APIs are basically settled. Were adding new features, but were very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to future releases, in terms of architecture.
The APIs are basically settled. Were adding new features, but were very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to future releases. The sorts of breaking changes that we discuss are things like “Oh yeah, that function should probably take `cx` as its argument...” not major changes to the way you write your application.
2. **Are there bugs?**
@@ -158,13 +152,13 @@ There are some practical differences that make a significant difference:
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
- **Server integration:** Leptos provides primitives that encourage HTML streaming and allow for easy async integration and RPC calls, even without WASM enabled, making it easy to opt into integrations between your frontend and backend code without pushing you toward any particular metaframework patterns.
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(0);` _(If you prefer or if it's more convenient for your API, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) to give a unified read/write signal.)_
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) to give a unified read/write signal.)_
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:
```rust
let (count, set_count) = create_signal(0); // a signal
let (count, set_count) = create_signal(cx, 0); // a signal
let double_count = move || count() * 2; // a derived signal
let memoized_count = create_memo(move |_| count() * 3); // a memo
let memoized_count = create_memo(cx, move |_| count() * 3); // a memo
// all are accessed by calling them
assert_eq!(count(), 0);
assert_eq!(double_count(), 0);

View File

@@ -1,13 +0,0 @@
# Security Policy
## Reporting a Vulnerability
To report a suspected security issue, please contact security@leptos.dev rather than opening
a public issue.
## Supported Versions
The most-recently-released version of the library is supported with security updates.
For example, if a security issue is discovered that affects 0.3.2 and all later releases,
a 0.4.x patch will be released but a new 0.3.x patch release will not be made. You should
plan to update to the latest version to receive any new features or bugfixes of any kind.

View File

@@ -5,9 +5,9 @@ edition = "2021"
[dependencies]
l021 = { package = "leptos", version = "0.2.1" }
leptos = { path = "../leptos", features = ["ssr"] }
leptos = { path = "../leptos", features = ["ssr", "nightly", "islands"] }
sycamore = { version = "0.8", features = ["ssr"] }
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
yew = { version = "0.20", features = ["ssr"] }
tokio-test = "0.4"
miniserde = "0.1"
gloo = "0.8"

View File

@@ -7,15 +7,15 @@ fn leptos_deep_creation(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, || {
let signal = create_rw_signal(0);
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for _ in 0..1000usize {
let prev = memos.last().copied();
if let Some(prev) = prev {
memos.push(create_memo(move |_| prev.get() + 1));
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(move |_| signal.get() + 1));
memos.push(create_memo(cx, move |_| signal.get() + 1));
}
}
})
@@ -31,14 +31,14 @@ fn leptos_deep_update(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, || {
let signal = create_rw_signal(0);
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(move |_| prev.get() + 1));
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(move |_| signal.get() + 1));
memos.push(create_memo(cx, move |_| signal.get() + 1));
}
}
signal.set(1);
@@ -56,11 +56,12 @@ fn leptos_narrowing_down(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, || {
let sigs = (0..1000).map(|n| create_signal(n)).collect::<Vec<_>>();
create_scope(runtime, |cx| {
let sigs =
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(move |_| {
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
@@ -77,10 +78,10 @@ fn leptos_fanning_out(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, || {
let sig = create_rw_signal(0);
create_scope(runtime, |cx| {
let sig = create_rw_signal(cx, 0);
let memos = (0..1000)
.map(|_| create_memo(move |_| sig.get()))
.map(|_| create_memo(cx, move |_| sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
@@ -98,16 +99,17 @@ fn leptos_narrowing_update(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, || {
create_scope(runtime, |cx| {
let acc = Rc::new(Cell::new(0));
let sigs = (0..1000).map(|n| create_signal(n)).collect::<Vec<_>>();
let sigs =
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(move |_| {
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
create_isomorphic_effect({
create_isomorphic_effect(cx, {
let acc = Rc::clone(&acc);
move |_| {
acc.set(memo());
@@ -139,9 +141,9 @@ fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
.map(|_| {
create_scope(runtime, {
let acc = Rc::clone(&acc);
move || {
let (r, w) = create_signal(0);
create_isomorphic_effect({
move |cx| {
let (r, w) = create_signal(cx, 0);
create_isomorphic_effect(cx, {
move |_| {
acc.set(r());
}
@@ -161,9 +163,7 @@ fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
#[bench]
fn rs_deep_update(b: &mut Bencher) {
use reactive_signals::{
runtimes::ClientRuntime, signal, types::Func, Scope, Signal,
};
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let sc = ClientRuntime::new_root_scope();
b.iter(|| {
@@ -184,9 +184,7 @@ fn rs_deep_update(b: &mut Bencher) {
#[bench]
fn rs_fanning_out(b: &mut Bencher) {
use reactive_signals::{
runtimes::ClientRuntime, signal, types::Func, Scope, Signal,
};
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let cx = ClientRuntime::new_root_scope();
b.iter(|| {
@@ -202,17 +200,18 @@ fn rs_fanning_out(b: &mut Bencher) {
#[bench]
fn rs_narrowing_update(b: &mut Bencher) {
use reactive_signals::{
runtimes::ClientRuntime, signal, types::Func, Scope, Signal,
};
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let cx = ClientRuntime::new_root_scope();
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let sigs = (0..1000).map(|n| signal!(cx, n)).collect::<Vec<_>>();
let sigs =
(0..1000).map(|n| signal!(cx, n)).collect::<Vec<_>>();
let memo = signal!(cx, {
let sigs = sigs.clone();
move || sigs.iter().map(|r| r.get()).sum::<i32>()
move || {
sigs.iter().map(|r| r.get()).sum::<i32>()
}
});
assert_eq!(memo.get(), 499500);
signal!(cx, {

View File

@@ -7,7 +7,7 @@ fn leptos_ssr_bench(b: &mut Bencher) {
leptos_dom::HydrationCtx::reset_id();
_ = create_scope(create_runtime(), |cx| {
#[component]
fn Counter(initial: i32) -> impl IntoView {
fn Counter(cx: Scope, initial: i32) -> impl IntoView {
let (value, set_value) = create_signal(cx, initial);
view! {
cx,

View File

@@ -1,7 +1,7 @@
pub use leptos::*;
use miniserde::*;
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
use wasm_bindgen::JsCast;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Todos(pub Vec<Todo>);
@@ -9,13 +9,13 @@ pub struct Todos(pub Vec<Todo>);
const STORAGE_KEY: &str = "todos-leptos";
impl Todos {
pub fn new() -> Self {
pub fn new(cx: Scope) -> Self {
Self(vec![])
}
pub fn new_with_1000() -> Self {
pub fn new_with_1000(cx: Scope) -> Self {
let todos = (0..1000)
.map(|id| Todo::new(id, format!("Todo #{id}")))
.map(|id| Todo::new(cx, id, format!("Todo #{id}")))
.collect();
Self(todos)
}
@@ -72,17 +72,13 @@ pub struct Todo {
}
impl Todo {
pub fn new(id: usize, title: String) -> Self {
Self::new_with_completed(id, title, false)
pub fn new(cx: Scope, id: usize, title: String) -> Self {
Self::new_with_completed(cx, id, title, false)
}
pub fn new_with_completed(
id: usize,
title: String,
completed: bool,
) -> Self {
let (title, set_title) = create_signal(title);
let (completed, set_completed) = create_signal(completed);
pub fn new_with_completed(cx: Scope, id: usize, title: String, completed: bool) -> Self {
let (title, set_title) = create_signal(cx, title);
let (completed, set_completed) = create_signal(cx, completed);
Self {
id,
title,
@@ -102,7 +98,7 @@ const ESCAPE_KEY: u32 = 27;
const ENTER_KEY: u32 = 13;
#[component]
pub fn TodoMVC(todos: Todos) -> impl IntoView {
pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
let mut next_id = todos
.0
.iter()
@@ -111,10 +107,10 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
.map(|last| last + 1)
.unwrap_or(0);
let (todos, set_todos) = create_signal(todos);
provide_context(set_todos);
let (todos, set_todos) = create_signal(cx, todos);
provide_context(cx, set_todos);
let (mode, set_mode) = create_signal(Mode::All);
let (mode, set_mode) = create_signal(cx, Mode::All);
let add_todo = move |ev: web_sys::KeyboardEvent| {
let target = event_target::<HtmlInputElement>(&ev);
@@ -124,7 +120,7 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
let title = event_target_value(&ev);
let title = title.trim();
if !title.is_empty() {
let new = Todo::new(next_id, title.to_string());
let new = Todo::new(cx, next_id, title.to_string());
set_todos.update(|t| t.add(new));
next_id += 1;
target.set_value("");
@@ -132,7 +128,7 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
}
};
let filtered_todos = create_memo::<Vec<Todo>>(move |_| {
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
todos.with(|todos| match mode.get() {
Mode::All => todos.0.to_vec(),
Mode::Active => todos
@@ -152,7 +148,7 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
// effect to serialize to JSON
// this does reactive reads, so it will automatically serialize on any relevant change
create_effect(move |_| {
create_effect(cx, move |_| {
if let Ok(Some(storage)) = window().local_storage() {
let objs = todos
.get()
@@ -167,7 +163,7 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
}
});
view! {
view! { cx,
<main>
<section class="todoapp">
<header class="header">
@@ -192,8 +188,8 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
<For
each=filtered_todos
key=|todo| todo.id
view=move |todo: Todo| {
view! { <Todo todo=todo.clone()/> }
view=move |cx, todo: Todo| {
view! { cx, <Todo todo=todo.clone()/> }
}
/>
</ul>
@@ -240,14 +236,14 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
<p>"Part of " <a href="http://todomvc.com">"TodoMVC"</a></p>
</footer>
</main>
}.into_view()
}.into_view(cx)
}
#[component]
pub fn Todo(todo: Todo) -> impl IntoView {
let (editing, set_editing) = create_signal(false);
let set_todos = use_context::<WriteSignal<Todos>>().unwrap();
//let input = NodeRef::new();
pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
let (editing, set_editing) = create_signal(cx, false);
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
//let input = NodeRef::new(cx);
let save = move |value: &str| {
let value = value.trim();
@@ -259,7 +255,7 @@ pub fn Todo(todo: Todo) -> impl IntoView {
set_editing(false);
};
view! {
view! { cx,
<li class="todo" class:editing=editing class:completed=move || (todo.completed)()>
<div class="view">
<input class="toggle" type="checkbox" prop:checked=move || (todo.completed)()/>
@@ -272,7 +268,7 @@ pub fn Todo(todo: Todo) -> impl IntoView {
{move || {
editing()
.then(|| {
view! {
view! { cx,
<input
class="edit"
class:hidden=move || !(editing)()
@@ -323,8 +319,8 @@ pub struct TodoSerialized {
}
impl TodoSerialized {
pub fn into_todo(self, ) -> Todo {
Todo::new_with_completed(self.id, self.title, self.completed)
pub fn into_todo(self, cx: Scope) -> Todo {
Todo::new_with_completed(cx, self.id, self.title, self.completed)
}
}

View File

@@ -12,8 +12,8 @@ fn leptos_todomvc_ssr(b: &mut Bencher) {
b.iter(|| {
use crate::todomvc::leptos::*;
let html = ::leptos::ssr::render_to_string(|| {
view! { <TodoMVC todos=Todos::new()/> }
let html = ::leptos::ssr::render_to_string(|cx| {
view! { cx, <TodoMVC todos=Todos::new(cx)/> }
});
assert!(html.len() > 1);
});

View File

@@ -65,7 +65,7 @@ And add a simple “Hello, world!” to your `main.rs`
use leptos::*;
fn main() {
mount_to_body(|| view! { <p>"Hello, world!"</p> })
mount_to_body(|| view! { <p>"Hello, world!"</p> })
}
```

View File

@@ -367,6 +367,7 @@ fn GlobalStateInput() -> impl IntoView {
// that we created in the other component
// neither of them will cause the other to rerun
let (name, set_name) = create_slice(
// we take a slice *from* `state`
state,
// our getter returns a "slice" of the data

View File

@@ -12,7 +12,6 @@
- [Error Handling](./view/07_errors.md)
- [Parent-Child Communication](./view/08_parent_child.md)
- [Passing Children to Components](./view/09_component_children.md)
- [No Macros: The View Builder Syntax](./view/builder.md)
- [Reactivity](./reactivity/README.md)
- [Working with Signals](./reactivity/working_with_signals.md)
- [Responding to Changes with `create_effect`](./reactivity/14_create_effect.md)

View File

@@ -235,9 +235,9 @@ At the very most, you might consider memoizing the final node before running som
```rust
let text = create_memo(move |_| {
d()
d()
});
create_effect(move |_| {
engrave_text_into_bar_of_gold(&text());
engrave_text_into_bar_of_gold(&text());
});
```

View File

@@ -18,7 +18,7 @@ let async_data = create_resource(
count,
// every time `count` changes, this will run
|value| async move {
logging::log!("loading data from API");
log!("loading data from API");
load_data(value).await
},
);

View File

@@ -4,7 +4,7 @@ In the previous chapter, we showed how you can create a simple loading screen to
```rust
let (count, set_count) = create_signal(0);
let once = create_resource(count, |count| async move { load_a(count).await });
let a = create_resource(count, |count| async move { load_a(count).await });
view! {
<h1>"My Data"</h1>

View File

@@ -54,7 +54,7 @@ RUN cargo leptos build --release -vv
FROM rustlang/rust:nightly-bullseye as runner
# Copy the server binary to the /app directory
COPY --from=builder /app/target/server/release/leptos_start /app/
COPY --from=builder /app/target/server/release/leptos_website /app/
# /target/site contains our JS/WASM/CSS, etc.
COPY --from=builder /app/target/site /app/site
# Copy Cargo.toml if its needed at runtime
@@ -68,7 +68,7 @@ ENV LEPTOS_SITE_ADDR="0.0.0.0:8080"
ENV LEPTOS_SITE_ROOT="site"
EXPOSE 8080
# Run the server
CMD ["/app/leptos_start"]
CMD ["/app/leptos_website"]
```
> Read more: [`gnu` and `musl` build files for Leptos apps](https://github.com/leptos-rs/leptos/issues/1152#issuecomment-1634916088).

View File

@@ -50,6 +50,7 @@ If you want to really understand the issue here, it may help to look at the expa
```rust
Suspense(
::leptos::component_props_builder(&Suspense)
.fallback(|| ())
.children({
@@ -60,6 +61,7 @@ Suspense(
leptos::Fragment::lazy(|| {
vec![
(Show(
::leptos::component_props_builder(&Show)
.when(|| true)
// but fallback is moved into Show here

View File

@@ -50,6 +50,7 @@ let (use_last, set_use_last) = create_signal(true);
// any time one of the source signals changes
create_effect(move |_| {
log(
if use_last() {
format!("{} {}", first(), last())
} else {
@@ -121,6 +122,7 @@ Like `create_resource`, `watch` takes a first argument, which is reactively trac
let (num, set_num) = create_signal(0);
let stop = watch(
move || num.get(),
move |num, prev_num, _| {
log::debug!("Number: {}; Prev: {:?}", num, prev_num);

View File

@@ -20,7 +20,7 @@ let text = move || if count_is_odd() {
// an effect automatically tracks the signals it depends on
// and reruns when they change
create_effect(move |_| {
logging::log!("text = {}", text());
log!("text = {}", text());
});
view! {

View File

@@ -16,7 +16,7 @@ Calling a `ReadSignal` as a function is syntax sugar for `.get()`. Calling a `Wr
```rust
let (count, set_count) = create_signal(0);
set_count(1);
logging::log!(count());
log!(count());
```
is the same as
@@ -24,7 +24,7 @@ is the same as
```rust
let (count, set_count) = create_signal(0);
set_count.set(1);
logging::log!(count.get());
log!(count.get());
```
You might notice that `.get()` and `.set()` can be implemented in terms of `.with()` and `.update()`. In other words, `count.get()` is identical with `count.with(|n| n.clone())`, and `count.set(1)` is implemented by doing `count.update(|n| *n = 1)`.
@@ -63,7 +63,7 @@ if names.with(Vec::is_empty) {
}
```
After all, `.with()` simply takes a function that takes the value by reference. Since `Vec::is_empty` takes `&self`, we can pass it in directly and avoid the unnecessary closure.
After all, `.with()` simply takes a function that takes the value by reference. Since `Vec::is_empty` takes `&self`, we can pass it in directly and avoid the unncessary closure.
## Making signals depend on each other

View File

@@ -33,6 +33,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
#[component]
pub fn BusyButton() -> impl IntoView {
view! {
<button on:click=move |_| {
spawn_local(async {
add_todo("So much to do!".to_string()).await;
@@ -69,18 +70,6 @@ There are a few things to note about the way you define a server function, too.
- We provide the macro a path. This is a prefix for the path at which well mount a server function handler on our server. (See examples for [Actix](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/main.rs#L44) and [Axum](https://github.com/leptos-rs/leptos/blob/598523cd9d0d775b017cb721e41ebae9349f01e2/examples/todo_app_sqlite_axum/src/main.rs#L51).)
- Youll need to have `serde` as a dependency with the `derive` featured enabled for the macro to work properly. You can easily add it to `Cargo.toml` with `cargo add serde --features=derive`.
## Server Function URL Prefixes
You can optionally define a specific URL prefix to be used in the definition of the server function.
This is done by providing an optional 2nd argument to the `#[server]` macro.
By default the URL prefix will be `/api`, if not specified.
Here are some examples:
```rust
#[server(AddTodo)] // will use the default URL prefix of `/api`
#[server(AddTodo, "/foo")] // will use the URL prefix of `/foo`
```
## Server Function Encodings
By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body of the request. (This means that server functions can be called from HTML forms, which well see in a future chapter.) But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]` macro to specify an alternate encoding:
@@ -116,21 +105,6 @@ In other words, you have two choices:
>
> The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that didnt support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of your app is not available.
## Server Functions Endpoint Paths
By default, a unique path will be generated. You can optionally define a specific endpoint path to be used in the URL. This is done by providing an optional 4th argument to the `#[server]` macro. Leptos will generate the complete path by concatenating the URL prefix (2nd argument) and the endpoint path (4th argument).
For example,
```rust
#[server(MyServerFnType, "/api", "Url", "hello")]
```
will generate a server function endpoint at `/api/hello` that accepts a POST request.
> **Can I use the same server function endpoint path with multiple encodings?**
>
> No. Different server functions must have unique paths. The `#[server]` macro automatically generates unique paths, but you need to be careful if you choose to specify the complete path manually, as the server looks up server functions by their path.
## An Important Note on Security
Server functions are a cool technology, but its very important to remember. **Server functions are not magic; theyre syntax sugar for defining a public API.** The _body_ of a server function is never made public; its just part of your server binary. But the server function is a publicly accessible API endpoint, and its return value is just a JSON or similar blob. You should _never_ return something sensitive from a server function.

View File

@@ -35,6 +35,7 @@ Heres a simplified example from our [`session_auth_axum` example](https://git
```rust
#[server(Login, "/api")]
pub async fn login(
username: String,
password: String,
remember: Option<String>,

View File

@@ -9,7 +9,7 @@ Put a log somewhere in your root component. (I usually call mine `<App/>`, but a
```rust
#[component]
pub fn App() -> impl IntoView {
logging::log!("where do I run?");
leptos::log!("where do I run?");
// ... whatever
}
```
@@ -166,7 +166,7 @@ For example, say that I want to store something in the browsers `localStorage
pub fn App() -> impl IntoView {
use gloo_storage::Storage;
let storage = gloo_storage::LocalStorage::raw();
logging::log!("{storage:?}");
leptos::log!("{storage:?}");
}
```
@@ -180,7 +180,7 @@ pub fn App() -> impl IntoView {
use gloo_storage::Storage;
create_effect(move |_| {
let storage = gloo_storage::LocalStorage::raw();
logging::log!("{storage:?}");
leptos::log!("{storage:?}");
});
}
```

View File

@@ -37,7 +37,7 @@ impl Todos {
#[cfg(test)]
mod tests {
#[test]
fn test_remaining() {
fn test_remaining {
// ...
}
}

View File

@@ -145,7 +145,7 @@ Derived signals let you create reactive computed values that can be used in mult
places in your application with minimal overhead.
Note: Using a derived signal like this means that the calculation runs once per
signal change and once per place we access `double_count`; in other words, twice. This is a
signal change per place we access `double_count`; in other words, twice. This is a
very cheap calculation, so thats fine. Well look at memos in a later chapter, which
are designed to solve this problem for expensive calculations.

View File

@@ -35,7 +35,9 @@ Instead, lets create a `<ProgressBar/>` component.
```rust
#[component]
fn ProgressBar() -> impl IntoView {
fn ProgressBar(
) -> impl IntoView {
view! {
<progress
max="50"
@@ -62,6 +64,7 @@ In Leptos, you define props by giving additional arguments to the component func
```rust
#[component]
fn ProgressBar(
progress: ReadSignal<i32>
) -> impl IntoView {
view! {
@@ -115,6 +118,7 @@ argument to the component function with `#[prop(optional)]`.
```rust
#[component]
fn ProgressBar(
// mark this prop optional
// you can specify it or not when you use <ProgressBar/>
#[prop(optional)]
@@ -145,6 +149,7 @@ with `#[prop(default = ...)`.
```rust
#[component]
fn ProgressBar(
#[prop(default = 100)]
max: u16,
progress: ReadSignal<i32>
@@ -194,6 +199,7 @@ implement the trait `Fn() -> i32`. So you could use a generic component:
```rust
#[component]
fn ProgressBar<F>(
#[prop(default = 100)]
max: u16,
progress: F
@@ -248,6 +254,7 @@ reactive value.
```rust
#[component]
fn ProgressBar(
#[prop(default = 100)]
max: u16,
#[prop(into)]
@@ -366,6 +373,7 @@ component function, and each one of the props:
/// Shows progress toward a goal.
#[component]
fn ProgressBar(
/// The maximum value of the progress bar.
#[prop(default = 100)]
max: u16,

View File

@@ -148,10 +148,10 @@ This _works_, for sure. But if you added a log, you might be surprised
```rust
let message = move || if value() > 5 {
logging::log!("{}: rendering Big", value());
log!("{}: rendering Big", value());
"Big"
} else {
logging::log!("{}: rendering Small", value());
log!("{}: rendering Small", value());
"Small"
};
```

View File

@@ -72,7 +72,10 @@ pub fn App() -> impl IntoView {
#[component]
pub fn ButtonB<F>(on_click: F) -> impl IntoView
pub fn ButtonB<F>(
on_click: F,
) -> impl IntoView
where
F: Fn(MouseEvent) + 'static,
{

View File

@@ -47,6 +47,7 @@ Lets define a component that takes some children and a render prop.
```rust
#[component]
pub fn TakesChildren<F, IV>(
/// Takes a function (type F) that returns anything that can be
/// converted into a View (type IV)
render_prop: F,

View File

@@ -1,98 +0,0 @@
# No Macros: The View Builder Syntax
> If youre perfectly happy with the `view!` macro syntax described so far, youre welcome to skip this chapter. The builder syntax described in this section is always available, but never required.
For one reason or another, many developers would prefer to avoid macros. Perhaps you dont like the limited `rustfmt` support. (Although, you should check out [`leptosfmt`](https://github.com/bram209/leptosfmt), which is an excellent tool!) Perhaps you worry about the effect of macros on compile time. Perhaps you prefer the aesthetics of pure Rust syntax, or you have trouble context-switching between an HTML-like syntax and your Rust code. Or perhaps you want more flexibility in how you create and manipulate HTML elements than the `view` macro provides.
If you fall into any of those camps, the builder syntax may be for you.
The `view` macro expands an HTML-like syntax to a series of Rust functions and method calls. If youd rather not use the `view` macro, you can simply use that expanded syntax yourself. And its actually pretty nice!
First off, if you want you can even drop the `#[component]` macro: a component is just a setup function that creates your view, so you can define a component as a simple function call:
```rust
pub fn counter(initial_value: i32, step: u32) -> impl IntoView { }
```
Elements are created by calling a function with the same name as the HTML element:
```rust
p()
```
You can add children to the element with [`.child()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.child), which takes a single child or a tuple or array of types that implement [`IntoView`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html).
```rust
p().child((em().child("Big, "), strong().child("bold "), "text"))
```
Attributes are added with [`.attr()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.attr). This can take any of the same types that you could pass as an attribute into the view macro (types that implement [`IntoAttribute`](https://docs.rs/leptos/latest/leptos/trait.IntoAttribute.html)).
```rust
p().attr("id", "foo").attr("data-count", move || count().to_string())
```
Similarly, the `class:`, `prop:`, and `style:` syntaxes map directly onto [`.class()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.class), [`.prop()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.prop), and [`.style()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.style) methods.
Event listeners can be added with [`.on()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.on). Typed events found in [`leptos::ev`](https://docs.rs/leptos/latest/leptos/ev/index.html) prevent typos in event names and allow for correct type inference in the callback function.
```rust
button()
.on(ev::click, move |_| set_count.update(|count| count.clear()))
.child("Clear")
```
> Many additional methods can be found in the [`HtmlElement`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.child) docs, including some methods that are not directly available in the `view` macro.
All of this adds up to a very Rusty syntax to build full-featured views, if you prefer this style.
```rust
/// A simple counter view.
// A component is really just a function call: it runs once to create the DOM and reactive system
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
let (count, set_count) = create_signal(0);
div()
.child((
button()
// typed events found in leptos::ev
// 1) prevent typos in event names
// 2) allow for correct type inference in callbacks
.on(ev::click, move |_| set_count.update(|count| count.clear()))
.child("Clear"),
button()
.on(ev::click, move |_| {
set_count.update(|count| count.decrease())
})
.child("-1"),
span().child(("Value: ", move || count.get().value(), "!")),
button()
.on(ev::click, move |_| {
set_count.update(|count| count.increase())
})
.child("+1"),
))
}
```
This also has the benefit of being more flexible: because these are all plain Rust functions and methods, its easier to use them in things like iterator adapters without any additional “magic”:
```rust
// take some set of attribute names and values
let attrs: Vec<(&str, AttributeValue)> = todo!();
// you can use the builder syntax to “spread” these onto the
// element in a way thats not possible with the view macro
let p = attrs
.into_iter()
.fold(p(), |el, (name, value)| el.attr(name, value));
```
> ## Performance Note
>
> One caveat: the `view` macro applies significant optimizations in server-side-rendering (SSR) mode to improve HTML rendering performance significantly (think 2-4x faster, depending on the characteristics of any given app). It does this by analyzing your `view` at compile time and converting the static parts into simple HTML strings, rather than expanding them into the builder syntax.
>
> This means two things:
>
> 1. The builder syntax and `view` macro should not be mixed, or should only be mixed very carefully: at least in SSR mode, the output of the `view` should be treated as a “black box” that cant have additional builder methods applied to it without causing inconsistencies.
> 2. Using the builder syntax will result in less-than-optimal SSR performance. It wont be slow, by any means (and its worth running your own benchmarks in any case), just slower than the `view`-optimized version.

View File

@@ -49,12 +49,10 @@ jq -R -s -c 'split("\n")[:-1]')
echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
'''
[tasks.test-report]
[tasks.test-runner-report]
workspace = false
description = "report web testing technology used by examples - OPTION: [all]"
description = "report ci test runners for each example - OPTION: [all]"
script = '''
set -emu
BOLD="\e[1m"
GREEN="\e[0;32m"
ITALIC="\e[3m"
@@ -62,10 +60,11 @@ YELLOW="\e[0;33m"
RESET="\e[0m"
echo
echo "${YELLOW}Web Test Technology${RESET}"
echo "${YELLOW}Test Runner Report${RESET}"
echo "${ITALIC}Pass the option \"all\" to show all the examples${RESET}"
echo
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' -not -path '*/node_modules/*' |
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' |
sed 's%./%%' |
sed 's%/Makefile.toml%%' |
grep -v Makefile.toml |
@@ -76,78 +75,38 @@ start_path=$(pwd)
for path in $makefile_paths; do
cd $path
crate_symbols=
test_runner=
pw_count=$(find . -name playwright.config.ts | wc -l)
while read -r line; do
case $line in
*"cucumber"*)
crate_symbols=$crate_symbols"C"
;;
*"fantoccini"*)
crate_symbols=$crate_symbols"D"
;;
esac
done <"./Cargo.toml"
test_count=$(grep -rl -E "#\[(test|rstest)\]" | wc -l)
if [ $test_count -gt 0 ]; then
test_runner="-C"
fi
while read -r line; do
case $line in
*"cargo-make/wasm-test.toml"*)
crate_symbols=$crate_symbols"W"
*"wasm-test.toml"*)
test_runner=$test_runner"-W"
;;
*"cargo-make/playwright-test.toml"*)
crate_symbols=$crate_symbols"P"
crate_symbols=$crate_symbols"N"
*"playwright-test.toml"*)
test_runner=$test_runner"-P"
;;
*"cargo-make/playwright-trunk-test.toml"*)
crate_symbols=$crate_symbols"P"
crate_symbols=$crate_symbols"T"
;;
*"cargo-make/trunk_server.toml"*)
crate_symbols=$crate_symbols"T"
;;
*"cargo-make/cargo-leptos-webdriver-test.toml"*)
crate_symbols=$crate_symbols"L"
;;
*"cargo-make/cargo-leptos-test.toml"*)
crate_symbols=$crate_symbols"L"
if [ $pw_count -gt 0 ]; then
crate_symbols=$crate_symbols"P"
fi
*"cargo-leptos-test.toml"*)
test_runner=$test_runner"-L"
;;
esac
done <"./Makefile.toml"
# Sort list of tools
sorted_crate_symbols=$(echo ${crate_symbols} | grep -o . | sort | tr -d "\n")
formatted_crate_symbols="${BOLD}${YELLOW}${sorted_crate_symbols}${RESET}"
crate_line=$path
if [ ! -z ${1+x} ]; then
if [ ! -z "$1" ]; then
# Show all examples
if [ ! -z $crate_symbols ]; then
crate_line=$crate_line$formatted_crate_symbols
fi
echo $crate_line
elif [ ! -z $crate_symbols ]; then
echo "$path ${BOLD}${test_runner}${RESET}"
elif [ ! -z $test_runner ]; then
# Filter out examples that do not run tests in `ci`
crate_line=$crate_line$formatted_crate_symbols
echo $crate_line
echo "$path ${BOLD}${test_runner}${RESET}"
fi
cd ${start_path}
done
c="${BOLD}${YELLOW}C${RESET} = Cucumber"
d="${BOLD}${YELLOW}D${RESET} = WebDriver"
l="${BOLD}${YELLOW}L${RESET} = Cargo Leptos"
n="${BOLD}${YELLOW}N${RESET} = Node"
p="${BOLD}${YELLOW}P${RESET} = Playwright"
t="${BOLD}${YELLOW}T${RESET} = Trunk"
w="${BOLD}${YELLOW}W${RESET} = WASM"
echo
echo "${ITALIC}Keys:${RESET} $c, $d, $l, $n, $p, $t, $w"
echo "${ITALIC}Runners: C = Cargo Test, L = Cargo Leptos Test, P = Playwright Test, W = WASM Test${RESET}"
echo
'''

View File

@@ -4,4 +4,4 @@ The examples in this directory are all built and tested against the current `mai
To the extent that new features have been released or breaking changes have been made since the previous release, the examples are compatible with the `main` branch and not the current release.
To see the examples as they were at the time of the `0.3.0` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.3.0/examples).
To see the examples as they were at the time of the `0.3.0` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.3.0/examples).

View File

@@ -2,3 +2,7 @@ extend = { path = "./cargo-leptos.toml" }
[tasks.integration-test]
dependencies = ["install-cargo-leptos", "cargo-leptos-e2e"]
[tasks.cargo-leptos-e2e]
command = "cargo"
args = ["leptos", "end-to-end"]

View File

@@ -1,7 +0,0 @@
extend = [
{ path = "./cargo-leptos.toml" },
{ path = "../cargo-make/webdriver.toml" },
]
[tasks.integration-test]
dependencies = ["install-cargo-leptos", "start-webdriver", "cargo-leptos-e2e"]

View File

@@ -1,12 +1,6 @@
extend = { path = "../cargo-make/client-process.toml" }
[tasks.install-cargo-leptos]
install_crate = { crate_name = "cargo-leptos", binary = "cargo-leptos", test_arg = "--help" }
[tasks.cargo-leptos-e2e]
command = "cargo"
args = ["leptos", "end-to-end"]
[tasks.build]
clear = true
command = "cargo"
@@ -31,3 +25,31 @@ install_crate = "cargo-all-features"
[tasks.start-client]
command = "cargo"
args = ["leptos", "watch"]
[tasks.stop-client]
condition = { env_set = ["APP_PROCESS_NAME"] }
script = '''
if [ ! -z $(pidof ${APP_PROCESS_NAME}) ]; then
pkill -f todo_app_sqlite
fi
if [ ! -z $(pidof ${APP_PROCESS_NAME}) ]; then
pkill -f cargo-leptos
fi
'''
[tasks.client-status]
condition = { env_set = ["APP_PROCESS_NAME"] }
script = '''
if [ -z $(pidof ${APP_PROCESS_NAME}) ]; then
echo " ${APP_PROCESS_NAME} is not running"
else
echo " ${APP_PROCESS_NAME} is up"
fi
if [ -z $(pidof cargo-leptos) ]; then
echo " cargo-leptos is not running"
else
echo " cargo-leptos is up"
fi
'''

View File

@@ -1,19 +1,18 @@
[tasks.clean]
dependencies = [
"clean-cargo",
"clean-trunk",
"clean-node_modules",
"clean-playwright",
"clean-cargo",
"clean-trunk",
"clean-node_modules",
"clean-playwright",
]
[tasks.clean-cargo]
command = "rm"
args = ["-rf", "target"]
command = "cargo"
args = ["clean"]
[tasks.clean-trunk]
script = '''
find . -type d -name dist | xargs rm -rf
'''
command = "trunk"
args = ["clean"]
[tasks.clean-node_modules]
script = '''

View File

@@ -1,35 +0,0 @@
[tasks.start-client]
[tasks.stop-client]
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
script = '''
if [ ! -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
pkill -ef ${CLIENT_PROCESS_NAME}
fi
'''
[tasks.client-status]
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
script = '''
if [ -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
echo " ${CLIENT_PROCESS_NAME} is not running"
else
echo " ${CLIENT_PROCESS_NAME} is up"
fi
'''
[tasks.maybe-start-client]
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
script = '''
if [ -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
echo " Starting ${CLIENT_PROCESS_NAME}"
cargo make start-client ${@} &
else
echo " ${CLIENT_PROCESS_NAME} is already started"
fi
'''
# ALIASES
[tasks.dev]
alias = "maybe-start-client"

View File

@@ -1,17 +0,0 @@
extend = [
{ path = "../cargo-make/playwright.toml" },
{ path = "../cargo-make/trunk_server.toml" },
]
[tasks.integration-test]
dependencies = [
"maybe-start-client",
"wait-one",
"test-playwright",
"stop-client",
]
[tasks.wait-one]
script = '''
sleep 1
'''

View File

@@ -1,12 +1,18 @@
extend = { path = "../cargo-make/client-process.toml" }
[env]
CLIENT_PROCESS_NAME = "trunk"
[tasks.build]
command = "trunk"
args = ["build"]
[tasks.start-client]
[tasks.start-trunk]
command = "trunk"
args = ["serve", "${@}"]
[tasks.stop-trunk]
script = '''
pkill -f "cargo-make"
pkill -f "trunk"
'''
# ALIASES
[tasks.dev]
dependencies = ["start-trunk"]

View File

@@ -1,5 +1,4 @@
use leptos::{SignalWrite, *};
use std::cell::{Ref, RefMut};
use leptos::*;
/// A simple counter component.
///
@@ -13,20 +12,12 @@ pub fn SimpleCounter(
) -> impl IntoView {
let (value, set_value) = create_signal(initial_value);
let something: Ref<'_, i32> = value.read();
spawn_local(async move {
let mut something_else: RefMut<'_, i32> = set_value.write();
async {}.await;
*something_else = 30;
});
view! {
<div>
<button on:click=move |_| set_value(0)>"Clear"</button>
<button on:click=move |_| *set_value.write() -= step>"-1"</button>
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
<span>"Value: " {value} "!"</span>
<button on:click=move |_| *set_value.write() += step>"+1"</button>
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>
</div>
}
}

View File

@@ -15,12 +15,13 @@ cfg_if! {
}
}
#[server]
// "/api" is an optional prefix that allows you to locate server functions wherever you'd like on the server
#[server(GetServerCount, "/api")]
pub async fn get_server_count() -> Result<i32, ServerFnError> {
Ok(COUNT.load(Ordering::Relaxed))
}
#[server]
#[server(AdjustServerCount, "/api")]
pub async fn adjust_server_count(
delta: i32,
msg: String,
@@ -32,7 +33,7 @@ pub async fn adjust_server_count(
Ok(new)
}
#[server]
#[server(ClearServerCount, "/api")]
pub async fn clear_server_count() -> Result<i32, ServerFnError> {
COUNT.store(0, Ordering::Relaxed);
_ = COUNT_CHANNEL.send(&0).await;
@@ -146,8 +147,6 @@ pub fn Counter() -> impl IntoView {
// but uses HTML forms to submit the actions
#[component]
pub fn FormCounter() -> impl IntoView {
// these struct names are auto-generated by #[server]
// they are just the PascalCased versions of the function names
let adjust = create_server_action::<AdjustServerCount>();
let clear = create_server_action::<ClearServerCount>();

View File

@@ -3,8 +3,9 @@ use leptos::{ev, html::*, *};
/// A simple counter view.
// A component is really just a function call: it runs once to create the DOM and reactive system
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
let count = RwSignal::new(Count::new(initial_value, step));
let (count, set_count) = create_signal(Count::new(initial_value, step));
// elements are created by calling a function with a Scope argument
// the function name is the same as the HTML tag name
div()
// children can be added with .child()
@@ -17,16 +18,27 @@ pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
// typed events found in leptos::ev
// 1) prevent typos in event names
// 2) allow for correct type inference in callbacks
.on(ev::click, move |_| count.update(Count::clear))
.on(ev::click, move |_| set_count.update(|count| count.clear()))
.child("Clear"),
button()
.on(ev::click, move |_| count.update(Count::decrease))
.on(ev::click, move |_| {
set_count.update(|count| count.decrease())
})
.child("-1"),
span().child(("Value: ", move || count.get().value(), "!")),
button()
.on(ev::click, move |_| count.update(Count::increase))
.child("+1"),
span()
.child("Value: ")
// reactive values are passed to .child() as a tuple
// (Scope, [child function]) so an effect can be created
.child(move || count.get().value())
.child("!"),
))
.child(
button()
.on(ev::click, move |_| {
set_count.update(|count| count.increase())
})
.child("+1"),
)
}
#[derive(Debug, Clone)]

View File

@@ -5,15 +5,12 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["csr"] }
leptos_meta = { path = "../../meta", features = ["csr"] }
log = "0.4"
console_log = "1"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen = "0.2.87"
wasm-bindgen-test = "0.3.37"
pretty_assertions = "1.4.0"
wasm-bindgen-test = "0.3.0"
[dev-dependencies.web-sys]
features = [

View File

@@ -1,6 +1,5 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/wasm-test.toml" },
{ path = "../cargo-make/trunk_server.toml" },
{ path = "../cargo-make/playwright-test.toml" },
]

View File

@@ -1,7 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs />
</head>
<body></body>
</html>
<head>
<title>Counters (Stable)</title>
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
</head>
<body></body>
</html>

View File

@@ -1,106 +0,0 @@
use leptos::*;
use leptos_meta::*;
const MANY_COUNTERS: usize = 1000;
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
#[derive(Copy, Clone)]
struct CounterUpdater {
set_counters: WriteSignal<CounterHolder>,
}
#[component]
pub fn Counters() -> impl IntoView {
let (next_counter_id, set_next_counter_id) = create_signal(0);
let (counters, set_counters) = create_signal::<CounterHolder>(vec![]);
provide_context(CounterUpdater { set_counters });
let add_counter = move |_| {
let id = next_counter_id.get();
let sig = create_signal(0);
set_counters.update(move |counters| counters.push((id, sig)));
set_next_counter_id.update(|id| *id += 1);
};
let add_many_counters = move |_| {
let next_id = next_counter_id.get();
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
let signal = create_signal(0);
(id, signal)
});
set_counters.update(move |counters| counters.extend(new_counters));
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
};
let clear_counters = move |_| {
set_counters.update(|counters| counters.clear());
};
view! {
<Title text="Counters (Stable)" />
<div>
<button on:click=add_counter>
"Add Counter"
</button>
<button on:click=add_many_counters>
{format!("Add {MANY_COUNTERS} Counters")}
</button>
<button on:click=clear_counters>
"Clear Counters"
</button>
<p>
"Total: "
<span data-testid="total">{move ||
counters.get()
.iter()
.map(|(_, (count, _))| count.get())
.sum::<i32>()
.to_string()
}</span>
" from "
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
" counters."
</p>
<ul>
<For
each={move || counters.get()}
key={|counter| counter.0}
view=move |(id, (value, set_value))| {
view! {
<Counter id value set_value/>
}
}
/>
</ul>
</div>
}
}
#[component]
fn Counter(
id: usize,
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context().unwrap();
let input = move |ev| {
set_value
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
};
view! {
<li>
<button data-testid="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
<input data-testid="counter_input" type="text"
prop:value={move || value.get().to_string()}
on:input=input
/>
<span>{value}</span>
<button data-testid="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
<button data-testid="remove_counter" on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
</li>
}
}

View File

@@ -1,4 +1,3 @@
use counters_stable::Counters;
use leptos::*;
fn main() {
@@ -6,3 +5,106 @@ fn main() {
console_error_panic_hook::set_once();
mount_to_body(|| view! { <Counters/> })
}
const MANY_COUNTERS: usize = 1000;
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
#[derive(Copy, Clone)]
struct CounterUpdater {
set_counters: WriteSignal<CounterHolder>,
}
#[component]
pub fn Counters() -> impl IntoView {
let (next_counter_id, set_next_counter_id) = create_signal(0);
let (counters, set_counters) = create_signal::<CounterHolder>(vec![]);
provide_context(CounterUpdater { set_counters });
let add_counter = move |_| {
let id = next_counter_id.get();
let sig = create_signal(0);
set_counters.update(move |counters| counters.push((id, sig)));
set_next_counter_id.update(|id| *id += 1);
};
let add_many_counters = move |_| {
let next_id = next_counter_id.get();
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
let signal = create_signal(0);
(id, signal)
});
set_counters.update(move |counters| counters.extend(new_counters));
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
};
let clear_counters = move |_| {
set_counters.update(|counters| counters.clear());
};
view! {
<div>
<button on:click=add_counter>
"Add Counter"
</button>
<button on:click=add_many_counters>
{format!("Add {MANY_COUNTERS} Counters")}
</button>
<button on:click=clear_counters>
"Clear Counters"
</button>
<p>
"Total: "
<span data-testid="total">{move ||
counters.get()
.iter()
.map(|(_, (count, _))| count.get())
.sum::<i32>()
.to_string()
}</span>
" from "
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
" counters."
</p>
<ul>
<For
each={move || counters.get()}
key={|counter| counter.0}
view=move |(id, (value, set_value))| {
view! {
<Counter id value set_value/>
}
}
/>
</ul>
</div>
}
}
#[component]
fn Counter(
id: usize,
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context().unwrap();
let input = move |ev| {
set_value
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
};
view! {
<li>
<button id="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
<input type="text"
prop:value={move || value.get().to_string()}
on:input=input
/>
<span>{value}</span>
<button id="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
<button on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
</li>
}
}

View File

@@ -1,17 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_increase_the_number_of_counters() {
// Given
ui::view_counters();
// When
ui::add_1k_counters();
ui::add_1k_counters();
ui::add_1k_counters();
// Then
assert_eq!(ui::counters(), 3000);
}

View File

@@ -1,17 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_increase_the_number_of_counters() {
// Given
ui::view_counters();
// When
ui::add_counter();
ui::add_counter();
ui::add_counter();
// Then
assert_eq!(ui::counters(), 3);
}

View File

@@ -1,19 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_reset_the_counts() {
// Given
ui::view_counters();
ui::add_counter();
ui::add_counter();
ui::add_counter();
// When
ui::clear_counters();
// Then
assert_eq!(ui::total(), 0);
assert_eq!(ui::counters(), 0);
}

View File

@@ -1,18 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_decrease_the_total_count() {
// Given
ui::view_counters();
ui::add_counter();
// When
ui::decrement_counter(1);
ui::decrement_counter(1);
ui::decrement_counter(1);
// Then
assert_eq!(ui::total(), -3);
}

View File

@@ -1,112 +0,0 @@
use counters_stable::Counters;
use leptos::*;
use wasm_bindgen::JsCast;
use web_sys::{Element, Event, EventInit, HtmlElement, HtmlInputElement};
// Actions
pub fn add_1k_counters() {
find_by_text("Add 1000 Counters").click();
}
pub fn add_counter() {
find_by_text("Add Counter").click();
}
pub fn clear_counters() {
find_by_text("Clear Counters").click();
}
pub fn decrement_counter(index: u32) {
counter_html_element(index, "decrement_count").click();
}
pub fn enter_count(index: u32, count: i32) {
let input = counter_input_element(index, "counter_input");
input.set_value(count.to_string().as_str());
let mut event_init = EventInit::new();
event_init.bubbles(true);
let event = Event::new_with_event_init_dict("input", &event_init).unwrap();
input.dispatch_event(&event).unwrap();
}
pub fn increment_counter(index: u32) {
counter_html_element(index, "increment_count").click();
}
pub fn remove_counter(index: u32) {
counter_html_element(index, "remove_counter").click();
}
pub fn view_counters() {
remove_existing_counters();
mount_to_body(|| view! { <Counters/> });
}
// Results
pub fn counters() -> i32 {
data_test_id("counters").parse::<i32>().unwrap()
}
pub fn title() -> String {
leptos::document().title()
}
pub fn total() -> i32 {
data_test_id("total").parse::<i32>().unwrap()
}
// Internal
fn counter_element(index: u32, text: &str) -> Element {
let selector =
format!("li:nth-child({}) [data-testid=\"{}\"]", index, text);
leptos::document()
.query_selector(&selector)
.unwrap()
.unwrap()
}
fn counter_html_element(index: u32, text: &str) -> HtmlElement {
counter_element(index, text)
.dyn_into::<HtmlElement>()
.unwrap()
}
fn counter_input_element(index: u32, text: &str) -> HtmlInputElement {
counter_element(index, text)
.dyn_into::<HtmlInputElement>()
.unwrap()
}
fn data_test_id(id: &str) -> String {
let selector = format!("[data-testid=\"{}\"]", id);
leptos::document()
.query_selector(&selector)
.unwrap()
.expect("counters not found")
.text_content()
.unwrap()
}
fn find_by_text(text: &str) -> HtmlElement {
let xpath = format!("//*[text()='{}']", text);
let document = leptos::document();
document
.evaluate(&xpath, &document)
.unwrap()
.iterate_next()
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
}
fn remove_existing_counters() {
if let Some(counter) =
leptos::document().query_selector("body div").unwrap()
{
counter.remove();
}
}

View File

@@ -1 +0,0 @@
pub mod counters_page;

View File

@@ -1,18 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_increase_the_total_count() {
// Given
ui::view_counters();
ui::add_counter();
// When
ui::increment_counter(1);
ui::increment_counter(1);
ui::increment_counter(1);
// Then
assert_eq!(ui::total(), 3);
}

View File

@@ -1,16 +0,0 @@
use wasm_bindgen_test::*;
// Test Suites
pub mod add_1k_counters;
pub mod add_counter;
pub mod clear_counters;
pub mod decrement_counter;
pub mod enter_count;
pub mod increment_counter;
pub mod remove_counter;
pub mod view_counters;
pub mod fixtures;
pub use fixtures::*;
wasm_bindgen_test_configure!(run_in_browser);

View File

@@ -1,18 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_decrement_the_number_of_counters() {
// Given
ui::view_counters();
ui::add_counter();
ui::add_counter();
ui::add_counter();
// When
ui::remove_counter(2);
// Then
assert_eq!(ui::counters(), 2);
}

View File

@@ -1,22 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_see_the_initial_counts() {
// When
ui::view_counters();
// Then
assert_eq!(ui::total(), 0);
assert_eq!(ui::counters(), 0);
}
#[wasm_bindgen_test]
fn should_see_the_title() {
// When
ui::view_counters();
// Then
assert_eq!(ui::title(), "Counters (Stable)");
}

View File

@@ -1,4 +1 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/playwright-trunk-test.toml" },
]
extend = [{ path = "../cargo-make/main.toml" }]

View File

@@ -1,4 +0,0 @@
node_modules/
/test-results/
/playwright-report/
/playwright/.cache/

View File

@@ -1,83 +0,0 @@
{
"name": "grip",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "grip",
"devDependencies": {
"@playwright/test": "^1.35.1"
}
},
"node_modules/.pnpm/@playwright+test@1.33.0": {
"extraneous": true
},
"node_modules/.pnpm/@types+node@20.2.1/node_modules/@types/node": {
"version": "20.2.1",
"extraneous": true,
"license": "MIT"
},
"node_modules/.pnpm/playwright-core@1.33.0/node_modules/playwright-core": {
"version": "1.33.0",
"extraneous": true,
"license": "Apache-2.0",
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@playwright/test": {
"version": "1.35.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz",
"integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==",
"dev": true,
"dependencies": {
"@types/node": "*",
"playwright-core": "1.35.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/@types/node": {
"version": "20.3.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz",
"integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==",
"dev": true
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright-core": {
"version": "1.35.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz",
"integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=16"
}
}
}
}

View File

@@ -1,7 +0,0 @@
{
"private": "true",
"scripts": {},
"devDependencies": {
"@playwright/test": "^1.35.1"
}
}

View File

@@ -1,77 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !process.env.DEV,
/* Retry on CI only */
retries: process.env.DEV ? 0 : 2,
/* Opt out of parallel tests on CI. */
workers: process.env.DEV ? 1 : 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [["html", { open: "never" }], ["list"]],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1:8080",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
// {
// name: "firefox",
// use: { ...devices["Desktop Firefox"] },
// },
// {
// name: "webkit",
// use: { ...devices["Desktop Safari"] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: "cd ../ && trunk serve",
// url: "http://127.0.0.1:8080",
// reuseExistingServer: false, //!process.env.CI,
// },
});

View File

@@ -1,23 +0,0 @@
import { test, expect } from "@playwright/test";
import { HomePage } from "./fixtures/home_page";
test.describe("Clear Number", () => {
test("should see the error message", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await ui.clearInput();
await expect(ui.errorMessage).toHaveText("Not a number! Errors: ");
});
test("should see the error list", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await ui.clearInput();
await expect(ui.errorList).toHaveText(
"cannot parse integer from empty string"
);
});
});

View File

@@ -1,17 +0,0 @@
import { test, expect } from "@playwright/test";
import { HomePage } from "./fixtures/home_page";
test.describe("Click Down Arrow", () => {
test("should see the negative number", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await ui.clickDownArrow();
await ui.clickDownArrow();
await ui.clickDownArrow();
await ui.clickDownArrow();
await ui.clickDownArrow();
await expect(ui.successMessage).toHaveText("You entered -5");
});
});

View File

@@ -1,15 +0,0 @@
import { test, expect } from "@playwright/test";
import { HomePage } from "./fixtures/home_page";
test.describe("Click Up Arrow", () => {
test("should see the positive number", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await ui.clickUpArrow();
await ui.clickUpArrow();
await ui.clickUpArrow();
await expect(ui.successMessage).toHaveText("You entered 3");
});
});

View File

@@ -1,56 +0,0 @@
import { expect, Locator, Page } from "@playwright/test";
export class HomePage {
readonly page: Page;
readonly pageTitle: Locator;
readonly numberInput: Locator;
readonly successMessage: Locator;
readonly errorMessage: Locator;
readonly errorList: Locator;
constructor(page: Page) {
this.page = page;
this.pageTitle = page.locator("h1");
this.numberInput = page.getByLabel(
"Type a number (or something that's not a number!)"
);
this.successMessage = page.locator("label p");
this.errorMessage = page.locator("div p");
this.errorList = page.getByRole("list");
}
async goto() {
await this.page.goto("/");
}
async enterNumber(count: string, index: number = 0) {
await Promise.all([
this.numberInput.waitFor(),
this.numberInput.fill(count),
]);
}
async clickUpArrow() {
await Promise.all([
this.numberInput.waitFor(),
this.numberInput.press("ArrowUp"),
]);
}
async clickDownArrow() {
await Promise.all([
this.numberInput.waitFor(),
this.numberInput.press("ArrowDown"),
]);
}
async clearInput() {
await Promise.all([
this.numberInput.waitFor(),
this.clickUpArrow(),
this.numberInput.press("Backspace"),
]);
}
}

View File

@@ -1,11 +0,0 @@
import { test, expect } from "@playwright/test";
import { HomePage } from "./fixtures/home_page";
test.describe("Open App", () => {
test("should see the page title", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await expect(ui.pageTitle).toHaveText("Error Handling");
});
});

View File

@@ -1,13 +0,0 @@
import { test, expect } from "@playwright/test";
import { HomePage } from "./fixtures/home_page";
test.describe("Type Number", () => {
test("should see the typed number", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await ui.enterNumber("7");
await expect(ui.successMessage).toHaveText("You entered 7");
});
});

View File

@@ -1,6 +1,6 @@
use crate::errors::AppError;
use cfg_if::cfg_if;
use leptos::{logging::log, Errors, *};
use leptos::{Errors, *};
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;

View File

@@ -12,7 +12,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
Router,
};
use errors_axum::*;
use leptos::{logging::log, *};
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
}}

View File

@@ -16,7 +16,7 @@ actix-web = { version = "4", optional = true, features = ["macros"] }
console_log = "1"
console_error_panic_hook = "0.1"
cfg-if = "1"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos = { path = "../../leptos", features = ["nightly", "islands"] }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router", features = ["nightly"] }
@@ -41,6 +41,12 @@ ssr = [
"leptos_router/ssr",
]
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web", "leptos_actix"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
@@ -88,3 +94,5 @@ lib-features = ["hydrate"]
#
# Optional. Defaults to false.
lib-default-features = false
lib-profile-release = "wasm-release"

View File

@@ -36,8 +36,8 @@ pub fn Stories() -> impl IntoView {
let (pending, set_pending) = create_signal(false);
let hide_more_link = move || {
stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|| pending()
pending()
|| stories.get().unwrap_or(None).unwrap_or_default().len() < 28
};
view! {
@@ -65,16 +65,20 @@ pub fn Stories() -> impl IntoView {
}}
</span>
<span>"page " {page}</span>
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
<Transition
fallback=move || view! { <p>"Loading..."</p> }
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
"more >"
</a>
</span>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
"more >"
</a>
</span>
</Transition>
</div>
<main class="news-list">
<div>

View File

@@ -25,45 +25,48 @@ pub fn Story() -> impl IntoView {
};
view! {
<Suspense fallback=|| view! { "Loading..." }>
<>
<Meta name="description" content=meta_description/>
{move || story.get().map(|story| match story {
None => view! { <div class="item-view">"Error loading this story."</div> },
Some(story) => view! {
<div class="item-view">
<div class="item-view-header">
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
<Suspense fallback=|| view! { "Loading..." }>
{move || story.get().map(|story| match story {
None => view! { <div class="item-view">"Error loading this story."</div> },
Some(story) => view! {
<div class="item-view">
<div class="item-view-header">
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
</div>
<div class="item-view-comments">
<p class="item-view-comments-header">
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"No comments yet.".into()
}}
</p>
<ul class="comment-children">
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
view=move |comment| view! { <Comment comment /> }
/>
</ul>
</div>
</div>
<div class="item-view-comments">
<p class="item-view-comments-header">
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"No comments yet.".into()
}}
</p>
<ul class="comment-children">
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
view=move |comment| view! { <Comment comment /> }
/>
</ul>
</div>
</div>
}})}
</Suspense>
}})
}
</Suspense>
</>
}
}

View File

@@ -1,5 +1,5 @@
use cfg_if::cfg_if;
use leptos::{logging::log, *};
use leptos::*;
// boilerplate to run in different modes
cfg_if! {

View File

@@ -36,11 +36,12 @@ pub fn Stories() -> impl IntoView {
let (pending, set_pending) = create_signal(false);
let hide_more_link = move || {
stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|| pending()
pending()
|| stories.get().unwrap_or(None).unwrap_or_default().len() < 28
};
view! {
<div class="news-view">
<div class="news-list-nav">
<span>
@@ -64,16 +65,20 @@ pub fn Stories() -> impl IntoView {
}}
</span>
<span>"page " {page}</span>
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
<Transition
fallback=move || view! { <p>"Loading..."</p> }
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
"more >"
</a>
</span>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
"more >"
</a>
</span>
</Transition>
</div>
<main class="news-list">
<div>

View File

@@ -25,45 +25,48 @@ pub fn Story() -> impl IntoView {
};
view! {
<Suspense fallback=|| view! { "Loading..." }>
<>
<Meta name="description" content=meta_description/>
{move || story.get().map(|story| match story {
None => view! { <div class="item-view">"Error loading this story."</div> },
Some(story) => view! {
<div class="item-view">
<div class="item-view-header">
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
<Suspense fallback=|| view! { "Loading..." }>
{move || story.get().map(|story| match story {
None => view! { <div class="item-view">"Error loading this story."</div> },
Some(story) => view! {
<div class="item-view">
<div class="item-view-header">
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
</div>
<div class="item-view-comments">
<p class="item-view-comments-header">
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"No comments yet.".into()
}}
</p>
<ul class="comment-children">
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
view=move | comment| view! { <Comment comment /> }
/>
</ul>
</div>
</div>
<div class="item-view-comments">
<p class="item-view-comments-header">
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"No comments yet.".into()
}}
</p>
<ul class="comment-children">
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
view=move | comment| view! { <Comment comment /> }
/>
</ul>
</div>
</div>
}})}
</Suspense>
}})
}
</Suspense>
</>
}
}

View File

@@ -0,0 +1,3 @@
[unstable]
build-std = ["std", "panic_abort", "core", "alloc"]
build-std-features = ["panic_immediate_abort"]

View File

@@ -0,0 +1 @@
target

View File

@@ -8,13 +8,6 @@ Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
.direnv
# Support playwright testing
node_modules/
test-results/
end2end/playwright-report/
playwright/.cache/
pnpm-lock.yaml
# Support trunk
dist
fly.toml

View File

@@ -0,0 +1,112 @@
[package]
name = "hackernews"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
panic = "abort"
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_log = "1"
console_error_panic_hook = "0.1"
cfg-if = "1"
leptos = { git = "https://github.com/leptos-rs/leptos", branch = "islands", features = [
"islands",
"nightly",
] }
leptos_meta = { git = "https://github.com/leptos-rs/leptos", branch = "islands", features = [
"nightly",
] }
leptos_router = { git = "https://github.com/leptos-rs/leptos", branch = "islands", features = [
"nightly",
] }
leptos_actix = { git = "https://github.com/leptos-rs/leptos", branch = "islands", optional = true, features = [
#"nonce",
"islands",
] }
log = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.11", default-features = false, features = [
"rustls-tls",
"json",
] }
tracing = "0.1"
# openssl = { version = "0.10", features = ["v110"] }
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wee_alloc = "0.4.5"
[features]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"dep:leptos_actix",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web", "leptos_actix"]
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
output-name = "hackernews"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "0.0.0.0:8080"
#site-addr = "127.0.0.1:3004"
# The port to use for automatic reload monitoring
reload-port = 3005
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
lib-profile-release = "wasm-release"

View File

@@ -0,0 +1,25 @@
FROM rustlang/rust:nightly-bullseye as builder
RUN wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz
#RUN tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
#RUN cp cargo-binstall /usr/local/cargo/bin
#RUN cargo binstall cargo-leptos -y
#RUN rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
#RUN rustup target add wasm32-unknown-unknown
RUN mkdir -p /app
WORKDIR /app
COPY . .
RUN cargo build --release --no-default-features --features=ssr
RUN ls -l /app/target
FROM rustlang/rust:nightly-bullseye as runner
COPY --from=builder /app/target/release/hackernews /app/
COPY --from=builder /app/pkg /app
COPY --from=builder /app/Cargo.toml /app/
WORKDIR /app
ENV RUST_LOG="info"
ENV LEPTOS_OUTPUT_NAME="hackernews"
ENV APP_ENVIRONMENT="production"
ENV LEPTOS_SITE_ADDR="0.0.0.0:8080"
ENV LEPTOS_SITE_ROOT="site"
EXPOSE 8080
CMD ["/app/hackernews"]

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Greg Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1 @@
extend = [{ path = "../cargo-make/main.toml" }]

View File

@@ -0,0 +1,43 @@
# Leptos Hacker News Example
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle. Make sure you have trunk installed with `cargo install trunk`.
## Server Side Rendering with cargo-leptos
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
1. Install cargo-leptos
```bash
cargo install --locked cargo-leptos
```
2. Build the site in watch mode, recompiling on file changes
```bash
cargo leptos watch
```
Open browser on [http://localhost:3000/](http://localhost:3000/)
3. When ready to deploy, run
```bash
cargo leptos build --release
```
## Server Side Rendering without cargo-leptos
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes..
1. Install wasm-pack
```bash
cargo install wasm-pack
```
2. Build the Webassembly used to hydrate the HTML from the server
```bash
wasm-pack build --target=web --debug --no-default-features --features=hydrate
```
3. Run the server to serve the Webassembly, JS, and HTML
```bash
cargo run --no-default-features --features=ssr
```

View File

@@ -0,0 +1,11 @@
mv .cargo/config.wasm.toml .cargo/config.toml
wasm-pack build --target=web --features=hydrate --release
cd pkg
rm *.br
cp hackernews.js hackernews.unmin.js
cat hackernews.unmin.js | esbuild > hackernews.js
brotli hackernews.js
brotli hackernews_bg.wasm
brotli style.css
cd ..
mv .cargo/config.toml .cargo/config.wasm.toml

View File

@@ -0,0 +1,15 @@
# fly.toml app configuration file generated for leptos-hackernews-islands on 2023-07-27T08:08:20-04:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = "leptos-hackernews-islands"
primary_region = "bos"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ["app"]

View File

@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z" data-cargo-features="csr"/>
<link data-trunk rel="css" href="./style.css"/>
</head>
<body></body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
body{color:#1e1333;background-color:#f2f3f5;margin:0;padding-top:55px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;font-size:15px;overflow-y:scroll}a{color:#1e1333;text-decoration:none}.header{z-index:999;background-color:#1e1333;height:55px;position:fixed;top:0;left:0;right:0}.header .inner{box-sizing:border-box;max-width:800px;margin:0 auto;padding:15px 5px}.header a{color:#fffc;vertical-align:middle;letter-spacing:.075em;margin-right:1.8em;font-weight:300;line-height:24px;transition:color .15s;display:inline-block}.header a:hover{color:#fff}.header a.active{color:#fff;font-weight:400}.header a:nth-child(6){margin-right:0}.header .github{color:#fff;float:right;margin:0;font-size:.9em}.logo{vertical-align:middle;width:24px;margin-right:10px;display:inline-block}.view{max-width:800px;margin:0 auto;position:relative}.fade-enter-active,.fade-exit-active{transition:all .2s}.fade-enter,.fade-exit-active{opacity:0}@media (max-width:860px){.header .inner{padding:15px 30px}}@media (max-width:600px){.header .inner{padding:15px}.header a{margin-right:1em}.header .github{display:none}}.news-view{padding-top:45px}.news-list,.news-list-nav{background-color:#fff;border-radius:2px}.news-list-nav{text-align:center;z-index:998;padding:15px 30px;position:fixed;top:55px;left:0;right:0;box-shadow:0 1px 2px #0000001a}.news-list-nav .page-link{margin:0 1em}.news-list-nav .disabled{color:#aaa}.news-list{width:100%;margin:30px 0;transition:all .5s cubic-bezier(.55,0,.1,1);position:absolute}.news-list ul{margin:0;padding:0;list-style-type:none}@media (max-width:600px){.news-list{margin:10px 0}}.news-item{background-color:#fff;border-bottom:1px solid #eee;padding:20px 30px 20px 80px;line-height:20px;position:relative}.news-item .score{color:#1e1333;text-align:center;width:80px;margin-top:-10px;font-size:1.1em;font-weight:700;position:absolute;top:50%;left:0}.news-item .host,.news-item .meta{color:#626262;font-size:.85em}.news-item .host a,.news-item .meta a{color:#626262;text-decoration:underline}.news-item .host a:hover,.news-item .meta a:hover{color:#1e1333}.item-view-header{background-color:#fff;padding:1.8em 2em 1em;box-shadow:0 1px 2px #0000001a}.item-view-header h1{margin:0 .5em 0 0;font-size:1.5em;display:inline}.item-view-header .host,.item-view-header .meta,.item-view-header .meta a{color:#626262}.item-view-header .meta a{text-decoration:underline}.item-view-comments{background-color:#fff;margin-top:10px;padding:0 2em .5em}.item-view-comments-header{margin:0;padding:1em 0;font-size:1.1em;position:relative}.item-view-comments-header .spinner{margin:-15px 0;display:inline-block}.comment-children{margin:0;padding:0;list-style-type:none}@media (max-width:600px){.item-view-header h1{font-size:1.25em}}.comment-children .comment-children{margin-left:1.5em}.comment{border-top:1px solid #eee;position:relative}.comment .by,.comment .text,.comment .toggle{margin:1em 0;font-size:.9em}.comment .by{color:#626262}.comment .by a{color:#626262;text-decoration:underline}.comment .text{overflow-wrap:break-word}.comment .text a:hover{color:#1e1333}.comment .text pre{white-space:pre-wrap}.comment .toggle{background-color:#fffbf2;border-radius:4px;padding:.3em .5em}.comment .toggle a{color:#626262;cursor:pointer}.comment .toggle.open{background-color:#0000;margin-bottom:-.5em;padding:0}.user-view{box-sizing:border-box;background-color:#fff;padding:2em 3em}.user-view h1{margin:0;font-size:1.5em}.user-view .meta{padding:0;list-style-type:none}.user-view .label{min-width:4em;display:inline-block}.user-view .about{margin:1em 0}.user-view .links a{text-decoration:underline}leptos-island,leptos-children{display:contents}

Some files were not shown because too many files have changed in this diff Show More