mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
61 Commits
nested-sus
...
v0.6.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e2fb62857 | ||
|
|
1da2fff706 | ||
|
|
9fd2987447 | ||
|
|
7996f835d0 | ||
|
|
d72b12524e | ||
|
|
8e79c5be5c | ||
|
|
de25658c36 | ||
|
|
e2e35a9659 | ||
|
|
bf1ba589c5 | ||
|
|
f70ebc1289 | ||
|
|
3cab09e015 | ||
|
|
b431315f7c | ||
|
|
5b40881e77 | ||
|
|
59d3cce3be | ||
|
|
6a83161368 | ||
|
|
4b00c16cb9 | ||
|
|
6d6019b956 | ||
|
|
3540291065 | ||
|
|
4809cf473e | ||
|
|
aa977001c1 | ||
|
|
c16189f095 | ||
|
|
d0a013c248 | ||
|
|
531ea74e33 | ||
|
|
545e87e540 | ||
|
|
2ca30a0b2d | ||
|
|
753bf1ed54 | ||
|
|
37c6387fea | ||
|
|
0a73487152 | ||
|
|
747aba0d7f | ||
|
|
04cf47d5da | ||
|
|
47abe00993 | ||
|
|
aa3700ffb9 | ||
|
|
0770b87cb7 | ||
|
|
330ebdb018 | ||
|
|
c3179d88cf | ||
|
|
ffcf3c2952 | ||
|
|
001ca5148e | ||
|
|
1e000afa78 | ||
|
|
0f7b8841b2 | ||
|
|
7dc0441f6c | ||
|
|
0a321a1bd7 | ||
|
|
88742952f0 | ||
|
|
8a4b972e0b | ||
|
|
95bd9cc544 | ||
|
|
23bc892a24 | ||
|
|
830fba794e | ||
|
|
98633c8700 | ||
|
|
b0f5c39711 | ||
|
|
b54aa7f3f5 | ||
|
|
e33ee7ec99 | ||
|
|
cd70b2f52b | ||
|
|
c75842ed0c | ||
|
|
4ad228bf47 | ||
|
|
bbe7115360 | ||
|
|
0658a550b0 | ||
|
|
4222c832b1 | ||
|
|
dfddbd6bf9 | ||
|
|
8a77691cb5 | ||
|
|
1dbe8b2d4b | ||
|
|
fe64f0d332 | ||
|
|
c00207aa46 |
@@ -31,10 +31,9 @@ jobs:
|
||||
dir_names: true
|
||||
dir_names_max_depth: "2"
|
||||
files: |
|
||||
examples
|
||||
!examples/cargo-make
|
||||
!examples/gtk
|
||||
!examples/hackernews_js_fetch
|
||||
examples/**
|
||||
!examples/cargo-make/**
|
||||
!examples/gtk/**
|
||||
!examples/Makefile.toml
|
||||
!examples/*.md
|
||||
json: true
|
||||
|
||||
4
.github/workflows/get-example-changed.yml
vendored
4
.github/workflows/get-example-changed.yml
vendored
@@ -25,8 +25,8 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
examples/**
|
||||
!examples/cargo-make
|
||||
!examples/gtk
|
||||
!examples/cargo-make/**
|
||||
!examples/gtk/**
|
||||
!examples/Makefile.toml
|
||||
!examples/*.md
|
||||
|
||||
|
||||
9
.github/workflows/run-cargo-make-task.yml
vendored
9
.github/workflows/run-cargo-make-task.yml
vendored
@@ -55,9 +55,9 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
@@ -107,6 +107,11 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Install Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v1.x
|
||||
|
||||
# Run Cargo Make Task
|
||||
- name: ${{ inputs.cargo_make_task }}
|
||||
run: |
|
||||
|
||||
@@ -25,7 +25,8 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.6.5"
|
||||
version = "0.6.8"
|
||||
rust-version = "1.75"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.6.5" }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "benchmarks"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
l0410 = { package = "leptos", version = "0.4.10", features = [
|
||||
|
||||
@@ -51,103 +51,5 @@ echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
|
||||
|
||||
[tasks.test-report]
|
||||
workspace = false
|
||||
description = "report web testing technology used by examples - OPTION: [all]"
|
||||
script = '''
|
||||
set -emu
|
||||
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
ITALIC="\e[3m"
|
||||
YELLOW="\e[0;33m"
|
||||
RESET="\e[0m"
|
||||
|
||||
echo
|
||||
echo "${YELLOW}Web Test Technology${RESET}"
|
||||
echo
|
||||
|
||||
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' -not -path '*/node_modules/*' |
|
||||
sed 's%./%%' |
|
||||
sed 's%/Makefile.toml%%' |
|
||||
grep -v Makefile.toml |
|
||||
sort -u)
|
||||
|
||||
start_path=$(pwd)
|
||||
|
||||
for path in $makefile_paths; do
|
||||
cd $path
|
||||
|
||||
crate_symbols=
|
||||
|
||||
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"
|
||||
|
||||
while read -r line; do
|
||||
case $line in
|
||||
*"cargo-make/wasm-test.toml"*)
|
||||
crate_symbols=$crate_symbols"W"
|
||||
;;
|
||||
*"cargo-make/playwright-test.toml"*)
|
||||
crate_symbols=$crate_symbols"P"
|
||||
crate_symbols=$crate_symbols"N"
|
||||
;;
|
||||
*"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
|
||||
;;
|
||||
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
|
||||
# Show all examples
|
||||
if [ ! -z $crate_symbols ]; then
|
||||
crate_line=$crate_line$formatted_crate_symbols
|
||||
fi
|
||||
echo $crate_line
|
||||
elif [ ! -z $crate_symbols ]; then
|
||||
# Filter out examples that do not run tests in `ci`
|
||||
crate_line=$crate_line$formatted_crate_symbols
|
||||
echo $crate_line
|
||||
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
|
||||
'''
|
||||
description = "show the cargo-make configuration for web examples [web|all|help]"
|
||||
script = { file = "./cargo-make/scripts/web-report.sh" }
|
||||
|
||||
@@ -16,7 +16,7 @@ You can also run any of the examples using [`cargo-make`](https://github.com/sag
|
||||
|
||||
Follow these steps to get any example up and running.
|
||||
|
||||
1. `cd` to the example root directory
|
||||
1. `cd` to the example you want to run
|
||||
2. Run `cargo make ci` to setup and test the example
|
||||
3. Run `cargo make start` to run the example
|
||||
4. Open the client URL in the console output (<http://127.0.0.1:8080> or <http://127.0.0.1:3000> by default)
|
||||
|
||||
@@ -3,32 +3,36 @@
|
||||
[tasks.stop-client]
|
||||
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ ! -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
|
||||
if pidof -q ${CLIENT_PROCESS_NAME}; then
|
||||
echo " Stopping ${CLIENT_PROCESS_NAME}"
|
||||
pkill -ef ${CLIENT_PROCESS_NAME}
|
||||
else
|
||||
echo " ${CLIENT_PROCESS_NAME} is already stopped"
|
||||
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
|
||||
if pidof -q ${CLIENT_PROCESS_NAME}; then
|
||||
echo " ${CLIENT_PROCESS_NAME} is up"
|
||||
else
|
||||
echo " ${CLIENT_PROCESS_NAME} is not running"
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.maybe-start-client]
|
||||
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
|
||||
if pidof -q ${CLIENT_PROCESS_NAME}; then
|
||||
echo " ${CLIENT_PROCESS_NAME} is already started"
|
||||
else
|
||||
echo " Starting ${CLIENT_PROCESS_NAME}"
|
||||
if [ -z ${SPAWN_CLIENT_PROCESS} ];then
|
||||
if [ -n "${SPAWN_CLIENT_PROCESS}" ];then
|
||||
echo "Spawning process..."
|
||||
cargo make start-client ${@} &
|
||||
else
|
||||
cargo make start-client ${@}
|
||||
fi
|
||||
else
|
||||
echo " ${CLIENT_PROCESS_NAME} is already started"
|
||||
fi
|
||||
'''
|
||||
|
||||
24
examples/cargo-make/deno-build.toml
Normal file
24
examples/cargo-make/deno-build.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[tasks.build]
|
||||
clear = true
|
||||
command = "deno"
|
||||
args = ["task", "build"]
|
||||
|
||||
[tasks.start-client]
|
||||
command = "deno"
|
||||
args = ["task", "start"]
|
||||
|
||||
[tasks.check]
|
||||
clear = true
|
||||
dependencies = ["check-debug", "check-release"]
|
||||
|
||||
[tasks.check-debug]
|
||||
toolchain = "nightly-2024-01-29"
|
||||
command = "cargo"
|
||||
args = ["check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-release]
|
||||
toolchain = "nightly-2024-01-29"
|
||||
command = "cargo"
|
||||
args = ["check-all-features", "--release"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -6,9 +6,17 @@ extend = [
|
||||
[tasks.integration-test]
|
||||
description = "Run integration test with automated start and stop of processes"
|
||||
env = { SPAWN_CLIENT_PROCESS = "1" }
|
||||
dependencies = ["start", "wait-one", "test-playwright", "stop"]
|
||||
run_task = { name = ["start", "wait-test-stop"], parallel = true }
|
||||
|
||||
[tasks.wait-one]
|
||||
[tasks.wait-test-stop]
|
||||
private = true
|
||||
dependencies = ["wait-server", "test-playwright", "stop"]
|
||||
|
||||
[tasks.wait-server]
|
||||
script = '''
|
||||
sleep 1
|
||||
for run in {1..12}; do
|
||||
echo "Waiting to ensure server is started..."
|
||||
sleep 10
|
||||
done
|
||||
echo "Times up, running tests"
|
||||
'''
|
||||
|
||||
176
examples/cargo-make/scripts/web-report.sh
Executable file
176
examples/cargo-make/scripts/web-report.sh
Executable file
@@ -0,0 +1,176 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -emu
|
||||
|
||||
BOLD="\e[1m"
|
||||
ITALIC="\e[3m"
|
||||
YELLOW="\e[1;33m"
|
||||
BLUE="\e[1;36m"
|
||||
RESET="\e[0m"
|
||||
|
||||
function web { #task: only include examples with web cargo-make configuration
|
||||
print_header
|
||||
print_crate_tags "$@"
|
||||
print_footer
|
||||
}
|
||||
|
||||
function all { #task: includes all examples
|
||||
print_header
|
||||
print_crate_tags "all"
|
||||
print_footer
|
||||
}
|
||||
|
||||
function print_header {
|
||||
echo -e "${YELLOW}Cargo Make Web Report${RESET}"
|
||||
echo
|
||||
echo -e "${ITALIC}Show how crates are configured to run and test web examples with cargo-make${RESET}"
|
||||
echo
|
||||
}
|
||||
|
||||
function print_crate_tags {
|
||||
local makefile_paths
|
||||
makefile_paths=$(find_makefile_lines)
|
||||
|
||||
local start_path
|
||||
start_path=$(pwd)
|
||||
|
||||
for path in $makefile_paths; do
|
||||
cd "$path"
|
||||
|
||||
local crate_tags=
|
||||
|
||||
# Add cargo tags
|
||||
while read -r line; do
|
||||
case $line in
|
||||
*"cucumber"*)
|
||||
crate_tags=$crate_tags"C"
|
||||
;;
|
||||
*"fantoccini"*)
|
||||
crate_tags=$crate_tags"F"
|
||||
;;
|
||||
*"package.metadata.leptos"*)
|
||||
crate_tags=$crate_tags"M"
|
||||
;;
|
||||
esac
|
||||
done <"./Cargo.toml"
|
||||
|
||||
#Add makefile tags
|
||||
|
||||
local pw_count
|
||||
pw_count=$(find . -name playwright.config.ts | wc -l)
|
||||
|
||||
while read -r line; do
|
||||
case $line in
|
||||
*"cargo-make/wasm-test.toml"*)
|
||||
crate_tags=$crate_tags"W"
|
||||
;;
|
||||
*"cargo-make/playwright-test.toml"*)
|
||||
crate_tags=$crate_tags"P"
|
||||
crate_tags=$crate_tags"N"
|
||||
;;
|
||||
*"cargo-make/playwright-trunk-test.toml"*)
|
||||
crate_tags=$crate_tags"P"
|
||||
crate_tags=$crate_tags"T"
|
||||
;;
|
||||
*"cargo-make/trunk_server.toml"*)
|
||||
crate_tags=$crate_tags"T"
|
||||
;;
|
||||
*"cargo-make/cargo-leptos-webdriver-test.toml"*)
|
||||
crate_tags=$crate_tags"L"
|
||||
;;
|
||||
*"cargo-make/cargo-leptos-test.toml"*)
|
||||
crate_tags=$crate_tags"L"
|
||||
if [ "$pw_count" -gt 0 ]; then
|
||||
crate_tags=$crate_tags"P"
|
||||
fi
|
||||
;;
|
||||
*"cargo-make/cargo-leptos.toml"*)
|
||||
crate_tags=$crate_tags"L"
|
||||
;;
|
||||
*"cargo-make/deno-build.toml"*)
|
||||
crate_tags=$crate_tags"D"
|
||||
;;
|
||||
esac
|
||||
done <"./Makefile.toml"
|
||||
|
||||
# Sort tags
|
||||
local keys
|
||||
keys=$(echo "$crate_tags" | grep -o . | sort | tr -d "\n")
|
||||
|
||||
# Find leptos projects that are not configured to build with cargo-leptos
|
||||
keys=${keys//"LM"/"L"}
|
||||
|
||||
# Find leptos projects that are not configured to build with deno
|
||||
keys=${keys//"DM"/"D"}
|
||||
|
||||
# Maybe print line
|
||||
local crate_line=$path
|
||||
|
||||
if [ -n "$crate_tags" ]; then
|
||||
local color=$YELLOW
|
||||
case $keys in
|
||||
*"M"*)
|
||||
color=$BLUE
|
||||
;;
|
||||
esac
|
||||
|
||||
crate_line="$crate_line ➤ ${color}$keys${RESET}"
|
||||
echo -e "$crate_line"
|
||||
elif [ "$#" -gt 0 ]; then
|
||||
crate_line="${BOLD}$crate_line${RESET}"
|
||||
echo -e "$crate_line"
|
||||
fi
|
||||
|
||||
cd "$start_path"
|
||||
done
|
||||
}
|
||||
|
||||
function find_makefile_lines {
|
||||
find . -name Makefile.toml -not -path '*/target/*' -not -path '*/node_modules/*' |
|
||||
sed 's%./%%' |
|
||||
sed 's%/Makefile.toml%%' |
|
||||
grep -v Makefile.toml |
|
||||
sort -u
|
||||
}
|
||||
|
||||
function print_footer {
|
||||
c="${BOLD}${YELLOW}C${RESET} = Cucumber Test Runner"
|
||||
d="${BOLD}${YELLOW}D${RESET} = Deno"
|
||||
f="${BOLD}${YELLOW}F${RESET} = Fantoccini WebDriver"
|
||||
l="${BOLD}${YELLOW}L${RESET} = Cargo Leptos"
|
||||
m="${BOLD}${BLUE}M${RESET} = Cargo Leptos Metadata Only (${ITALIC}ci is not configured to build with cargo-leptos or deno${RESET})"
|
||||
n="${BOLD}${YELLOW}N${RESET} = Node"
|
||||
p="${BOLD}${YELLOW}P${RESET} = Playwright Test"
|
||||
t="${BOLD}${YELLOW}T${RESET} = Trunk"
|
||||
w="${BOLD}${YELLOW}W${RESET} = WASM Test"
|
||||
|
||||
echo
|
||||
echo -e "${ITALIC}Report Keys:${RESET}\n $c\n $d\n $f\n $l\n $m\n $n\n $p\n $t\n $w"
|
||||
echo
|
||||
}
|
||||
|
||||
###################
|
||||
# HELP
|
||||
###################
|
||||
|
||||
function list_help_for {
|
||||
local task=$1
|
||||
grep -E "^function.+ #$task" "$0" |
|
||||
sed 's/function/ /' |
|
||||
sed -e "s| { #$task: |~|g" |
|
||||
column -s"~" -t |
|
||||
sort
|
||||
}
|
||||
|
||||
function help { #help: show task descriptions
|
||||
echo -e "${BOLD}Usage:${RESET} ./$(basename "$0") <task> [options]"
|
||||
echo
|
||||
echo "Show the cargo-make configuration for web examples"
|
||||
echo
|
||||
echo -e "${BOLD}Tasks:${RESET}"
|
||||
list_help_for task
|
||||
echo
|
||||
}
|
||||
|
||||
TIMEFORMAT="./web-report.sh completed in %3lR"
|
||||
time "${@:-all}" # Show the report by default
|
||||
@@ -3,18 +3,21 @@
|
||||
[tasks.stop-server]
|
||||
condition = { env_set = ["SERVER_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ ! -z $(pidof ${SERVER_PROCESS_NAME}) ]; then
|
||||
if pidof -q ${SERVER_PROCESS_NAME}; then
|
||||
echo " Stopping ${SERVER_PROCESS_NAME}"
|
||||
pkill -ef ${SERVER_PROCESS_NAME}
|
||||
else
|
||||
echo " ${SERVER_PROCESS_NAME} is already stopped"
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.server-status]
|
||||
condition = { env_set = ["SERVER_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ -z $(pidof ${SERVER_PROCESS_NAME}) ]; then
|
||||
echo " ${SERVER_PROCESS_NAME} is not running"
|
||||
else
|
||||
if pidof -q ${SERVER_PROCESS_NAME}; then
|
||||
echo " ${SERVER_PROCESS_NAME} is up"
|
||||
else
|
||||
echo " ${SERVER_PROCESS_NAME} is not running"
|
||||
fi
|
||||
'''
|
||||
|
||||
@@ -24,11 +27,11 @@ script = '''
|
||||
YELLOW="\e[0;33m"
|
||||
RESET="\e[0m"
|
||||
|
||||
if [ -z $(pidof ${SERVER_PROCESS_NAME}) ]; then
|
||||
if pidof -q ${SERVER_PROCESS_NAME}; then
|
||||
echo " ${SERVER_PROCESS_NAME} is already started"
|
||||
else
|
||||
echo " Starting ${SERVER_PROCESS_NAME}"
|
||||
echo " ${YELLOW}>> Run cargo make stop to end process${RESET}"
|
||||
cargo make start-server ${@} &
|
||||
else
|
||||
echo " ${SERVER_PROCESS_NAME} is already started"
|
||||
fi
|
||||
'''
|
||||
|
||||
@@ -6,25 +6,33 @@ script = '''
|
||||
RESET="\e[0m"
|
||||
|
||||
if command -v chromedriver; then
|
||||
if [ -z $(pidof chromedriver) ]; then
|
||||
if pidof -q chromedriver; then
|
||||
echo " chromedriver is already started"
|
||||
else
|
||||
echo "Starting chomedriver"
|
||||
chromedriver --port=4444 &
|
||||
fi
|
||||
else
|
||||
echo "${RED}${BOLD}ERROR${RESET} - chromedriver is required by this task"
|
||||
echo "${RED}${BOLD}ERROR${RESET} - chromedriver not found"
|
||||
exit 1
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.stop-webdriver]
|
||||
script = '''
|
||||
pkill -f "chromedriver"
|
||||
if pidof -q chromedriver; then
|
||||
echo " Stopping chromedriver"
|
||||
pkill -ef "chromedriver"
|
||||
else
|
||||
echo " chromedriver is already stopped"
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.webdriver-status]
|
||||
script = '''
|
||||
if [ -z $(pidof chromedriver) ]; then
|
||||
echo chromedriver is not running
|
||||
else
|
||||
if pidof -q chromedriver; then
|
||||
echo chromedriver is up
|
||||
else
|
||||
echo chromedriver is not running
|
||||
fi
|
||||
'''
|
||||
|
||||
@@ -141,9 +141,12 @@ pub fn Counter() -> impl IntoView {
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch(())>"-1"</button>
|
||||
<Suspense fallback=move |_| view!{ <span>"Value: "</span>}>
|
||||
<span>"Value: " { counter.get().map(|count| count.unwrap_or(0)).unwrap_or(0);} "!"</span>
|
||||
</Suspense>
|
||||
<span>
|
||||
"Value: "
|
||||
<Suspense>
|
||||
{move || counter.and_then(|count| *count)} "!"
|
||||
</Suspense>
|
||||
</span>
|
||||
<button on:click=move |_| inc.dispatch(())>"+1"</button>
|
||||
</div>
|
||||
<Suspense>
|
||||
@@ -201,7 +204,7 @@ pub fn FormCounter() -> impl IntoView {
|
||||
<input type="hidden" name="msg" value="form value down"/>
|
||||
<input type="submit" value="-1"/>
|
||||
</ActionForm>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<span>"Value: " <Suspense>{move || value().to_string()} "!"</Suspense></span>
|
||||
<ActionForm action=adjust>
|
||||
<input type="hidden" name="delta" value="1"/>
|
||||
<input type="hidden" name="msg" value="form value up"/>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "counter_without_macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.75"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "counters_stable"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.75"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
[env]
|
||||
VERIFY_GTK = false
|
||||
|
||||
[tasks.verify-flow]
|
||||
condition = { env_set = ["VERIFY_GTK"] }
|
||||
|
||||
[tasks.verify]
|
||||
condition = { env_set = ["VERIFY_GTK"] }
|
||||
@@ -1 +1,8 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
|
||||
CLIENT_PROCESS_NAME = "hackernews_islands"
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/deno-build.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
|
||||
CLIENT_PROCESS_NAME = "deno"
|
||||
|
||||
@@ -30,10 +30,10 @@ sqlx = { version = "0.7.2", features = [
|
||||
], optional = true }
|
||||
thiserror = "1.0"
|
||||
wasm-bindgen = "0.2"
|
||||
axum_session_auth = { version = "0.10", features = [
|
||||
axum_session_auth = { version = "0.12.1", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
axum_session = { version = "0.10", features = [
|
||||
axum_session = { version = "0.12.4", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
bcrypt = { version = "0.15", optional = true }
|
||||
|
||||
@@ -70,7 +70,7 @@ async fn main() {
|
||||
SessionConfig::default().with_table_name("axum_sessions");
|
||||
let auth_config = AuthConfig::<i64>::default();
|
||||
let session_store = SessionStore::<SessionSqlitePool>::new(
|
||||
Some(pool.clone().into()),
|
||||
Some(SessionSqlitePool::from(pool.clone())),
|
||||
session_config,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -9,12 +9,13 @@ use thiserror::Error;
|
||||
pub fn App() -> impl IntoView {
|
||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
provide_meta_context();
|
||||
let fallback = || view! { "Page not found." }.into_view();
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/ssr_modes.css"/>
|
||||
<Title text="Welcome to Leptos"/>
|
||||
|
||||
<Router>
|
||||
<Router fallback>
|
||||
<main>
|
||||
<Routes>
|
||||
// We’ll load the home page with out-of-order streaming and <Suspense/>
|
||||
|
||||
@@ -9,12 +9,13 @@ use thiserror::Error;
|
||||
pub fn App() -> impl IntoView {
|
||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
provide_meta_context();
|
||||
let fallback = || view! { "Page not found." }.into_view();
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/ssr_modes.css"/>
|
||||
<Title text="Welcome to Leptos"/>
|
||||
|
||||
<Router>
|
||||
<Router fallback>
|
||||
<main>
|
||||
<Routes>
|
||||
// We’ll load the home page with out-of-order streaming and <Suspense/>
|
||||
|
||||
@@ -44,7 +44,7 @@ skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "tailwind_axum"
|
||||
output-name = "leptos_tailwind"
|
||||
# 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
|
||||
|
||||
18
examples/tailwind_axum/package-lock.json
generated
18
examples/tailwind_axum/package-lock.json
generated
@@ -9,7 +9,6 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"preline": "^1.8.0",
|
||||
"tailwindcss": "^3.3.2"
|
||||
}
|
||||
},
|
||||
@@ -104,15 +103,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/any-promise": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||
@@ -699,14 +689,6 @@
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||
},
|
||||
"node_modules/preline": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/preline/-/preline-1.8.0.tgz",
|
||||
"integrity": "sha512-guttn86Fc/+AbvN9oKcr2z3zU7DL3Q5dl7nhcR4nTi5F02LXQc7WIYwgIXMR97kymCs52feiju6glXO3dUIpvA==",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.2"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
|
||||
@@ -7,8 +7,7 @@ pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
|
||||
<Stylesheet id="leptos" href="/pkg/tailwind_axum.css"/>
|
||||
<Stylesheet id="leptos" href="/pkg/leptos_tailwind.css"/>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Router>
|
||||
<Routes>
|
||||
|
||||
@@ -8,7 +8,7 @@ pub fn App() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
|
||||
<Stylesheet id="leptos" href="/pkg/tailwind.css"/>
|
||||
<Stylesheet id="leptos" href="/style/output.css"/>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Router>
|
||||
<Routes>
|
||||
|
||||
@@ -6,6 +6,7 @@ authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Actix integrations for the Leptos web framework."
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
actix-http = "3"
|
||||
|
||||
@@ -203,7 +203,7 @@ pub fn handle_server_fns() -> Route {
|
||||
/// context, allowing you to pass in info about the route or user from Actix, or other info.
|
||||
///
|
||||
/// **NOTE**: If your server functions expect a context, make sure to provide it both in
|
||||
/// [`handle_server_fns_with_context`] **and** in [`leptos_routes_with_context`] (or whatever
|
||||
/// [`handle_server_fns_with_context`] **and** in [`LeptosRoutes::leptos_routes_with_context`] (or whatever
|
||||
/// rendering method you are using). During SSR, server functions are called by the rendering
|
||||
/// method, while subsequent calls from the client are handled by the server function handler.
|
||||
/// The same context needs to be provided to both handlers.
|
||||
@@ -1215,13 +1215,18 @@ where
|
||||
let mode = listing.mode();
|
||||
|
||||
for method in listing.methods() {
|
||||
let additional_context = additional_context.clone();
|
||||
let additional_context_and_method = move || {
|
||||
provide_context(method);
|
||||
additional_context();
|
||||
};
|
||||
router = if let Some(static_mode) = listing.static_mode() {
|
||||
router.route(
|
||||
path,
|
||||
static_route(
|
||||
options.clone(),
|
||||
app_fn.clone(),
|
||||
additional_context.clone(),
|
||||
additional_context_and_method.clone(),
|
||||
method,
|
||||
static_mode,
|
||||
),
|
||||
@@ -1233,7 +1238,7 @@ where
|
||||
SsrMode::OutOfOrder => {
|
||||
render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
additional_context_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
method,
|
||||
)
|
||||
@@ -1241,7 +1246,7 @@ where
|
||||
SsrMode::PartiallyBlocked => {
|
||||
render_app_to_stream_with_context_and_replace_blocks(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
additional_context_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
method,
|
||||
true,
|
||||
@@ -1250,14 +1255,14 @@ where
|
||||
SsrMode::InOrder => {
|
||||
render_app_to_stream_in_order_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
additional_context_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
method,
|
||||
)
|
||||
}
|
||||
SsrMode::Async => render_app_async_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
additional_context_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
method,
|
||||
),
|
||||
|
||||
148
integrations/actix/tests/extract_routes.rs
Normal file
148
integrations/actix/tests/extract_routes.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
use leptos::*;
|
||||
use leptos_actix::generate_route_list;
|
||||
use leptos_router::{Route, Router, Routes, TrailingSlash};
|
||||
|
||||
#[component]
|
||||
fn DefaultApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
view! {
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/baz/:name/" view/>
|
||||
<Route path="/baz/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_app() {
|
||||
let routes = generate_route_list(DefaultApp);
|
||||
|
||||
// We still have access to the original (albeit normalized) Leptos paths:
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.leptos_path(),
|
||||
&["/bar", "/baz/*any", "/baz/:id", "/baz/:name", "/foo"],
|
||||
);
|
||||
|
||||
// ... But leptos-actix has also reformatted "paths" to work for Actix.
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.path(),
|
||||
&["/bar", "/baz/{id}", "/baz/{name}", "/baz/{tail:.*}", "/foo"],
|
||||
);
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ExactApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
let trailing_slash = TrailingSlash::Exact;
|
||||
view! {
|
||||
<Router trailing_slash>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/baz/:name/" view/>
|
||||
<Route path="/baz/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exact_app() {
|
||||
let routes = generate_route_list(ExactApp);
|
||||
|
||||
// In Exact mode, the Leptos paths no longer have their trailing slashes stripped:
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.leptos_path(),
|
||||
&["/bar/", "/baz/*any", "/baz/:id", "/baz/:name/", "/foo"],
|
||||
);
|
||||
|
||||
// Actix paths also have trailing slashes as a result:
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.path(),
|
||||
&[
|
||||
"/bar/",
|
||||
"/baz/{id}",
|
||||
"/baz/{name}/",
|
||||
"/baz/{tail:.*}",
|
||||
"/foo",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn RedirectApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
let trailing_slash = TrailingSlash::Redirect;
|
||||
view! {
|
||||
<Router trailing_slash>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/baz/:name/" view/>
|
||||
<Route path="/baz/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_redirect_app() {
|
||||
let routes = generate_route_list(RedirectApp);
|
||||
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.leptos_path(),
|
||||
&[
|
||||
"/bar",
|
||||
"/bar/",
|
||||
"/baz/*any",
|
||||
"/baz/:id",
|
||||
"/baz/:id/",
|
||||
"/baz/:name",
|
||||
"/baz/:name/",
|
||||
"/foo",
|
||||
"/foo/",
|
||||
],
|
||||
);
|
||||
|
||||
// ... But leptos-actix has also reformatted "paths" to work for Actix.
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.path(),
|
||||
&[
|
||||
"/bar",
|
||||
"/bar/",
|
||||
"/baz/{id}",
|
||||
"/baz/{id}/",
|
||||
"/baz/{name}",
|
||||
"/baz/{name}/",
|
||||
"/baz/{tail:.*}",
|
||||
"/foo",
|
||||
"/foo/",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_same<'t, T, F, U>(
|
||||
input: &'t Vec<T>,
|
||||
mapper: F,
|
||||
expected_sorted_values: &[U],
|
||||
) where
|
||||
F: Fn(&'t T) -> U + 't,
|
||||
U: Ord + std::fmt::Debug,
|
||||
{
|
||||
let mut values: Vec<U> = input.iter().map(mapper).collect();
|
||||
values.sort();
|
||||
assert_eq!(values, expected_sorted_values);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Axum integrations for the Leptos web framework."
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7", default-features = false, features = [
|
||||
|
||||
@@ -79,6 +79,20 @@ impl ResponseParts {
|
||||
}
|
||||
|
||||
/// Allows you to override details of the HTTP response like the status code and add Headers/Cookies.
|
||||
///
|
||||
/// `ResponseOptions` is provided via context when you use most of the handlers provided in this
|
||||
/// crate, including [`.leptos_routes`](LeptosRoutes::leptos_routes),
|
||||
/// [`.leptos_routes_with_context`](LeptosRoutes::leptos_routes_with_context), [`handle_server_fns`], etc.
|
||||
/// You can find the full set of provided context types in each handler function.
|
||||
///
|
||||
/// If you provide your own handler, you will need to provide `ResponseOptions` via context
|
||||
/// yourself if you want to access it via context.
|
||||
/// ```rust,ignore
|
||||
/// #[server]
|
||||
/// pub async fn get_opts() -> Result<(), ServerFnError> {
|
||||
/// let opts = expect_context::<leptos_axum::ResponseOptions>();
|
||||
/// Ok(())
|
||||
/// }
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
|
||||
|
||||
@@ -257,7 +271,13 @@ async fn handle_server_fns_inner(
|
||||
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
|
||||
// capture current span to enable trace context propagation
|
||||
let current_span = tracing::Span::current();
|
||||
|
||||
spawn_task!(async move {
|
||||
// enter captured span for trace context propagation in spawned task
|
||||
let _guard = current_span.enter();
|
||||
|
||||
let path = req.uri().path().to_string();
|
||||
let (req, parts) = generate_request_and_parts(req);
|
||||
|
||||
@@ -1609,6 +1629,11 @@ where
|
||||
let path = listing.path();
|
||||
|
||||
for method in listing.methods() {
|
||||
let cx_with_state = cx_with_state.clone();
|
||||
let cx_with_state_and_method = move || {
|
||||
provide_context(method);
|
||||
cx_with_state();
|
||||
};
|
||||
router = if let Some(static_mode) = listing.static_mode() {
|
||||
#[cfg(feature = "default")]
|
||||
{
|
||||
@@ -1617,7 +1642,7 @@ where
|
||||
path,
|
||||
LeptosOptions::from_ref(options),
|
||||
app_fn.clone(),
|
||||
cx_with_state.clone(),
|
||||
cx_with_state_and_method.clone(),
|
||||
method,
|
||||
static_mode,
|
||||
)
|
||||
@@ -1637,7 +1662,7 @@ where
|
||||
SsrMode::OutOfOrder => {
|
||||
let s = render_app_to_stream_with_context(
|
||||
LeptosOptions::from_ref(options),
|
||||
cx_with_state.clone(),
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
@@ -1651,7 +1676,7 @@ where
|
||||
SsrMode::PartiallyBlocked => {
|
||||
let s = render_app_to_stream_with_context_and_replace_blocks(
|
||||
LeptosOptions::from_ref(options),
|
||||
cx_with_state.clone(),
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
true
|
||||
);
|
||||
@@ -1666,7 +1691,7 @@ where
|
||||
SsrMode::InOrder => {
|
||||
let s = render_app_to_stream_in_order_with_context(
|
||||
LeptosOptions::from_ref(options),
|
||||
cx_with_state.clone(),
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
@@ -1680,7 +1705,7 @@ where
|
||||
SsrMode::Async => {
|
||||
let s = render_app_async_with_context(
|
||||
LeptosOptions::from_ref(options),
|
||||
cx_with_state.clone(),
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
|
||||
@@ -6,6 +6,7 @@ authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Utilities to help build server integrations for the Leptos web framework."
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3"
|
||||
|
||||
@@ -2,6 +2,7 @@ use futures::{Stream, StreamExt};
|
||||
use leptos::{nonce::use_nonce, use_context, RuntimeId};
|
||||
use leptos_config::LeptosOptions;
|
||||
use leptos_meta::MetaContext;
|
||||
use std::{borrow::Cow, collections::HashMap, env, fs};
|
||||
|
||||
extern crate tracing;
|
||||
|
||||
@@ -55,7 +56,9 @@ pub fn html_parts_separated(
|
||||
options: &LeptosOptions,
|
||||
meta: Option<&MetaContext>,
|
||||
) -> (String, &'static str) {
|
||||
let pkg_path = &options.site_pkg_dir;
|
||||
let pkg_path = option_env!("CDN_PKG_PATH")
|
||||
.map(Cow::from)
|
||||
.unwrap_or_else(|| format!("/{}", options.site_pkg_dir).into());
|
||||
let output_name = &options.output_name;
|
||||
let nonce = use_nonce();
|
||||
let nonce = nonce
|
||||
@@ -100,6 +103,14 @@ pub fn html_parts_separated(
|
||||
} else {
|
||||
"() => mod.hydrate()"
|
||||
};
|
||||
|
||||
let (js_hash, wasm_hash, css_hash) = get_hashes(options);
|
||||
|
||||
let head = head.replace(
|
||||
&format!("{output_name}.css"),
|
||||
&format!("{output_name}{css_hash}.css"),
|
||||
);
|
||||
|
||||
let head = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html{html_metadata}>
|
||||
@@ -107,8 +118,8 @@ pub fn html_parts_separated(
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
{head}
|
||||
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js"{nonce}>
|
||||
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin=""{nonce}>
|
||||
<link rel="modulepreload" href="{pkg_path}/{output_name}{js_hash}.js"{nonce}>
|
||||
<link rel="preload" href="{pkg_path}/{wasm_output_name}{wasm_hash}.wasm" as="fetch" type="application/wasm" crossorigin=""{nonce}>
|
||||
<script type="module"{nonce}>
|
||||
function idle(c) {{
|
||||
if ("requestIdleCallback" in window) {{
|
||||
@@ -118,9 +129,9 @@ pub fn html_parts_separated(
|
||||
}}
|
||||
}}
|
||||
idle(() => {{
|
||||
import('/{pkg_path}/{output_name}.js')
|
||||
import('{pkg_path}/{output_name}{js_hash}.js')
|
||||
.then(mod => {{
|
||||
mod.default('/{pkg_path}/{wasm_output_name}.wasm').then({import_callback});
|
||||
mod.default('{pkg_path}/{wasm_output_name}{wasm_hash}.wasm').then({import_callback});
|
||||
}})
|
||||
}});
|
||||
</script>
|
||||
@@ -131,6 +142,46 @@ pub fn html_parts_separated(
|
||||
(head, tail)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn get_hashes(options: &LeptosOptions) -> (String, String, String) {
|
||||
let mut ext_to_hash = HashMap::from([
|
||||
("js".to_string(), "".to_string()),
|
||||
("wasm".to_string(), "".to_string()),
|
||||
("css".to_string(), "".to_string()),
|
||||
]);
|
||||
|
||||
if options.hash_files {
|
||||
let hash_path = env::current_exe()
|
||||
.map(|path| {
|
||||
path.parent().map(|p| p.to_path_buf()).unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.join(&options.hash_file);
|
||||
|
||||
if hash_path.exists() {
|
||||
let hashes = fs::read_to_string(&hash_path)
|
||||
.expect("failed to read hash file");
|
||||
for line in hashes.lines() {
|
||||
let line = line.trim();
|
||||
if !line.is_empty() {
|
||||
if let Some((k, v)) = line.split_once(':') {
|
||||
ext_to_hash.insert(
|
||||
k.trim().to_string(),
|
||||
format!(".{}", v.trim()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
ext_to_hash["js"].clone(),
|
||||
ext_to_hash["wasm"].clone(),
|
||||
ext_to_hash["css"].clone(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub async fn build_async_response(
|
||||
stream: impl Stream<Item = String> + 'static,
|
||||
|
||||
@@ -7,6 +7,7 @@ license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained reactivity to build declarative user interfaces."
|
||||
readme = "../README.md"
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "1"
|
||||
@@ -15,6 +16,7 @@ leptos_macro = { workspace = true }
|
||||
leptos_reactive = { workspace = true }
|
||||
leptos_server = { workspace = true }
|
||||
leptos_config = { workspace = true }
|
||||
leptos-spin-macro = { version="0.1", optional = true}
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.18"
|
||||
typed-builder-macro = "0.18"
|
||||
@@ -66,7 +68,10 @@ miniserde = ["leptos_reactive/miniserde"]
|
||||
rkyv = ["leptos_reactive/rkyv"]
|
||||
tracing = ["leptos_macro/tracing"]
|
||||
nonce = ["leptos_dom/nonce"]
|
||||
spin = ["leptos_reactive/spin"]
|
||||
spin = [
|
||||
"leptos_reactive/spin",
|
||||
"leptos-spin-macro"
|
||||
]
|
||||
experimental-islands = [
|
||||
"leptos_dom/experimental-islands",
|
||||
"leptos_macro/experimental-islands",
|
||||
@@ -87,7 +92,9 @@ denylist = [
|
||||
"rustls",
|
||||
"default-tls",
|
||||
"wasm-bindgen",
|
||||
"trace-component-props"
|
||||
"trace-component-props",
|
||||
"spin",
|
||||
"experimental-islands"
|
||||
]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
|
||||
@@ -179,7 +179,14 @@ pub mod error {
|
||||
pub use leptos_macro::template;
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "template_macro")))]
|
||||
pub use leptos_macro::view as template;
|
||||
pub use leptos_macro::{component, island, server, slice, slot, view, Params};
|
||||
pub use leptos_macro::{component, island, slice, slot, view, Params};
|
||||
cfg_if::cfg_if!(
|
||||
if #[cfg(feature="spin")] {
|
||||
pub use leptos_spin_macro::server;
|
||||
} else {
|
||||
pub use leptos_macro::server;
|
||||
}
|
||||
);
|
||||
pub use leptos_reactive::*;
|
||||
pub use leptos_server::{
|
||||
self, create_action, create_multi_action, create_server_action,
|
||||
|
||||
@@ -36,7 +36,7 @@ use std::rc::Rc;
|
||||
/// <div>
|
||||
/// <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }>
|
||||
/// {move || {
|
||||
/// cats.read().map(|data| match data {
|
||||
/// cats.get().map(|data| match data {
|
||||
/// None => view! { <pre>"Error"</pre> }.into_view(),
|
||||
/// Some(cats) => cats
|
||||
/// .iter()
|
||||
@@ -173,6 +173,9 @@ where
|
||||
runtime,
|
||||
);
|
||||
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
let prev_no_hydrate =
|
||||
SharedContext::no_hydrate();
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
{
|
||||
SharedContext::set_no_hydrate(
|
||||
@@ -180,7 +183,7 @@ where
|
||||
);
|
||||
}
|
||||
|
||||
with_owner(owner, {
|
||||
let rendered = with_owner(owner, {
|
||||
move || {
|
||||
HydrationCtx::continue_from(
|
||||
current_id,
|
||||
@@ -194,7 +197,15 @@ where
|
||||
.render_to_string()
|
||||
.to_string()
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
SharedContext::set_no_hydrate(
|
||||
prev_no_hydrate,
|
||||
);
|
||||
|
||||
#[allow(clippy::let_and_return)]
|
||||
rendered
|
||||
}
|
||||
},
|
||||
// in-order streaming
|
||||
@@ -205,6 +216,9 @@ where
|
||||
runtime,
|
||||
);
|
||||
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
let prev_no_hydrate =
|
||||
SharedContext::no_hydrate();
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
{
|
||||
SharedContext::set_no_hydrate(
|
||||
@@ -212,7 +226,7 @@ where
|
||||
);
|
||||
}
|
||||
|
||||
with_owner(owner, {
|
||||
let rendered = with_owner(owner, {
|
||||
move || {
|
||||
HydrationCtx::continue_from(
|
||||
current_id,
|
||||
@@ -225,7 +239,15 @@ where
|
||||
.into_view()
|
||||
.into_stream_chunks()
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
SharedContext::set_no_hydrate(
|
||||
prev_no_hydrate,
|
||||
);
|
||||
|
||||
#[allow(clippy::let_and_return)]
|
||||
rendered
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -155,7 +155,9 @@ fn is_first_run(
|
||||
first_run: RwSignal<bool>,
|
||||
suspense_context: &SuspenseContext,
|
||||
) -> bool {
|
||||
if cfg!(feature = "csr") {
|
||||
if cfg!(feature = "csr")
|
||||
|| (cfg!(feature = "hydrate") && !HydrationCtx::is_hydrating())
|
||||
{
|
||||
false
|
||||
} else {
|
||||
match (
|
||||
|
||||
@@ -7,9 +7,10 @@ license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Configuration for the Leptos web framework."
|
||||
readme = "../README.md"
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
config = { version = "0.13.3", default-features = false, features = ["toml"] }
|
||||
config = { version = "0.14", default-features = false, features = ["toml"] }
|
||||
regex = "1.7.0"
|
||||
serde = { version = "1.0.151", features = ["derive"] }
|
||||
thiserror = "1.0.38"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{net::AddrParseError, num::ParseIntError};
|
||||
use std::{net::AddrParseError, num::ParseIntError, str::ParseBoolError};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error, Clone)]
|
||||
@@ -31,3 +31,9 @@ impl From<AddrParseError> for LeptosConfigError {
|
||||
Self::ConfigError(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseBoolError> for LeptosConfigError {
|
||||
fn from(e: ParseBoolError) -> Self {
|
||||
Self::ConfigError(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,15 @@ pub struct LeptosOptions {
|
||||
#[builder(default = default_not_found_path())]
|
||||
#[serde(default = "default_not_found_path")]
|
||||
pub not_found_path: String,
|
||||
/// The file name of the hash text file generated by cargo-leptos. Defaults to `hash.txt`.
|
||||
#[builder(default = default_hash_file_name())]
|
||||
#[serde(default = "default_hash_file_name")]
|
||||
pub hash_file: String,
|
||||
/// If true, hashes will be generated for all files in the site_root and added to their file names.
|
||||
/// Defaults to `true`.
|
||||
#[builder(default = default_hash_files())]
|
||||
#[serde(default = "default_hash_files")]
|
||||
pub hash_files: bool,
|
||||
}
|
||||
|
||||
impl LeptosOptions {
|
||||
@@ -108,6 +117,8 @@ impl LeptosOptions {
|
||||
env_w_default("LEPTOS_RELOAD_WS_PROTOCOL", "ws")?.as_str(),
|
||||
)?,
|
||||
not_found_path: env_w_default("LEPTOS_NOT_FOUND_PATH", "/404")?,
|
||||
hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?,
|
||||
hash_files: env_w_default("LEPTOS_HASH_FILES", "false")?.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -146,6 +157,14 @@ fn default_not_found_path() -> String {
|
||||
"/404".to_string()
|
||||
}
|
||||
|
||||
fn default_hash_file_name() -> String {
|
||||
"hash.txt".to_string()
|
||||
}
|
||||
|
||||
fn default_hash_files() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn env_wo_default(key: &str) -> Result<Option<String>, LeptosConfigError> {
|
||||
match std::env::var(key) {
|
||||
Ok(val) => Ok(Some(val)),
|
||||
|
||||
@@ -6,6 +6,7 @@ authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "DOM operations for the Leptos web framework."
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-recursion = "1"
|
||||
|
||||
@@ -103,6 +103,25 @@ pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
|
||||
_ = request_animation_frame_with_handle(cb);
|
||||
}
|
||||
|
||||
// Closure::once_into_js only frees the callback when it's actually
|
||||
// called, so this instead uses into_js_value, which can be freed by
|
||||
// the host JS engine's GC if it supports weak references (which all
|
||||
// modern brower engines do). The way this works is that the provided
|
||||
// callback's captured data is dropped immediately after being called,
|
||||
// as before, but it leaves behind a small stub closure rust-side that
|
||||
// will be freed "eventually" by the JS GC. If the function is never
|
||||
// called (e.g., it's a cancelled timeout or animation frame callback)
|
||||
// then it will also be freed eventually.
|
||||
fn closure_once(cb: impl FnOnce() + 'static) -> JsValue {
|
||||
let mut wrapped_cb: Option<Box<dyn FnOnce()>> = Some(Box::new(cb));
|
||||
let closure = Closure::new(move || {
|
||||
if let Some(cb) = wrapped_cb.take() {
|
||||
cb()
|
||||
}
|
||||
});
|
||||
closure.into_js_value()
|
||||
}
|
||||
|
||||
/// Runs the given function between the next repaint using
|
||||
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame),
|
||||
/// returning a cancelable handle.
|
||||
@@ -128,7 +147,7 @@ pub fn request_animation_frame_with_handle(
|
||||
.map(AnimationFrameRequestHandle)
|
||||
}
|
||||
|
||||
raf(Closure::once_into_js(cb))
|
||||
raf(closure_once(cb))
|
||||
}
|
||||
|
||||
/// Handle that is generated by [request_idle_callback_with_handle] and can be
|
||||
@@ -237,7 +256,7 @@ pub fn set_timeout_with_handle(
|
||||
.map(TimeoutHandle)
|
||||
}
|
||||
|
||||
st(Closure::once_into_js(cb), duration)
|
||||
st(closure_once(cb), duration)
|
||||
}
|
||||
|
||||
/// "Debounce" a callback function. This will cause it to wait for a period of `delay`
|
||||
|
||||
@@ -7,6 +7,7 @@ license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Utility types used for dev mode and hot-reloading for the Leptos web framework."
|
||||
readme = "../README.md"
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
|
||||
@@ -7,6 +7,7 @@ license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "view macro for the Leptos web framework."
|
||||
readme = "../README.md"
|
||||
rust-version.workspace = true
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
@@ -447,7 +447,7 @@ impl ToTokens for Model {
|
||||
};
|
||||
|
||||
quote! {
|
||||
#[::leptos::wasm_bindgen::prelude::wasm_bindgen]
|
||||
#[::leptos::wasm_bindgen::prelude::wasm_bindgen(wasm_bindgen = ::leptos::wasm_bindgen)]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn #hydrate_fn_name(el: ::leptos::web_sys::HtmlElement) {
|
||||
if let Some(Ok(key)) = el.dataset().get(::leptos::wasm_bindgen::intern("hkc")).map(|key| std::str::FromStr::from_str(&key)) {
|
||||
|
||||
@@ -870,6 +870,7 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// - `name`: sets the identifier for the server function’s type, which is a struct created
|
||||
/// to hold the arguments (defaults to the function identifier in PascalCase)
|
||||
/// - `prefix`: a prefix at which the server function handler will be mounted (defaults to `/api`)
|
||||
/// your prefix must begin with `/`. Otherwise your function won't be found.
|
||||
/// - `endpoint`: specifies the exact path at which the server function handler will be mounted,
|
||||
/// relative to the prefix (defaults to the function name followed by unique hash)
|
||||
/// - `input`: the encoding for the arguments (defaults to `PostUrl`)
|
||||
@@ -883,6 +884,11 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// - `"GetCbor"`: `GET` request with URL-encoded arguments and CBOR response
|
||||
/// - `req` and `res` specify the HTTP request and response types to be used on the server (these
|
||||
/// should usually only be necessary if you are integrating with a server other than Actix/Axum)
|
||||
/// - `impl_from`: specifies whether to implement trait `From` for server function's type or not.
|
||||
/// By default, if a server function only has one argument, the macro automatically implements the `From` trait
|
||||
/// to convert from the argument type to the server function type, and vice versa, allowing you to convert
|
||||
/// between them easily. Setting `impl_from` to `false` disables this, which can be necessary for argument types
|
||||
/// for which this would create a conflicting implementation. (defaults to `true`)
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[server(
|
||||
@@ -891,6 +897,7 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// endpoint = "my_fn",
|
||||
/// input = Cbor,
|
||||
/// output = Json
|
||||
/// impl_from = true
|
||||
/// )]
|
||||
/// pub async fn my_wacky_server_fn(input: Vec<String>) -> Result<usize, ServerFnError> {
|
||||
/// todo!()
|
||||
@@ -900,17 +907,17 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// ## Server Function Encodings
|
||||
///
|
||||
/// Server functions are designed to allow a flexible combination of `input` and `output` encodings, the set
|
||||
/// of which can be found in the [`server_fn::codec`] module.
|
||||
/// of which can be found in the [`server_fn::codec`](../server_fn/codec/index.html) module.
|
||||
///
|
||||
/// The serialization/deserialization process for server functions consists of a series of steps,
|
||||
/// each of which is represented by a different trait:
|
||||
/// 1. [`IntoReq`]: The client serializes the [`ServerFn`] argument type into an HTTP request.
|
||||
/// 2. The [`Client`] sends the request to the server.
|
||||
/// 3. [`FromReq`]: The server deserializes the HTTP request back into the [`ServerFn`] type.
|
||||
/// 4. The server calls calls [`ServerFn::run_body`] on the data.
|
||||
/// 5. [`IntoRes`]: The server serializes the [`ServerFn::Output`] type into an HTTP response.
|
||||
/// 6. The server integration applies any middleware from [`ServerFn::middlewares`] and responds to the request.
|
||||
/// 7. [`FromRes`]: The client deserializes the response back into the [`ServerFn::Output`] type.
|
||||
/// 1. [`IntoReq`](../server_fn/codec/trait.IntoReq.html): The client serializes the [`ServerFn`](../server_fn/trait.ServerFn.html) argument type into an HTTP request.
|
||||
/// 2. The [`Client`](../server_fn/client/trait.Client.html) sends the request to the server.
|
||||
/// 3. [`FromReq`](../server_fn/codec/trait.FromReq.html): The server deserializes the HTTP request back into the [`ServerFn`](../server_fn/client/trait.Client.html) type.
|
||||
/// 4. The server calls calls [`ServerFn::run_body`](../server_fn/trait.ServerFn.html#tymethod.run_body) on the data.
|
||||
/// 5. [`IntoRes`](../server_fn/codec/trait.IntoRes.html): The server serializes the [`ServerFn::Output`](../server_fn/trait.ServerFn.html#associatedtype.Output) type into an HTTP response.
|
||||
/// 6. The server integration applies any middleware from [`ServerFn::middleware`](../server_fn/middleware/index.html) and responds to the request.
|
||||
/// 7. [`FromRes`](../server_fn/codec/trait.FromRes.html): The client deserializes the response back into the [`ServerFn::Output`](../server_fn/trait.ServerFn.html#associatedtype.Output) type.
|
||||
///
|
||||
/// Whatever encoding is provided to `input` should implement `IntoReq` and `FromReq`. Whatever encoding is provided
|
||||
/// to `output` should implement `IntoRes` and `FromRes`.
|
||||
@@ -932,8 +939,8 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
|
||||
/// inside the function body can’t fail, the processes of serialization/deserialization and the
|
||||
/// network call are fallible.
|
||||
/// - [`ServerFnError`] can be generic over some custom error type. If so, that type should implement
|
||||
/// [`FromStr`] and [`Display`], but does not need to implement [`Error`]. This is so the value
|
||||
/// - [`ServerFnError`](../server_fn/error/enum.ServerFnError.html) can be generic over some custom error type. If so, that type should implement
|
||||
/// [`FromStr`](std::str::FromStr) and [`Display`](std::fmt::Display), but does not need to implement [`Error`](std::error::Error). This is so the value
|
||||
/// can be easily serialized and deserialized along with the result.
|
||||
/// - **Server functions are part of the public API of your application.** A server function is an
|
||||
/// ad hoc HTTP API endpoint, not a magic formula. Any server function can be accessed by any HTTP
|
||||
|
||||
@@ -6,6 +6,7 @@ authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Reactive system for the Leptos web framework."
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
slotmap = { version = "1", features = ["serde"] }
|
||||
|
||||
@@ -86,7 +86,7 @@ use std::any::{Any, TypeId};
|
||||
///
|
||||
/// ### Solution
|
||||
///
|
||||
/// If you are using the full Leptos framework, you can use the [`Provider`](leptos::Provider)
|
||||
/// If you are using the full Leptos framework, you can use the [`Provider`](../leptos/fn.Provider.html)
|
||||
/// component to solve this issue.
|
||||
///
|
||||
/// ```rust
|
||||
|
||||
@@ -339,11 +339,13 @@ thread_local! {
|
||||
impl SharedContext {
|
||||
/// Whether the renderer should currently add hydration IDs.
|
||||
pub fn no_hydrate() -> bool {
|
||||
println!("no_hydrate == {}", NO_HYDRATE.with(Cell::get));
|
||||
NO_HYDRATE.with(Cell::get)
|
||||
}
|
||||
|
||||
/// Sets whether the renderer should not add hydration IDs.
|
||||
pub fn set_no_hydrate(hydrate: bool) {
|
||||
println!("set_no_hydrate == {}", hydrate);
|
||||
NO_HYDRATE.with(|cell| cell.set(hydrate));
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ use runtime::*;
|
||||
pub use runtime::{
|
||||
as_child_of_current_owner, batch, create_runtime, current_runtime,
|
||||
on_cleanup, run_as_child, set_current_runtime,
|
||||
spawn_local_with_current_owner, spawn_local_with_owner,
|
||||
spawn_local_with_current_owner, spawn_local_with_owner, try_batch,
|
||||
try_spawn_local_with_current_owner, try_spawn_local_with_owner,
|
||||
try_with_owner, untrack, untrack_with_diagnostics, with_current_owner,
|
||||
with_owner, Owner, RuntimeId, ScopedFuture,
|
||||
@@ -143,7 +143,8 @@ pub use suspense::{GlobalSuspenseContext, SuspenseContext};
|
||||
pub use trigger::*;
|
||||
pub use watch::*;
|
||||
|
||||
pub(crate) fn console_warn(s: &str) {
|
||||
#[doc(hidden)]
|
||||
pub fn console_warn(s: &str) {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", any(feature = "csr", feature = "hydrate")))] {
|
||||
web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(s));
|
||||
|
||||
@@ -86,7 +86,88 @@ pub fn create_memo<T>(f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
|
||||
where
|
||||
T: PartialEq + 'static,
|
||||
{
|
||||
Runtime::current().create_memo(f)
|
||||
Runtime::current().create_owning_memo(move |current_value| {
|
||||
let new_value = f(current_value.as_ref());
|
||||
let is_different = current_value.as_ref() != Some(&new_value);
|
||||
(new_value, is_different)
|
||||
})
|
||||
}
|
||||
|
||||
/// Like [`create_memo`], `create_owning_memo` creates an efficient derived reactive value based on
|
||||
/// other reactive values, but with two differences:
|
||||
/// 1. The argument to the memo function is owned instead of borrowed.
|
||||
/// 2. The function must also return whether the value has changed, as the first element of the tuple.
|
||||
///
|
||||
/// All of the other caveats and guarantees are the same as the usual "borrowing" memos.
|
||||
///
|
||||
/// This type of memo is useful for memos which can avoid computation by re-using the last value,
|
||||
/// especially slices that need to allocate.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # fn really_expensive_computation(value: i32) -> i32 { value };
|
||||
/// # let runtime = create_runtime();
|
||||
/// pub struct State {
|
||||
/// name: String,
|
||||
/// token: String,
|
||||
/// }
|
||||
///
|
||||
/// let state = create_rw_signal(State {
|
||||
/// name: "Alice".to_owned(),
|
||||
/// token: "abcdef".to_owned(),
|
||||
/// });
|
||||
///
|
||||
/// // If we used `create_memo`, we'd need to allocate every time the state changes, but by using
|
||||
/// // `create_owning_memo` we can allocate only when `state.name` changes.
|
||||
/// let name = create_owning_memo(move |old_name| {
|
||||
/// state.with(move |state| {
|
||||
/// if let Some(name) =
|
||||
/// old_name.filter(|old_name| old_name == &state.name)
|
||||
/// {
|
||||
/// (name, false)
|
||||
/// } else {
|
||||
/// (state.name.clone(), true)
|
||||
/// }
|
||||
/// })
|
||||
/// });
|
||||
/// let set_name = move |name| state.update(|state| state.name = name);
|
||||
///
|
||||
/// // We can also re-use the last allocation even when the value changes, which is usually faster,
|
||||
/// // but may have some caveats (e.g. if the value size is drastically reduced, the memory will
|
||||
/// // still be used for the life of the memo).
|
||||
/// let token = create_owning_memo(move |old_token| {
|
||||
/// state.with(move |state| {
|
||||
/// let is_different = old_token.as_ref() != Some(&state.token);
|
||||
/// let mut token = old_token.unwrap_or_else(String::new);
|
||||
///
|
||||
/// if is_different {
|
||||
/// token.clone_from(&state.token);
|
||||
/// }
|
||||
/// (token, is_different)
|
||||
/// })
|
||||
/// });
|
||||
/// let set_token = move |new_token| state.update(|state| state.token = new_token);
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature="ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn create_owning_memo<T>(
|
||||
f: impl Fn(Option<T>) -> (T, bool) + 'static,
|
||||
) -> Memo<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Runtime::current().create_owning_memo(f)
|
||||
}
|
||||
|
||||
/// An efficient derived reactive value based on other reactive values.
|
||||
@@ -216,6 +297,65 @@ impl<T> Memo<T> {
|
||||
{
|
||||
create_memo(f)
|
||||
}
|
||||
|
||||
/// Creates a new owning memo from the given function.
|
||||
///
|
||||
/// This is identical to [`create_owning_memo`].
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # fn really_expensive_computation(value: i32) -> i32 { value };
|
||||
/// # let runtime = create_runtime();
|
||||
/// pub struct State {
|
||||
/// name: String,
|
||||
/// token: String,
|
||||
/// }
|
||||
///
|
||||
/// let state = RwSignal::new(State {
|
||||
/// name: "Alice".to_owned(),
|
||||
/// token: "abcdef".to_owned(),
|
||||
/// });
|
||||
///
|
||||
/// // If we used `Memo::new`, we'd need to allocate every time the state changes, but by using
|
||||
/// // `Memo::new_owning` we can allocate only when `state.name` changes.
|
||||
/// let name = Memo::new_owning(move |old_name| {
|
||||
/// state.with(move |state| {
|
||||
/// if let Some(name) =
|
||||
/// old_name.filter(|old_name| old_name == &state.name)
|
||||
/// {
|
||||
/// (name, false)
|
||||
/// } else {
|
||||
/// (state.name.clone(), true)
|
||||
/// }
|
||||
/// })
|
||||
/// });
|
||||
/// let set_name = move |name| state.update(|state| state.name = name);
|
||||
///
|
||||
/// // We can also re-use the last allocation even when the value changes, which is usually faster,
|
||||
/// // but may have some caveats (e.g. if the value size is drastically reduced, the memory will
|
||||
/// // still be used for the life of the memo).
|
||||
/// let token = Memo::new_owning(move |old_token| {
|
||||
/// state.with(move |state| {
|
||||
/// let is_different = old_token.as_ref() != Some(&state.token);
|
||||
/// let mut token = old_token.unwrap_or_else(String::new);
|
||||
///
|
||||
/// if is_different {
|
||||
/// token.clone_from(&state.token);
|
||||
/// }
|
||||
/// (token, is_different)
|
||||
/// })
|
||||
/// });
|
||||
/// let set_token = move |new_token| state.update(|state| state.token = new_token);
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
#[track_caller]
|
||||
pub fn new_owning(f: impl Fn(Option<T>) -> (T, bool) + 'static) -> Memo<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
create_owning_memo(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for Memo<T>
|
||||
@@ -524,8 +664,8 @@ impl_get_fn_traits![Memo];
|
||||
|
||||
pub(crate) struct MemoState<T, F>
|
||||
where
|
||||
T: PartialEq + 'static,
|
||||
F: Fn(Option<&T>) -> T,
|
||||
T: 'static,
|
||||
F: Fn(Option<T>) -> (T, bool),
|
||||
{
|
||||
pub f: F,
|
||||
pub t: PhantomData<T>,
|
||||
@@ -535,8 +675,8 @@ where
|
||||
|
||||
impl<T, F> AnyComputation for MemoState<T, F>
|
||||
where
|
||||
T: PartialEq + 'static,
|
||||
F: Fn(Option<&T>) -> T,
|
||||
T: 'static,
|
||||
F: Fn(Option<T>) -> (T, bool),
|
||||
{
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
@@ -551,24 +691,16 @@ where
|
||||
)
|
||||
)]
|
||||
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool {
|
||||
let (new_value, is_different) = {
|
||||
let value = value.borrow();
|
||||
let curr_value = value
|
||||
.downcast_ref::<Option<T>>()
|
||||
.expect("to downcast memo value");
|
||||
let mut value = value.borrow_mut();
|
||||
let curr_value = value
|
||||
.downcast_mut::<Option<T>>()
|
||||
.expect("to downcast memo value");
|
||||
|
||||
// run the effect
|
||||
let new_value = (self.f)(curr_value.as_ref());
|
||||
let is_different = curr_value.as_ref() != Some(&new_value);
|
||||
(new_value, is_different)
|
||||
};
|
||||
if is_different {
|
||||
let mut value = value.borrow_mut();
|
||||
let curr_value = value
|
||||
.downcast_mut::<Option<T>>()
|
||||
.expect("to downcast memo value");
|
||||
*curr_value = Some(new_value);
|
||||
}
|
||||
// run the memo
|
||||
let (new_value, is_different) = (self.f)(curr_value.take());
|
||||
|
||||
// set new value
|
||||
*curr_value = Some(new_value);
|
||||
|
||||
is_different
|
||||
}
|
||||
|
||||
@@ -440,7 +440,7 @@ impl<'a> From<Oco<'a, str>> for Oco<'a, [u8]> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned from [`Oco::try_from`] for unsuccessful
|
||||
/// Error returned from `Oco::try_from` for unsuccessful
|
||||
/// conversion from `Oco<'_, [u8]>` to `Oco<'_, str>`.
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
#[error("invalid utf-8 sequence: {_0}")]
|
||||
|
||||
@@ -1003,11 +1003,11 @@ where
|
||||
/// // when we read the signal, it contains either
|
||||
/// // 1) None (if the Future isn't ready yet) or
|
||||
/// // 2) Some(T) (if the future's already resolved)
|
||||
/// assert_eq!(cats.read(), Some(vec!["1".to_string()]));
|
||||
/// assert_eq!(cats.get(), Some(vec!["1".to_string()]));
|
||||
///
|
||||
/// // when the signal's value changes, the `Resource` will generate and run a new `Future`
|
||||
/// set_how_many_cats.set(2);
|
||||
/// assert_eq!(cats.read(), Some(vec!["2".to_string()]));
|
||||
/// assert_eq!(cats.get(), Some(vec!["2".to_string()]));
|
||||
/// # }
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
|
||||
@@ -1201,12 +1201,12 @@ impl RuntimeId {
|
||||
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub(crate) fn create_memo<T>(
|
||||
pub(crate) fn create_owning_memo<T>(
|
||||
self,
|
||||
f: impl Fn(Option<&T>) -> T + 'static,
|
||||
f: impl Fn(Option<T>) -> (T, bool) + 'static,
|
||||
) -> Memo<T>
|
||||
where
|
||||
T: PartialEq + Any + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
Memo {
|
||||
id: self.create_concrete_memo(
|
||||
@@ -1397,12 +1397,30 @@ impl Drop for SetObserverOnDrop {
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the runtime has already been disposed.
|
||||
///
|
||||
/// To avoid panicking under any circumstances, use [`try_batch`].
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, features = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[inline(always)]
|
||||
pub fn batch<T>(f: impl FnOnce() -> T) -> T {
|
||||
try_batch(f).expect(
|
||||
"tried to run a batched update in a runtime that has been disposed",
|
||||
)
|
||||
}
|
||||
|
||||
/// Attempts to batch any reactive updates, preventing effects from running until the whole
|
||||
/// function has run. This allows you to prevent rerunning effects if multiple
|
||||
/// signal updates might cause the same effect to run.
|
||||
///
|
||||
/// Unlike [`batch`], this will not panic if the runtime has been disposed.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, features = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[inline(always)]
|
||||
pub fn try_batch<T>(f: impl FnOnce() -> T) -> Result<T, ReactiveSystemError> {
|
||||
with_runtime(move |runtime| {
|
||||
let batching = SetBatchingOnDrop(runtime.batching.get());
|
||||
runtime.batching.set(true);
|
||||
@@ -1415,7 +1433,6 @@ pub fn batch<T>(f: impl FnOnce() -> T) -> T {
|
||||
runtime.run_effects();
|
||||
val
|
||||
})
|
||||
.expect("tried to run a batched update in a runtime that has been disposed")
|
||||
}
|
||||
|
||||
struct SetBatchingOnDrop(bool);
|
||||
|
||||
@@ -212,3 +212,84 @@ fn dynamic_dependencies() {
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owning_memo_slice() {
|
||||
use std::rc::Rc;
|
||||
let runtime = create_runtime();
|
||||
|
||||
// this could be serialized to and from localstorage with miniserde
|
||||
pub struct State {
|
||||
name: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
let state = create_rw_signal(State {
|
||||
name: "Alice".to_owned(),
|
||||
token: "is this a token????".to_owned(),
|
||||
});
|
||||
|
||||
// We can allocate only when `state.name` changes
|
||||
let name = create_owning_memo(move |old_name| {
|
||||
state.with(move |state| {
|
||||
if let Some(name) =
|
||||
old_name.filter(|old_name| old_name == &state.name)
|
||||
{
|
||||
(name, false)
|
||||
} else {
|
||||
(state.name.clone(), true)
|
||||
}
|
||||
})
|
||||
});
|
||||
let set_name = move |name| state.update(|state| state.name = name);
|
||||
|
||||
// We can also re-use the last token allocation, which may be even better if the tokens are
|
||||
// always of the same length
|
||||
let token = create_owning_memo(move |old_token| {
|
||||
state.with(move |state| {
|
||||
let is_different = old_token.as_ref() != Some(&state.token);
|
||||
let mut token = old_token.unwrap_or_else(String::new);
|
||||
|
||||
if is_different {
|
||||
token.clone_from(&state.token);
|
||||
}
|
||||
(token, is_different)
|
||||
})
|
||||
});
|
||||
let set_token =
|
||||
move |new_token| state.update(|state| state.token = new_token);
|
||||
|
||||
let count_name_updates = Rc::new(std::cell::Cell::new(0));
|
||||
assert_eq!(count_name_updates.get(), 0);
|
||||
create_isomorphic_effect({
|
||||
let count_name_updates = Rc::clone(&count_name_updates);
|
||||
move |_| {
|
||||
name.track();
|
||||
count_name_updates.set(count_name_updates.get() + 1);
|
||||
}
|
||||
});
|
||||
assert_eq!(count_name_updates.get(), 1);
|
||||
|
||||
let count_token_updates = Rc::new(std::cell::Cell::new(0));
|
||||
assert_eq!(count_token_updates.get(), 0);
|
||||
create_isomorphic_effect({
|
||||
let count_token_updates = Rc::clone(&count_token_updates);
|
||||
move |_| {
|
||||
token.track();
|
||||
count_token_updates.set(count_token_updates.get() + 1);
|
||||
}
|
||||
});
|
||||
assert_eq!(count_token_updates.get(), 1);
|
||||
|
||||
set_name("Bob".to_owned());
|
||||
name.with(|name| assert_eq!(name, "Bob"));
|
||||
assert_eq!(count_name_updates.get(), 2);
|
||||
assert_eq!(count_token_updates.get(), 1);
|
||||
|
||||
set_token("this is not a token!".to_owned());
|
||||
token.with(|token| assert_eq!(token, "this is not a token!"));
|
||||
assert_eq!(count_name_updates.get(), 2);
|
||||
assert_eq!(count_token_updates.get(), 2);
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "RPC for the Leptos web framework."
|
||||
readme = "../README.md"
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
leptos_reactive = { workspace = true }
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
//use crate::{ServerFn, ServerFnError};
|
||||
#[cfg(debug_assertions)]
|
||||
use leptos_reactive::console_warn;
|
||||
use leptos_reactive::{
|
||||
batch, create_rw_signal, is_suppressing_resource_load, signal_prelude::*,
|
||||
spawn_local, store_value, use_context, ReadSignal, RwSignal, StoredValue,
|
||||
create_rw_signal, is_suppressing_resource_load, signal_prelude::*,
|
||||
spawn_local, store_value, try_batch, use_context, ReadSignal, RwSignal,
|
||||
StoredValue,
|
||||
};
|
||||
use server_fn::{error::ServerFnUrlError, ServerFn, ServerFnError};
|
||||
use std::{cell::Cell, future::Future, pin::Pin, rc::Rc};
|
||||
@@ -93,14 +96,24 @@ where
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn dispatch(&self, input: I) {
|
||||
self.0.with_value(|a| a.dispatch(input))
|
||||
#[cfg(debug_assertions)]
|
||||
let loc = std::panic::Location::caller();
|
||||
|
||||
self.0.with_value(|a| {
|
||||
a.dispatch(
|
||||
input,
|
||||
#[cfg(debug_assertions)]
|
||||
loc,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Create an [Action].
|
||||
///
|
||||
/// [Action] is a type of [Signal] which represent imperative calls to
|
||||
/// an asynchronous function. Where a [Resource] is driven as a function
|
||||
/// an asynchronous function. Where a [Resource](leptos_reactive::Resource) is driven as a function
|
||||
/// of a [Signal], [Action]s are [Action::dispatch]ed by events or handlers.
|
||||
///
|
||||
/// ```rust
|
||||
@@ -229,7 +242,7 @@ impl<I> Action<I, Result<I::Output, ServerFnError<I::Error>>>
|
||||
where
|
||||
I: ServerFn + 'static,
|
||||
{
|
||||
/// Create an [Action] to imperatively call a [server_fn::server] function.
|
||||
/// Create an [Action] to imperatively call a [server](leptos_macro::server) function.
|
||||
///
|
||||
/// The struct representing your server function's arguments should be
|
||||
/// provided to the [Action]. Unless specified as an argument to the server
|
||||
@@ -366,7 +379,11 @@ where
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn dispatch(&self, input: I) {
|
||||
pub fn dispatch(
|
||||
&self,
|
||||
input: I,
|
||||
#[cfg(debug_assertions)] loc: &'static std::panic::Location<'static>,
|
||||
) {
|
||||
if !is_suppressing_resource_load() {
|
||||
let fut = (self.action_fn)(&input);
|
||||
self.input.set(Some(input));
|
||||
@@ -379,7 +396,7 @@ where
|
||||
pending_dispatches.set(pending_dispatches.get().saturating_sub(1));
|
||||
spawn_local(async move {
|
||||
let new_value = fut.await;
|
||||
batch(move || {
|
||||
let res = try_batch(move || {
|
||||
value.set(Some(new_value));
|
||||
input.set(None);
|
||||
version.update(|n| *n += 1);
|
||||
@@ -389,6 +406,18 @@ where
|
||||
pending.set(false);
|
||||
}
|
||||
});
|
||||
|
||||
if res.is_err() {
|
||||
#[cfg(debug_assertions)]
|
||||
console_warn(&format!(
|
||||
"At {loc}, you are dispatching an action in a runtime \
|
||||
that has already been disposed. This may be because \
|
||||
you are calling `.dispatch()` in the body of a \
|
||||
component, during initial server-side rendering. If \
|
||||
that's the case, you should probably be using \
|
||||
`create_resource` instead of `create_action`."
|
||||
));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.6.5"
|
||||
version = "0.6.8"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Tools to set HTML metadata in the Leptos web framework."
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "1"
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
//!
|
||||
//! #[component]
|
||||
//! fn MyApp() -> impl IntoView {
|
||||
//! // Provides a [`MetaContext`], if there is not already one provided.
|
||||
//! provide_meta_context();
|
||||
//!
|
||||
//! let (name, set_name) = create_signal("Alice".to_string());
|
||||
//!
|
||||
//! view! {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.6.5"
|
||||
version = "0.6.8"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Router for the Leptos web framework."
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
hooks::has_router, use_navigate, use_resolved_path, NavigateOptions,
|
||||
ToHref, Url,
|
||||
hooks::has_router, resolve_redirect_url, use_navigate, use_resolved_path,
|
||||
NavigateOptions, ToHref, Url,
|
||||
};
|
||||
use leptos::{
|
||||
html::form,
|
||||
@@ -447,8 +447,10 @@ where
|
||||
{
|
||||
let has_router = has_router();
|
||||
if !has_router {
|
||||
_ = server_fn::redirect::set_redirect_hook(|path: &str| {
|
||||
_ = window().location().set_href(path);
|
||||
_ = server_fn::redirect::set_redirect_hook(|loc: &str| {
|
||||
if let Some(url) = resolve_redirect_url(loc) {
|
||||
_ = window().location().set_href(&url.href());
|
||||
}
|
||||
});
|
||||
}
|
||||
let action_url = if let Some(url) = action.url() {
|
||||
@@ -478,6 +480,10 @@ where
|
||||
action.dispatch(new_input);
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Error converting form field into server function \
|
||||
arguments: {err:?}"
|
||||
);
|
||||
batch(move || {
|
||||
value.set(Some(Err(ServerFnError::Serialization(
|
||||
err.to_string(),
|
||||
@@ -541,8 +547,10 @@ where
|
||||
{
|
||||
let has_router = has_router();
|
||||
if !has_router {
|
||||
_ = server_fn::redirect::set_redirect_hook(|path: &str| {
|
||||
_ = window().location().set_href(path);
|
||||
_ = server_fn::redirect::set_redirect_hook(|loc: &str| {
|
||||
if let Some(url) = resolve_redirect_url(loc) {
|
||||
_ = window().location().set_href(&url.href());
|
||||
}
|
||||
});
|
||||
}
|
||||
let action_url = if let Some(url) = action.url() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
matching::{resolve_path, PathMatch, RouteDefinition, RouteMatch},
|
||||
ParamsMap, RouterContext, SsrMode, StaticData, StaticMode, StaticParamsMap,
|
||||
TrailingSlash,
|
||||
};
|
||||
use leptos::{leptos_dom::Transparent, *};
|
||||
use std::{
|
||||
@@ -17,6 +18,16 @@ thread_local! {
|
||||
static ROUTE_ID: Cell<usize> = const { Cell::new(0) };
|
||||
}
|
||||
|
||||
// RouteDefinition.id is `pub` and required to be unique.
|
||||
// Should we make this public so users can generate unique IDs?
|
||||
pub(in crate::components) fn new_route_id() -> usize {
|
||||
ROUTE_ID.with(|id| {
|
||||
let next = id.get() + 1;
|
||||
id.set(next);
|
||||
next
|
||||
})
|
||||
}
|
||||
|
||||
/// Represents an HTTP method that can be handled by this route.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub enum Method {
|
||||
@@ -65,6 +76,11 @@ pub fn Route<E, F, P>(
|
||||
/// accessed with [`use_route_data`](crate::use_route_data).
|
||||
#[prop(optional, into)]
|
||||
data: Option<Loader>,
|
||||
/// How this route should handle trailing slashes in its path.
|
||||
/// Overrides any setting applied to [`crate::components::Router`].
|
||||
/// Serves as a default for any inner Routes.
|
||||
#[prop(optional)]
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
/// `children` may be empty or include nested routes.
|
||||
#[prop(optional)]
|
||||
children: Option<Children>,
|
||||
@@ -83,6 +99,7 @@ where
|
||||
data,
|
||||
None,
|
||||
None,
|
||||
trailing_slash,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -115,6 +132,11 @@ pub fn ProtectedRoute<P, E, F, C>(
|
||||
/// accessed with [`use_route_data`](crate::use_route_data).
|
||||
#[prop(optional, into)]
|
||||
data: Option<Loader>,
|
||||
/// How this route should handle trailing slashes in its path.
|
||||
/// Overrides any setting applied to [`crate::components::Router`].
|
||||
/// Serves as a default for any inner Routes.
|
||||
#[prop(optional)]
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
/// `children` may be empty or include nested routes.
|
||||
#[prop(optional)]
|
||||
children: Option<Children>,
|
||||
@@ -143,6 +165,7 @@ where
|
||||
data,
|
||||
None,
|
||||
None,
|
||||
trailing_slash,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -171,6 +194,11 @@ pub fn StaticRoute<E, F, P, S>(
|
||||
/// accessed with [`use_route_data`](crate::use_route_data).
|
||||
#[prop(optional, into)]
|
||||
data: Option<Loader>,
|
||||
/// How this route should handle trailing slashes in its path.
|
||||
/// Overrides any setting applied to [`crate::components::Router`].
|
||||
/// Serves as a default for any inner Routes.
|
||||
#[prop(optional)]
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
/// `children` may be empty or include nested routes.
|
||||
#[prop(optional)]
|
||||
children: Option<Children>,
|
||||
@@ -193,6 +221,7 @@ where
|
||||
data,
|
||||
Some(mode),
|
||||
Some(Arc::new(static_params)),
|
||||
trailing_slash,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -210,6 +239,7 @@ pub(crate) fn define_route(
|
||||
data: Option<Loader>,
|
||||
static_mode: Option<StaticMode>,
|
||||
static_params: Option<StaticData>,
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
) -> RouteDefinition {
|
||||
let children = children
|
||||
.map(|children| {
|
||||
@@ -226,14 +256,8 @@ pub(crate) fn define_route(
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let id = ROUTE_ID.with(|id| {
|
||||
let next = id.get() + 1;
|
||||
id.set(next);
|
||||
next
|
||||
});
|
||||
|
||||
RouteDefinition {
|
||||
id,
|
||||
id: new_route_id(),
|
||||
path,
|
||||
children,
|
||||
view,
|
||||
@@ -242,6 +266,7 @@ pub(crate) fn define_route(
|
||||
data,
|
||||
static_mode,
|
||||
static_params,
|
||||
trailing_slash,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,6 +410,11 @@ impl RouteContext {
|
||||
pub fn outlet(&self) -> impl IntoView {
|
||||
(self.inner.outlet)()
|
||||
}
|
||||
|
||||
/// The http method used to navigate to this route. Defaults to [`Method::Get`] when unavailable like in client side routing
|
||||
pub fn method(&self) -> Method {
|
||||
use_context().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RouteContextInner {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
create_location, matching::resolve_path, scroll_to_el, use_location,
|
||||
use_navigate, Branch, History, Location, LocationChange, RouteContext,
|
||||
RouterIntegrationContext, State,
|
||||
create_location, matching::resolve_path, resolve_redirect_url,
|
||||
scroll_to_el, use_location, use_navigate, Branch, History, Location,
|
||||
LocationChange, RouteContext, RouterIntegrationContext, State,
|
||||
};
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
use crate::{unescape, Url};
|
||||
@@ -24,6 +24,7 @@ use std::{
|
||||
use thiserror::Error;
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
static GLOBAL_ROUTERS_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
@@ -40,13 +41,16 @@ pub fn Router(
|
||||
/// A signal that will be set while the navigation process is underway.
|
||||
#[prop(optional, into)]
|
||||
set_is_routing: Option<SignalSetter<bool>>,
|
||||
/// How trailing slashes should be handled in [`Route`] paths.
|
||||
#[prop(optional)]
|
||||
trailing_slash: TrailingSlash,
|
||||
/// The `<Router/>` should usually wrap your whole page. It can contain
|
||||
/// any elements, and should include a [`Routes`](crate::Routes) component somewhere
|
||||
/// to define and display [`Route`](crate::Route)s.
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
// create a new RouterContext and provide it to every component beneath the router
|
||||
let router = RouterContext::new(base, fallback);
|
||||
let router = RouterContext::new(base, fallback, trailing_slash);
|
||||
provide_context(router);
|
||||
provide_context(GlobalSuspenseContext::new());
|
||||
if let Some(set_is_routing) = set_is_routing {
|
||||
@@ -56,15 +60,24 @@ pub fn Router(
|
||||
// set server function redirect hook
|
||||
let navigate = use_navigate();
|
||||
let navigate = SendWrapper::new(navigate);
|
||||
let router_hook = Box::new(move |path: &str| {
|
||||
let path = path.to_string();
|
||||
// delay by a tick here, so that the Action updates *before* the redirect
|
||||
request_animation_frame({
|
||||
let router_hook = Box::new(move |loc: &str| {
|
||||
let Some(url) = resolve_redirect_url(loc) else {
|
||||
return; // resolve_redirect_url() already logs an error
|
||||
};
|
||||
let current_origin =
|
||||
leptos_dom::helpers::location().origin().unwrap_throw();
|
||||
if url.origin() == current_origin {
|
||||
let navigate = navigate.clone();
|
||||
move || {
|
||||
navigate(&path, Default::default());
|
||||
}
|
||||
});
|
||||
// delay by a tick here, so that the Action updates *before* the redirect
|
||||
request_animation_frame(move || {
|
||||
navigate(&url.href(), Default::default());
|
||||
});
|
||||
// Use set_href() if the conditions for client-side navigation were not satisfied
|
||||
} else if let Err(e) =
|
||||
leptos_dom::helpers::location().set_href(&url.href())
|
||||
{
|
||||
leptos::logging::error!("Failed to redirect: {e:#?}");
|
||||
}
|
||||
}) as RedirectHook;
|
||||
_ = server_fn::redirect::set_redirect_hook(router_hook);
|
||||
|
||||
@@ -93,6 +106,7 @@ pub(crate) struct RouterContextInner {
|
||||
id: usize,
|
||||
pub location: Location,
|
||||
pub base: RouteContext,
|
||||
trailing_slash: TrailingSlash,
|
||||
pub possible_routes: RefCell<Option<Vec<Branch>>>,
|
||||
#[allow(unused)] // used in CSR/hydrate
|
||||
base_path: String,
|
||||
@@ -129,6 +143,7 @@ impl RouterContext {
|
||||
pub(crate) fn new(
|
||||
base: Option<&'static str>,
|
||||
fallback: Option<fn() -> View>,
|
||||
trailing_slash: TrailingSlash,
|
||||
) -> Self {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
@@ -210,6 +225,7 @@ impl RouterContext {
|
||||
path_stack: store_value(vec![location.pathname.get_untracked()]),
|
||||
location,
|
||||
base,
|
||||
trailing_slash,
|
||||
history: Box::new(history),
|
||||
|
||||
reference,
|
||||
@@ -248,6 +264,10 @@ impl RouterContext {
|
||||
self.inner.id
|
||||
}
|
||||
|
||||
pub(crate) fn trailing_slash(&self) -> TrailingSlash {
|
||||
self.inner.trailing_slash.clone()
|
||||
}
|
||||
|
||||
/// A list of all possible routes this router can match.
|
||||
pub fn possible_branches(&self) -> Vec<Branch> {
|
||||
self.inner
|
||||
@@ -505,3 +525,64 @@ impl Default for NavigateOptions {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Declares how you would like to handle trailing slashes in Route paths. This
|
||||
/// can be set on [`Router`] and overridden in [`crate::components::Route`]
|
||||
#[derive(Default, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum TrailingSlash {
|
||||
/// This is the default behavior as of Leptos 0.5. Trailing slashes in your
|
||||
/// `Route` path are stripped. i.e.: the following two route declarations
|
||||
/// are equivalent:
|
||||
/// * `<Route path="/foo">`
|
||||
/// * `<Route path="/foo/">`
|
||||
#[default]
|
||||
Drop,
|
||||
|
||||
/// This mode will respect your path as it is written. Ex:
|
||||
/// * If you specify `<Route path="/foo">`, then `/foo` matches, but
|
||||
/// `/foo/` does not.
|
||||
/// * If you specify `<Route path="/foo/">`, then `/foo/` matches, but
|
||||
/// `/foo` does not.
|
||||
Exact,
|
||||
|
||||
/// Like `Exact`, this mode respects your path as-written. But it will also
|
||||
/// add redirects to the specified path if a user nagivates to a URL that is
|
||||
/// off by only the trailing slash.
|
||||
///
|
||||
/// Given `<Route path="/foo">`
|
||||
/// * Visiting `/foo` is valid.
|
||||
/// * Visiting `/foo/` serves a redirect to `/foo`
|
||||
///
|
||||
/// Given `<Route path="/foo/">`
|
||||
/// * Visiting `/foo` serves a redirect to `/foo/`
|
||||
/// * Visiting `/foo/` is valid.
|
||||
Redirect,
|
||||
}
|
||||
|
||||
impl TrailingSlash {
|
||||
/// Should we redirect requests that come in with the wrong (extra/missing) trailng slash?
|
||||
pub(crate) fn should_redirect(&self) -> bool {
|
||||
use TrailingSlash::*;
|
||||
match self {
|
||||
Redirect => true,
|
||||
Drop | Exact => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_route_path(&self, path: &mut String) {
|
||||
if !self.should_drop() {
|
||||
return;
|
||||
}
|
||||
while path.ends_with('/') {
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
|
||||
fn should_drop(&self) -> bool {
|
||||
use TrailingSlash::*;
|
||||
match self {
|
||||
Redirect | Exact => false,
|
||||
Drop => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use crate::{
|
||||
animation::*,
|
||||
components::route::new_route_id,
|
||||
matching::{
|
||||
expand_optionals, get_route_matches, join_paths, Branch, Matcher,
|
||||
RouteDefinition, RouteMatch,
|
||||
},
|
||||
use_is_back_navigation, RouteContext, RouterContext, SetIsRouting,
|
||||
use_is_back_navigation, use_route, NavigateOptions, Redirect, RouteContext,
|
||||
RouterContext, SetIsRouting, TrailingSlash,
|
||||
};
|
||||
use leptos::{leptos_dom::HydrationCtx, *};
|
||||
use std::{
|
||||
@@ -82,7 +84,7 @@ pub fn Routes(
|
||||
let base_route = router.base();
|
||||
let base = base.unwrap_or_default();
|
||||
|
||||
Branches::initialize(router_id, &base, children());
|
||||
Branches::initialize(&router, &base, children());
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(context) = use_context::<crate::PossibleBranchContext>() {
|
||||
@@ -164,7 +166,7 @@ pub fn AnimatedRoutes(
|
||||
let base_route = router.base();
|
||||
let base = base.unwrap_or_default();
|
||||
|
||||
Branches::initialize(router_id, &base, children());
|
||||
Branches::initialize(&router, &base, children());
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(context) = use_context::<crate::PossibleBranchContext>() {
|
||||
@@ -278,7 +280,7 @@ thread_local! {
|
||||
}
|
||||
|
||||
impl Branches {
|
||||
pub fn initialize(router_id: usize, base: &str, children: Fragment) {
|
||||
pub fn initialize(router: &RouterContext, base: &str, children: Fragment) {
|
||||
BRANCHES.with(|branches| {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
@@ -293,9 +295,9 @@ impl Branches {
|
||||
}
|
||||
|
||||
let mut current = branches.borrow_mut();
|
||||
if !current.contains_key(&(router_id, Cow::from(base))) {
|
||||
if !current.contains_key(&(router.id(), Cow::from(base))) {
|
||||
let mut branches = Vec::new();
|
||||
let children = children
|
||||
let mut children = children
|
||||
.as_children()
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
@@ -315,6 +317,7 @@ impl Branches {
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
inherit_settings(&mut children, router);
|
||||
create_branches(
|
||||
&children,
|
||||
base,
|
||||
@@ -323,7 +326,7 @@ impl Branches {
|
||||
true,
|
||||
base,
|
||||
);
|
||||
current.insert((router_id, Cow::Owned(base.into())), branches);
|
||||
current.insert((router.id(), Cow::Owned(base.into())), branches);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -344,6 +347,36 @@ impl Branches {
|
||||
}
|
||||
}
|
||||
|
||||
// <Route>s may inherit settings from each other or <Router>.
|
||||
// This mutates RouteDefinitions to propagate those settings.
|
||||
fn inherit_settings(children: &mut [RouteDefinition], router: &RouterContext) {
|
||||
struct InheritProps {
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
}
|
||||
fn route_def_inherit(
|
||||
children: &mut [RouteDefinition],
|
||||
inherited: InheritProps,
|
||||
) {
|
||||
for child in children {
|
||||
if child.trailing_slash.is_none() {
|
||||
child.trailing_slash = inherited.trailing_slash.clone();
|
||||
}
|
||||
route_def_inherit(
|
||||
&mut child.children,
|
||||
InheritProps {
|
||||
trailing_slash: child.trailing_slash.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
route_def_inherit(
|
||||
children,
|
||||
InheritProps {
|
||||
trailing_slash: Some(router.trailing_slash()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn route_states(
|
||||
router_id: usize,
|
||||
base: String,
|
||||
@@ -553,6 +586,7 @@ pub(crate) struct RouterState {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RouteData {
|
||||
// This ID is always the same as key.id. Deprecate?
|
||||
pub id: usize,
|
||||
pub key: RouteDefinition,
|
||||
pub pattern: String,
|
||||
@@ -647,24 +681,110 @@ fn create_routes(
|
||||
parents_path, route_def.path
|
||||
);
|
||||
}
|
||||
let trailing_slash = route_def
|
||||
.trailing_slash
|
||||
.clone()
|
||||
.expect("trailng_slash should be set by this point");
|
||||
let mut acc = Vec::new();
|
||||
for original_path in expand_optionals(&route_def.path) {
|
||||
let path = join_paths(base, &original_path);
|
||||
let mut path = join_paths(base, &original_path).to_string();
|
||||
trailing_slash.normalize_route_path(&mut path);
|
||||
let pattern = if is_leaf {
|
||||
path
|
||||
} else if let Some((path, _splat)) = path.split_once("/*") {
|
||||
path.to_string()
|
||||
} else {
|
||||
path.split("/*")
|
||||
.next()
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or(path)
|
||||
path
|
||||
};
|
||||
acc.push(RouteData {
|
||||
|
||||
let route_data = RouteData {
|
||||
key: route_def.clone(),
|
||||
id: route_def.id,
|
||||
matcher: Matcher::new_with_partial(&pattern, !is_leaf),
|
||||
pattern,
|
||||
original_path: original_path.into_owned(),
|
||||
});
|
||||
};
|
||||
|
||||
if route_data.matcher.is_wildcard() {
|
||||
// already handles trailing_slash
|
||||
} else if let Some(redirect_route) = redirect_route_for(route_def) {
|
||||
let pattern = &redirect_route.path;
|
||||
let redirect_route_data = RouteData {
|
||||
id: redirect_route.id,
|
||||
matcher: Matcher::new_with_partial(pattern, !is_leaf),
|
||||
pattern: pattern.to_owned(),
|
||||
original_path: pattern.to_owned(),
|
||||
key: redirect_route,
|
||||
};
|
||||
acc.push(redirect_route_data);
|
||||
}
|
||||
|
||||
acc.push(route_data);
|
||||
}
|
||||
acc
|
||||
}
|
||||
|
||||
/// A new route that redirects to `route` with the correct trailng slash.
|
||||
fn redirect_route_for(route: &RouteDefinition) -> Option<RouteDefinition> {
|
||||
if matches!(route.path.as_str(), "" | "/") {
|
||||
// Root paths are an exception to the rule and are always equivalent:
|
||||
return None;
|
||||
}
|
||||
|
||||
let trailing_slash = route
|
||||
.trailing_slash
|
||||
.clone()
|
||||
.expect("trailing_slash should be defined by now");
|
||||
|
||||
if !trailing_slash.should_redirect() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Are we creating a new route that adds or removes a slash?
|
||||
let add_slash = route.path.ends_with('/');
|
||||
let view = Rc::new(move || {
|
||||
view! {
|
||||
<FixTrailingSlash add_slash />
|
||||
}
|
||||
.into_view()
|
||||
});
|
||||
|
||||
let new_pattern = if add_slash {
|
||||
// If we need to add a slash, we need to match on the path w/o it:
|
||||
route.path.trim_end_matches('/').to_string()
|
||||
} else {
|
||||
format!("{}/", route.path)
|
||||
};
|
||||
let new_route = RouteDefinition {
|
||||
path: new_pattern,
|
||||
children: vec![],
|
||||
data: None,
|
||||
methods: route.methods,
|
||||
id: new_route_id(),
|
||||
view,
|
||||
ssr_mode: route.ssr_mode,
|
||||
static_mode: route.static_mode,
|
||||
static_params: None,
|
||||
trailing_slash: None, // Shouldn't be needed/used from here on out
|
||||
};
|
||||
|
||||
Some(new_route)
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn FixTrailingSlash(add_slash: bool) -> impl IntoView {
|
||||
let route = use_route();
|
||||
let path = if add_slash {
|
||||
format!("{}/", route.path())
|
||||
} else {
|
||||
route.path().trim_end_matches('/').to_string()
|
||||
};
|
||||
let options = NavigateOptions {
|
||||
replace: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
view! {
|
||||
<Redirect path options/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
mod test_extract_routes;
|
||||
|
||||
use crate::{
|
||||
Branch, Method, RouterIntegrationContext, ServerIntegration, SsrMode,
|
||||
StaticDataMap, StaticMode, StaticParamsMap, StaticPath,
|
||||
provide_server_redirect, Branch, Method, RouterIntegrationContext,
|
||||
ServerIntegration, SsrMode, StaticDataMap, StaticMode, StaticParamsMap,
|
||||
StaticPath,
|
||||
};
|
||||
use leptos::*;
|
||||
use std::{
|
||||
@@ -42,6 +45,9 @@ impl RouteListing {
|
||||
}
|
||||
|
||||
/// The path this route handles.
|
||||
///
|
||||
/// This should be formatted for whichever web server integegration is being used. (ex: leptos-actix.)
|
||||
/// When returned from leptos-router, it matches `self.leptos_path()`.
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
@@ -128,21 +134,9 @@ where
|
||||
{
|
||||
let runtime = create_runtime();
|
||||
|
||||
let integration = ServerIntegration {
|
||||
path: "http://leptos.rs/".to_string(),
|
||||
};
|
||||
|
||||
provide_context(RouterIntegrationContext::new(integration));
|
||||
let branches = PossibleBranchContext::default();
|
||||
provide_context(branches.clone());
|
||||
|
||||
additional_context();
|
||||
|
||||
leptos::suppress_resource_load(true);
|
||||
_ = app_fn().into_view();
|
||||
leptos::suppress_resource_load(false);
|
||||
|
||||
let branches = get_branches(app_fn, additional_context);
|
||||
let branches = branches.0.borrow();
|
||||
|
||||
let mut static_data_map: StaticDataMap = HashMap::new();
|
||||
let routes = branches
|
||||
.iter()
|
||||
@@ -182,3 +176,29 @@ where
|
||||
runtime.dispose();
|
||||
(routes, static_data_map)
|
||||
}
|
||||
|
||||
fn get_branches<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
additional_context: impl Fn() + 'static + Clone,
|
||||
) -> PossibleBranchContext
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let integration = ServerIntegration {
|
||||
path: "http://leptos.rs/".to_string(),
|
||||
};
|
||||
|
||||
provide_context(RouterIntegrationContext::new(integration));
|
||||
let branches = PossibleBranchContext::default();
|
||||
provide_context(branches.clone());
|
||||
// Suppress startup warning about using <Redirect/> without ServerRedirectFunction:
|
||||
provide_server_redirect(|_str| ());
|
||||
|
||||
additional_context();
|
||||
|
||||
leptos::suppress_resource_load(true);
|
||||
_ = app_fn().into_view();
|
||||
leptos::suppress_resource_load(false);
|
||||
|
||||
branches
|
||||
}
|
||||
|
||||
258
router/src/extract_routes/test_extract_routes.rs
Normal file
258
router/src/extract_routes/test_extract_routes.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
// This is here, vs /router/tests/, because it accesses some `pub(crate)`
|
||||
// features to test crate internals that wouldn't be available there.
|
||||
|
||||
#![cfg(all(test, feature = "ssr"))]
|
||||
|
||||
use crate::*;
|
||||
use itertools::Itertools;
|
||||
use leptos::*;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
#[component]
|
||||
fn DefaultApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
view! {
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/name/:name/" view/>
|
||||
<Route path="/any/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ExactApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
let trailing_slash = TrailingSlash::Exact;
|
||||
view! {
|
||||
<Router trailing_slash>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/name/:name/" view/>
|
||||
<Route path="/any/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn RedirectApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
let trailing_slash = TrailingSlash::Redirect;
|
||||
view! {
|
||||
<Router trailing_slash>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/name/:name/" view/>
|
||||
<Route path="/any/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_generated_routes_default() {
|
||||
// By default, we use the behavior as of Leptos 0.5, which is equivalent to TrailingSlash::Drop.
|
||||
assert_generated_paths(
|
||||
DefaultApp,
|
||||
&["/any/*any", "/bar", "/baz/:id", "/foo", "/name/:name"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generated_routes_exact() {
|
||||
// Allow users to precisely define whether slashes are present:
|
||||
assert_generated_paths(
|
||||
ExactApp,
|
||||
&["/any/*any", "/bar/", "/baz/:id", "/foo", "/name/:name/"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generated_routes_redirect() {
|
||||
// TralingSlashes::Redirect generates paths to redirect to the path with the "correct" trailing slash ending (or lack thereof).
|
||||
assert_generated_paths(
|
||||
RedirectApp,
|
||||
&[
|
||||
"/any/*any",
|
||||
"/bar",
|
||||
"/bar/",
|
||||
"/baz/:id",
|
||||
"/baz/:id/",
|
||||
"/foo",
|
||||
"/foo/",
|
||||
"/name/:name",
|
||||
"/name/:name/",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rendered_redirect() {
|
||||
// Given an app that uses TrailngSlsahes::Redirect, rendering the redirected path
|
||||
// should render the redirect. Other paths should not.
|
||||
|
||||
let expected_redirects = &[
|
||||
("/bar", "/bar/"),
|
||||
("/baz/some_id/", "/baz/some_id"),
|
||||
("/name/some_name", "/name/some_name/"),
|
||||
("/foo/", "/foo"),
|
||||
];
|
||||
|
||||
let redirect_result = Rc::new(RefCell::new(Option::None));
|
||||
let rc = redirect_result.clone();
|
||||
let server_redirect = move |new_value: &str| {
|
||||
rc.replace(Some(new_value.to_string()));
|
||||
};
|
||||
|
||||
let _runtime = Disposable(create_runtime());
|
||||
let history = TestHistory::new("/");
|
||||
provide_context(RouterIntegrationContext::new(history.clone()));
|
||||
provide_server_redirect(server_redirect);
|
||||
|
||||
// We expect these redirects to exist:
|
||||
for (src, dest) in expected_redirects {
|
||||
let loc = format!("https://example.com{src}");
|
||||
history.goto(&loc);
|
||||
redirect_result.replace(None);
|
||||
RedirectApp().into_view().render_to_string();
|
||||
let redirected_to = redirect_result.borrow().clone();
|
||||
assert!(
|
||||
redirected_to.is_some(),
|
||||
"Should redirect from {src} to {dest}"
|
||||
);
|
||||
assert_eq!(redirected_to.unwrap(), *dest);
|
||||
}
|
||||
|
||||
// But the destination paths shouldn't themselves redirect:
|
||||
redirect_result.replace(None);
|
||||
for (_src, dest) in expected_redirects {
|
||||
let loc = format!("https://example.com{dest}");
|
||||
history.goto(&loc);
|
||||
RedirectApp().into_view().render_to_string();
|
||||
let redirected_to = redirect_result.borrow().clone();
|
||||
assert!(
|
||||
redirected_to.is_none(),
|
||||
"Destination of redirect shouldn't also redirect: {dest}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct Disposable(RuntimeId);
|
||||
|
||||
// If the test fails, and we don't dispose, we get irrelevant panics.
|
||||
impl Drop for Disposable {
|
||||
fn drop(&mut self) {
|
||||
self.0.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestHistory {
|
||||
loc: RwSignal<LocationChange>,
|
||||
}
|
||||
|
||||
impl TestHistory {
|
||||
fn new(initial: &str) -> Self {
|
||||
let lc = LocationChange {
|
||||
value: initial.to_owned(),
|
||||
..Default::default()
|
||||
};
|
||||
Self {
|
||||
loc: create_rw_signal(lc),
|
||||
}
|
||||
}
|
||||
|
||||
fn goto(&self, loc: &str) {
|
||||
let change = LocationChange {
|
||||
value: loc.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
self.navigate(&change);
|
||||
}
|
||||
}
|
||||
|
||||
impl History for TestHistory {
|
||||
fn location(&self) -> ReadSignal<LocationChange> {
|
||||
self.loc.read_only()
|
||||
}
|
||||
|
||||
fn navigate(&self, new_loc: &LocationChange) {
|
||||
self.loc.update(|loc| loc.value = new_loc.value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
// WARNING!
|
||||
//
|
||||
// Despite generate_route_list_inner() using a new leptos_reactive::RuntimeID
|
||||
// each time we call this function, somehow Routes are leaked between different
|
||||
// apps. To avoid that, make sure to put each call in a separate #[test] method.
|
||||
//
|
||||
// TODO: Better isolation for different apps to avoid this issue?
|
||||
fn assert_generated_paths<F, IV>(app: F, expected_sorted_paths: &[&str])
|
||||
where
|
||||
F: Clone + Fn() -> IV + 'static,
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let (routes, static_data) = generate_route_list_inner(app);
|
||||
|
||||
let mut paths = routes.iter().map(|route| route.path()).collect_vec();
|
||||
paths.sort();
|
||||
|
||||
assert_eq!(paths, expected_sorted_paths);
|
||||
|
||||
let mut keys = static_data.keys().collect_vec();
|
||||
keys.sort();
|
||||
assert_eq!(paths, keys);
|
||||
|
||||
// integrations can update "path" to be valid for themselves, but
|
||||
// when routes are returned by leptos_router, these are equal:
|
||||
assert!(routes
|
||||
.iter()
|
||||
.all(|route| route.path() == route.leptos_path()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unique_route_ids() {
|
||||
let branches = get_branches(RedirectApp);
|
||||
assert!(!branches.is_empty());
|
||||
|
||||
assert!(branches
|
||||
.iter()
|
||||
.flat_map(|branch| &branch.routes)
|
||||
.map(|route| route.id)
|
||||
.all_unique());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unique_route_patterns() {
|
||||
let branches = get_branches(RedirectApp);
|
||||
assert!(!branches.is_empty());
|
||||
|
||||
assert!(branches
|
||||
.iter()
|
||||
.flat_map(|branch| &branch.routes)
|
||||
.map(|route| route.pattern.as_str())
|
||||
.all_unique());
|
||||
}
|
||||
|
||||
fn get_branches<F, IV>(app_fn: F) -> Vec<Branch>
|
||||
where
|
||||
F: Fn() -> IV + Clone + 'static,
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let runtime = create_runtime();
|
||||
let additional_context = || ();
|
||||
let branches = super::get_branches(app_fn, additional_context);
|
||||
let branches = branches.0.borrow().clone();
|
||||
runtime.dispose();
|
||||
branches
|
||||
}
|
||||
@@ -3,8 +3,8 @@ use crate::{
|
||||
RouterContext,
|
||||
};
|
||||
use leptos::{
|
||||
create_memo, request_animation_frame, signal_prelude::*, use_context, Memo,
|
||||
Oco,
|
||||
create_memo, request_animation_frame, signal_prelude::*, use_context,
|
||||
window, Memo, Oco,
|
||||
};
|
||||
use std::{rc::Rc, str::FromStr};
|
||||
|
||||
@@ -216,3 +216,28 @@ pub(crate) fn use_is_back_navigation() -> ReadSignal<bool> {
|
||||
let router = use_router();
|
||||
router.inner.is_back.read_only()
|
||||
}
|
||||
|
||||
/// Resolves a redirect location to an (absolute) URL.
|
||||
pub(crate) fn resolve_redirect_url(loc: &str) -> Option<web_sys::Url> {
|
||||
let origin = match window().location().origin() {
|
||||
Ok(origin) => origin,
|
||||
Err(e) => {
|
||||
leptos::logging::error!("Failed to get origin: {:#?}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Use server function's URL as base instead.
|
||||
let base = origin;
|
||||
|
||||
match web_sys::Url::new_with_base(loc, &base) {
|
||||
Ok(url) => Some(url),
|
||||
Err(e) => {
|
||||
leptos::logging::error!(
|
||||
"Invalid redirect location: {}",
|
||||
e.as_string().unwrap_or_default(),
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,14 +31,8 @@ impl Matcher {
|
||||
Some((p, s)) => (p, Some(s.to_string())),
|
||||
None => (path, None),
|
||||
};
|
||||
let segments = pattern
|
||||
.split('/')
|
||||
.filter(|n| !n.is_empty())
|
||||
.map(|n| n.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let segments: Vec<String> = get_segments(pattern);
|
||||
let len = segments.len();
|
||||
|
||||
Self {
|
||||
splat,
|
||||
segments,
|
||||
@@ -49,10 +43,7 @@ impl Matcher {
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn test(&self, location: &str) -> Option<PathMatch> {
|
||||
let loc_segments = location
|
||||
.split('/')
|
||||
.filter(|n| !n.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
let loc_segments: Vec<&str> = get_segments(location);
|
||||
|
||||
let loc_len = loc_segments.len();
|
||||
let len_diff: i32 = loc_len as i32 - self.len as i32;
|
||||
@@ -107,4 +98,24 @@ impl Matcher {
|
||||
Some(PathMatch { path, params })
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(crate) fn is_wildcard(&self) -> bool {
|
||||
self.splat.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_segments<'a, S: From<&'a str>>(pattern: &'a str) -> Vec<S> {
|
||||
// URL root paths ("/" and "") are equivalent and treated as 0-segment paths.
|
||||
// non-root paths with trailing slashes get extra empty segment at the end.
|
||||
// This makes sure that segment matching is trailing-slash sensitive.
|
||||
let mut segments: Vec<S> = pattern
|
||||
.split('/')
|
||||
.filter(|p| !p.is_empty())
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
if !segments.is_empty() && pattern.ends_with('/') {
|
||||
segments.push("".into());
|
||||
}
|
||||
segments
|
||||
}
|
||||
|
||||
@@ -51,7 +51,14 @@ fn has_scheme(path: &str) -> bool {
|
||||
|
||||
#[doc(hidden)]
|
||||
fn normalize(path: &str, omit_slash: bool) -> Cow<'_, str> {
|
||||
let s = path.trim_start_matches('/').trim_end_matches('/');
|
||||
let s = path.trim_start_matches('/');
|
||||
let trim_end = s
|
||||
.chars()
|
||||
.rev()
|
||||
.take_while(|c| *c == '/')
|
||||
.count()
|
||||
.saturating_sub(1);
|
||||
let s = &s[0..s.len() - trim_end];
|
||||
if s.is_empty() || omit_slash || begins_with_query_or_hash(s) {
|
||||
s.into()
|
||||
} else {
|
||||
@@ -70,9 +77,10 @@ fn begins_with_query_or_hash(text: &str) -> bool {
|
||||
}
|
||||
|
||||
fn remove_wildcard(text: &str) -> String {
|
||||
text.split_once('*')
|
||||
.map(|(prefix, _)| prefix.trim_end_matches('/'))
|
||||
text.rsplit_once('*')
|
||||
.map(|(prefix, _)| prefix)
|
||||
.unwrap_or(text)
|
||||
.trim_end_matches('/')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
@@ -83,4 +91,14 @@ mod tests {
|
||||
fn normalize_query_string_with_opening_slash() {
|
||||
assert_eq!(normalize("/?foo=bar", false), "?foo=bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_retain_trailing_slash() {
|
||||
assert_eq!(normalize("foo/bar/", false), "/foo/bar/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_dedup_trailing_slashes() {
|
||||
assert_eq!(normalize("foo/bar/////", false), "/foo/bar/");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{Loader, Method, SsrMode, StaticData, StaticMode};
|
||||
use crate::{Loader, Method, SsrMode, StaticData, StaticMode, TrailingSlash};
|
||||
use leptos::leptos_dom::View;
|
||||
use std::rc::Rc;
|
||||
|
||||
@@ -25,6 +25,8 @@ pub struct RouteDefinition {
|
||||
pub static_mode: Option<StaticMode>,
|
||||
/// The data required to fill any dynamic segments in the path during static rendering.
|
||||
pub static_params: Option<StaticData>,
|
||||
/// How a trailng slash in `path` should be handled.
|
||||
pub trailing_slash: Option<TrailingSlash>,
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for RouteDefinition {
|
||||
@@ -34,6 +36,7 @@ impl core::fmt::Debug for RouteDefinition {
|
||||
.field("children", &self.children)
|
||||
.field("ssr_mode", &self.ssr_mode)
|
||||
.field("static_render", &self.static_mode)
|
||||
.field("trailing_slash", &self.trailing_slash)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,5 +44,14 @@ cfg_if! {
|
||||
assert_eq!(join_paths("/foo", ":bar/baz"), "/foo/:bar/baz");
|
||||
assert_eq!(join_paths("", ":bar/baz"), "/:bar/baz");
|
||||
}
|
||||
|
||||
// Additional tests NOT from Solid Router:
|
||||
#[test]
|
||||
fn join_paths_for_root() {
|
||||
assert_eq!(join_paths("", ""), "");
|
||||
assert_eq!(join_paths("", "/"), "");
|
||||
assert_eq!(join_paths("/", ""), "");
|
||||
assert_eq!(join_paths("/", "/"), "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
59
router/tests/trailing_slashes.rs
Normal file
59
router/tests/trailing_slashes.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
//! Some extra tests for Matcher NOT based on SolidJS's tests cases (as in matcher.rs)
|
||||
|
||||
use leptos_router::*;
|
||||
|
||||
#[test]
|
||||
fn trailing_slashes_match_exactly() {
|
||||
let matcher = Matcher::new("/foo/");
|
||||
assert_matches(&matcher, "/foo/");
|
||||
assert_no_match(&matcher, "/foo");
|
||||
|
||||
let matcher = Matcher::new("/foo/bar/");
|
||||
assert_matches(&matcher, "/foo/bar/");
|
||||
assert_no_match(&matcher, "/foo/bar");
|
||||
|
||||
let matcher = Matcher::new("/");
|
||||
assert_matches(&matcher, "/");
|
||||
assert_matches(&matcher, "");
|
||||
|
||||
let matcher = Matcher::new("");
|
||||
assert_matches(&matcher, "");
|
||||
|
||||
// Despite returning a pattern of "", web servers (known: Actix-Web and Axum)
|
||||
// may send us a path of "/". We should match those at the root:
|
||||
assert_matches(&matcher, "/");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn trailing_slashes_params_match_exactly() {
|
||||
let matcher = Matcher::new("/foo/:bar/");
|
||||
assert_matches(&matcher, "/foo/bar/");
|
||||
assert_matches(&matcher, "/foo/42/");
|
||||
assert_matches(&matcher, "/foo/%20/");
|
||||
|
||||
assert_no_match(&matcher, "/foo/bar");
|
||||
assert_no_match(&matcher, "/foo/42");
|
||||
assert_no_match(&matcher, "/foo/%20");
|
||||
|
||||
let m = matcher.test("/foo/asdf/").unwrap();
|
||||
assert_eq!(m.params, params_map! { "bar" => "asdf" });
|
||||
}
|
||||
|
||||
fn assert_matches(matcher: &Matcher, path: &str) {
|
||||
assert!(
|
||||
matches(matcher, path),
|
||||
"{matcher:?} should match path {path:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_no_match(matcher: &Matcher, path: &str) {
|
||||
assert!(
|
||||
!matches(matcher, path),
|
||||
"{matcher:?} should NOT match path {path:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn matches(m: &Matcher, loc: &str) -> bool {
|
||||
m.test(loc).is_some()
|
||||
}
|
||||
@@ -7,6 +7,7 @@ license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "RPC for any web framework."
|
||||
readme = "../README.md"
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
server_fn_macro_default = { workspace = true }
|
||||
@@ -27,7 +28,9 @@ once_cell = "1"
|
||||
actix-web = { version = "4", optional = true }
|
||||
|
||||
# axum
|
||||
axum = { version = "0.7", optional = true, default-features = false, features = ["multipart"] }
|
||||
axum = { version = "0.7", optional = true, default-features = false, features = [
|
||||
"multipart",
|
||||
] }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-layer = { version = "0.3", optional = true }
|
||||
|
||||
@@ -73,8 +76,6 @@ url = "2"
|
||||
|
||||
[features]
|
||||
default = ["json", "cbor"]
|
||||
form-redirects = []
|
||||
actix = ["ssr", "dep:actix-web", "dep:send_wrapper"]
|
||||
axum-no-default = [
|
||||
"ssr",
|
||||
"dep:axum",
|
||||
@@ -83,10 +84,9 @@ axum-no-default = [
|
||||
"dep:tower",
|
||||
"dep:tower-layer",
|
||||
]
|
||||
axum = [
|
||||
"axum/default",
|
||||
"axum-no-default",
|
||||
]
|
||||
form-redirects = []
|
||||
actix = ["ssr", "dep:actix-web", "dep:send_wrapper"]
|
||||
axum = ["axum/default", "axum-no-default"]
|
||||
browser = [
|
||||
"dep:gloo-net",
|
||||
"dep:js-sys",
|
||||
@@ -112,7 +112,21 @@ all-features = true
|
||||
|
||||
# disables some feature combos for testing in CI
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["rustls", "default-tls", "form-redirects"]
|
||||
denylist = [
|
||||
"rustls",
|
||||
"default-tls",
|
||||
"form-redirects",
|
||||
"gloo-net",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"send_wrapper",
|
||||
"ciborium",
|
||||
"hyper",
|
||||
"inventory",
|
||||
]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"actix",
|
||||
@@ -130,4 +144,48 @@ skip_feature_sets = [
|
||||
"browser",
|
||||
"reqwest",
|
||||
],
|
||||
[
|
||||
"default-tls",
|
||||
"rustls",
|
||||
],
|
||||
[
|
||||
"browser",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"axum-no-default",
|
||||
"actix",
|
||||
],
|
||||
[
|
||||
"axum-no-default",
|
||||
"browser",
|
||||
],
|
||||
[
|
||||
"rkyv",
|
||||
"json",
|
||||
],
|
||||
[
|
||||
"rkyv",
|
||||
"cbor",
|
||||
],
|
||||
[
|
||||
"rkyv",
|
||||
"url",
|
||||
],
|
||||
[
|
||||
"rkyv",
|
||||
"serde-lite",
|
||||
],
|
||||
[
|
||||
"url",
|
||||
"json",
|
||||
],
|
||||
[
|
||||
"url",
|
||||
"cbor",
|
||||
],
|
||||
[
|
||||
"url",
|
||||
"serde-lite",
|
||||
],
|
||||
]
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
//! 6. The server integration applies any middleware from [`ServerFn::middlewares`] and responds to the request.
|
||||
//! 7. [`FromRes`]: The client deserializes the response back into the [`ServerFn::Output`] type.
|
||||
//!
|
||||
//! [server]: <https://docs.rs/server_fn/latest/server_fn/attr.server.html>
|
||||
//! [server]: ../leptos/attr.server.html
|
||||
//! [`serde_qs`]: <https://docs.rs/serde_qs/latest/serde_qs/>
|
||||
//! [`cbor`]: <https://docs.rs/cbor/latest/cbor/>
|
||||
|
||||
@@ -472,7 +472,7 @@ pub mod axum {
|
||||
|
||||
/// Explicitly register a server function. This is only necessary if you are
|
||||
/// running the server in a WASM environment (or a rare environment that the
|
||||
/// `inventory`).
|
||||
/// `inventory` crate won't work in.).
|
||||
pub fn register_explicit<T>()
|
||||
where
|
||||
T: ServerFn<
|
||||
@@ -556,7 +556,7 @@ pub mod actix {
|
||||
|
||||
/// Explicitly register a server function. This is only necessary if you are
|
||||
/// running the server in a WASM environment (or a rare environment that the
|
||||
/// `inventory`).
|
||||
/// `inventory` crate won't work in.).
|
||||
pub fn register_explicit<T>()
|
||||
where
|
||||
T: ServerFn<
|
||||
|
||||
@@ -25,9 +25,9 @@ pub fn set_redirect_hook(
|
||||
REDIRECT_HOOK.set(Box::new(hook))
|
||||
}
|
||||
|
||||
/// Calls the hook that has been set by [`set_redirect_hook`] to redirect to `path`.
|
||||
pub fn call_redirect_hook(path: &str) {
|
||||
/// Calls the hook that has been set by [`set_redirect_hook`] to redirect to `loc`.
|
||||
pub fn call_redirect_hook(loc: &str) {
|
||||
if let Some(hook) = REDIRECT_HOOK.get() {
|
||||
hook(path)
|
||||
hook(loc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ pub fn server_macro_impl(
|
||||
res_ty,
|
||||
client,
|
||||
custom_wrapper,
|
||||
impl_from,
|
||||
} = args;
|
||||
let prefix = prefix.unwrap_or_else(|| Literal::string(default_path));
|
||||
let fn_path = fn_path.unwrap_or_else(|| Literal::string(""));
|
||||
@@ -206,8 +207,11 @@ pub fn server_macro_impl(
|
||||
FnArg::Receiver(_) => None,
|
||||
FnArg::Typed(t) => Some((&t.pat, &t.ty)),
|
||||
});
|
||||
let from_impl =
|
||||
(body.inputs.len() == 1 && first_field.is_some()).then(|| {
|
||||
let impl_from = impl_from.map(|v| v.value).unwrap_or(true);
|
||||
let from_impl = (body.inputs.len() == 1
|
||||
&& first_field.is_some()
|
||||
&& impl_from)
|
||||
.then(|| {
|
||||
let field = first_field.unwrap();
|
||||
let (name, ty) = field;
|
||||
quote! {
|
||||
@@ -434,7 +438,7 @@ pub fn server_macro_impl(
|
||||
quote! {
|
||||
#server_fn_path::request::BrowserMockReq
|
||||
}
|
||||
} else if cfg!(feature = "axum-no-default") {
|
||||
} else if cfg!(feature = "axum") {
|
||||
quote! {
|
||||
#server_fn_path::axum_export::http::Request<#server_fn_path::axum_export::body::Body>
|
||||
}
|
||||
@@ -458,7 +462,7 @@ pub fn server_macro_impl(
|
||||
quote! {
|
||||
#server_fn_path::response::BrowserMockRes
|
||||
}
|
||||
} else if cfg!(feature = "axum-no-default") {
|
||||
} else if cfg!(feature = "axum") {
|
||||
quote! {
|
||||
#server_fn_path::axum_export::http::Response<#server_fn_path::axum_export::body::Body>
|
||||
}
|
||||
@@ -638,7 +642,7 @@ fn err_type(return_ty: &Type) -> Result<Option<&GenericArgument>> {
|
||||
{
|
||||
if let Some(segment) = pat.path.segments.last() {
|
||||
if segment.ident == "ServerFnError" {
|
||||
let args = &pat.path.segments[0].arguments;
|
||||
let args = &segment.arguments;
|
||||
match args {
|
||||
// Result<T, ServerFnError>
|
||||
PathArguments::None => return Ok(None),
|
||||
@@ -676,6 +680,7 @@ struct ServerFnArgs {
|
||||
client: Option<Type>,
|
||||
custom_wrapper: Option<Path>,
|
||||
builtin_encoding: bool,
|
||||
impl_from: Option<LitBool>,
|
||||
}
|
||||
|
||||
impl Parse for ServerFnArgs {
|
||||
@@ -693,6 +698,7 @@ impl Parse for ServerFnArgs {
|
||||
let mut res_ty: Option<Type> = None;
|
||||
let mut client: Option<Type> = None;
|
||||
let mut custom_wrapper: Option<Path> = None;
|
||||
let mut impl_from: Option<LitBool> = None;
|
||||
|
||||
let mut use_key_and_value = false;
|
||||
let mut arg_pos = 0;
|
||||
@@ -800,6 +806,14 @@ impl Parse for ServerFnArgs {
|
||||
));
|
||||
}
|
||||
custom_wrapper = Some(stream.parse()?);
|
||||
} else if key == "impl_from" {
|
||||
if impl_from.is_some() {
|
||||
return Err(syn::Error::new(
|
||||
key.span(),
|
||||
"keyword argument repeated: `impl_from`",
|
||||
));
|
||||
}
|
||||
impl_from = Some(stream.parse()?);
|
||||
} else {
|
||||
return Err(lookahead.error());
|
||||
}
|
||||
@@ -895,6 +909,7 @@ impl Parse for ServerFnArgs {
|
||||
res_ty,
|
||||
client,
|
||||
custom_wrapper,
|
||||
impl_from,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user