Compare commits

..

1 Commits

Author SHA1 Message Date
Greg Johnston
bdb8ff1884 fix: hydration ID mismatch with nested Suspense 2024-02-09 16:30:40 -05:00
49 changed files with 273 additions and 810 deletions

View File

@@ -31,9 +31,10 @@ jobs:
dir_names: true
dir_names_max_depth: "2"
files: |
examples/**
!examples/cargo-make/**
!examples/gtk/**
examples
!examples/cargo-make
!examples/gtk
!examples/hackernews_js_fetch
!examples/Makefile.toml
!examples/*.md
json: true

View File

@@ -25,8 +25,8 @@ jobs:
with:
files: |
examples/**
!examples/cargo-make/**
!examples/gtk/**
!examples/cargo-make
!examples/gtk
!examples/Makefile.toml
!examples/*.md

View File

@@ -55,9 +55,9 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 18
- uses: pnpm/action-setup@v3
- uses: pnpm/action-setup@v2
name: Install pnpm
id: pnpm-install
with:
@@ -107,11 +107,6 @@ 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: |

View File

@@ -25,7 +25,7 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.6.6"
version = "0.6.5"
[workspace.dependencies]
leptos = { path = "./leptos", version = "0.6.5" }

View File

@@ -51,5 +51,103 @@ echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
[tasks.test-report]
workspace = false
description = "show the cargo-make configuration for web examples [web|all|help]"
script = { file = "./cargo-make/scripts/web-report.sh" }
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
'''

View File

@@ -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 you want to run
1. `cd` to the example root directory
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)

View File

@@ -3,36 +3,32 @@
[tasks.stop-client]
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
script = '''
if pidof -q ${CLIENT_PROCESS_NAME}; then
echo " Stopping ${CLIENT_PROCESS_NAME}"
if [ ! -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
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 pidof -q ${CLIENT_PROCESS_NAME}; then
echo " ${CLIENT_PROCESS_NAME} is up"
else
if [ -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
echo " ${CLIENT_PROCESS_NAME} is not running"
else
echo " ${CLIENT_PROCESS_NAME} is up"
fi
'''
[tasks.maybe-start-client]
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
script = '''
if pidof -q ${CLIENT_PROCESS_NAME}; then
echo " ${CLIENT_PROCESS_NAME} is already started"
else
if [ -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
echo " Starting ${CLIENT_PROCESS_NAME}"
if [ -n "${SPAWN_CLIENT_PROCESS}" ];then
echo "Spawning process..."
if [ -z ${SPAWN_CLIENT_PROCESS} ];then
cargo make start-client ${@} &
else
cargo make start-client ${@}
fi
else
echo " ${CLIENT_PROCESS_NAME} is already started"
fi
'''

View File

@@ -1,24 +0,0 @@
[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"

View File

@@ -1,176 +0,0 @@
#!/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

View File

@@ -3,21 +3,18 @@
[tasks.stop-server]
condition = { env_set = ["SERVER_PROCESS_NAME"] }
script = '''
if pidof -q ${SERVER_PROCESS_NAME}; then
echo " Stopping ${SERVER_PROCESS_NAME}"
if [ ! -z $(pidof ${SERVER_PROCESS_NAME}) ]; then
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 pidof -q ${SERVER_PROCESS_NAME}; then
echo " ${SERVER_PROCESS_NAME} is up"
else
if [ -z $(pidof ${SERVER_PROCESS_NAME}) ]; then
echo " ${SERVER_PROCESS_NAME} is not running"
else
echo " ${SERVER_PROCESS_NAME} is up"
fi
'''
@@ -27,11 +24,11 @@ script = '''
YELLOW="\e[0;33m"
RESET="\e[0m"
if pidof -q ${SERVER_PROCESS_NAME}; then
echo " ${SERVER_PROCESS_NAME} is already started"
else
if [ -z $(pidof ${SERVER_PROCESS_NAME}) ]; then
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
'''

View File

@@ -6,33 +6,25 @@ script = '''
RESET="\e[0m"
if command -v chromedriver; then
if pidof -q chromedriver; then
echo " chromedriver is already started"
else
echo "Starting chomedriver"
if [ -z $(pidof chromedriver) ]; then
chromedriver --port=4444 &
fi
else
echo "${RED}${BOLD}ERROR${RESET} - chromedriver not found"
echo "${RED}${BOLD}ERROR${RESET} - chromedriver is required by this task"
exit 1
fi
'''
[tasks.stop-webdriver]
script = '''
if pidof -q chromedriver; then
echo " Stopping chromedriver"
pkill -ef "chromedriver"
else
echo " chromedriver is already stopped"
fi
pkill -f "chromedriver"
'''
[tasks.webdriver-status]
script = '''
if pidof -q chromedriver; then
echo chromedriver is up
else
if [ -z $(pidof chromedriver) ]; then
echo chromedriver is not running
else
echo chromedriver is up
fi
'''

View File

@@ -141,12 +141,9 @@ pub fn Counter() -> impl IntoView {
<div>
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
<button on:click=move |_| dec.dispatch(())>"-1"</button>
<span>
"Value: "
<Suspense>
{move || counter.and_then(|count| *count)} "!"
</Suspense>
</span>
<Suspense fallback=move |_| view!{ <span>"Value: "</span>}>
<span>"Value: " { counter.get().map(|count| count.unwrap_or(0)).unwrap_or(0);} "!"</span>
</Suspense>
<button on:click=move |_| inc.dispatch(())>"+1"</button>
</div>
<Suspense>
@@ -204,7 +201,7 @@ pub fn FormCounter() -> impl IntoView {
<input type="hidden" name="msg" value="form value down"/>
<input type="submit" value="-1"/>
</ActionForm>
<span>"Value: " <Suspense>{move || value().to_string()} "!"</Suspense></span>
<span>"Value: " {move || value().to_string()} "!"</span>
<ActionForm action=adjust>
<input type="hidden" name="delta" value="1"/>
<input type="hidden" name="msg" value="form value up"/>

View File

@@ -0,0 +1,8 @@
[env]
VERIFY_GTK = false
[tasks.verify-flow]
condition = { env_set = ["VERIFY_GTK"] }
[tasks.verify]
condition = { env_set = ["VERIFY_GTK"] }

View File

@@ -1,8 +1 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos.toml" },
]
[env]
CLIENT_PROCESS_NAME = "hackernews_islands"
extend = [{ path = "../cargo-make/main.toml" }]

View File

@@ -1,8 +1 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/deno-build.toml" },
]
[env]
CLIENT_PROCESS_NAME = "deno"
extend = [{ path = "../cargo-make/main.toml" }]

View File

@@ -30,10 +30,10 @@ sqlx = { version = "0.7.2", features = [
], optional = true }
thiserror = "1.0"
wasm-bindgen = "0.2"
axum_session_auth = { version = "0.12.1", features = [
axum_session_auth = { version = "0.10", features = [
"sqlite-rustls",
], optional = true }
axum_session = { version = "0.12.4", features = [
axum_session = { version = "0.10", features = [
"sqlite-rustls",
], optional = true }
bcrypt = { version = "0.15", optional = true }

View File

@@ -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(SessionSqlitePool::from(pool.clone())),
Some(pool.clone().into()),
session_config,
)
.await

View File

@@ -98,6 +98,7 @@ pub fn App() -> impl IntoView {
</Route>
</Routes>
</main>
<footer><p>"Does the footer hydrate correctly?"</p></footer>
</Router>
}
}

View File

@@ -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 = "leptos_tailwind"
output-name = "tailwind_axum"
# 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

View File

@@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"preline": "^1.8.0",
"tailwindcss": "^3.3.2"
}
},
@@ -103,6 +104,15 @@
"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",
@@ -689,6 +699,14 @@
"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",

View File

@@ -7,7 +7,8 @@ pub fn App() -> impl IntoView {
provide_meta_context();
view! {
<Stylesheet id="leptos" href="/pkg/leptos_tailwind.css"/>
<Stylesheet id="leptos" href="/pkg/tailwind_axum.css"/>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Router>
<Routes>

View File

@@ -8,7 +8,7 @@ pub fn App() -> impl IntoView {
view! {
<Stylesheet id="leptos" href="/style/output.css"/>
<Stylesheet id="leptos" href="/pkg/tailwind.css"/>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Router>
<Routes>

View File

@@ -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 [`LeptosRoutes::leptos_routes_with_context`] (or whatever
/// [`handle_server_fns_with_context`] **and** in [`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.

View File

@@ -257,13 +257,7 @@ 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);

View File

@@ -2,7 +2,6 @@ use futures::{Stream, StreamExt};
use leptos::{nonce::use_nonce, use_context, RuntimeId};
use leptos_config::LeptosOptions;
use leptos_meta::MetaContext;
use std::borrow::Cow;
extern crate tracing;
@@ -56,9 +55,7 @@ pub fn html_parts_separated(
options: &LeptosOptions,
meta: Option<&MetaContext>,
) -> (String, &'static str) {
let pkg_path = option_env!("CDN_PKG_PATH")
.map(Cow::from)
.unwrap_or_else(|| format!("/{}", options.site_pkg_dir).into());
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
let nonce = use_nonce();
let nonce = nonce
@@ -110,8 +107,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"{nonce}>
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin=""{nonce}>
<script type="module"{nonce}>
function idle(c) {{
if ("requestIdleCallback" in window) {{
@@ -121,9 +118,9 @@ pub fn html_parts_separated(
}}
}}
idle(() => {{
import('{pkg_path}/{output_name}.js')
import('/{pkg_path}/{output_name}.js')
.then(mod => {{
mod.default('{pkg_path}/{wasm_output_name}.wasm').then({import_callback});
mod.default('/{pkg_path}/{wasm_output_name}.wasm').then({import_callback});
}})
}});
</script>

View File

@@ -15,7 +15,6 @@ 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"
@@ -67,10 +66,7 @@ miniserde = ["leptos_reactive/miniserde"]
rkyv = ["leptos_reactive/rkyv"]
tracing = ["leptos_macro/tracing"]
nonce = ["leptos_dom/nonce"]
spin = [
"leptos_reactive/spin",
"leptos-spin-macro"
]
spin = ["leptos_reactive/spin"]
experimental-islands = [
"leptos_dom/experimental-islands",
"leptos_macro/experimental-islands",
@@ -91,9 +87,7 @@ denylist = [
"rustls",
"default-tls",
"wasm-bindgen",
"trace-component-props",
"spin",
"experimental-islands"
"trace-component-props"
]
skip_feature_sets = [
[

View File

@@ -179,14 +179,7 @@ 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, 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_macro::{component, island, server, slice, slot, view, Params};
pub use leptos_reactive::*;
pub use leptos_server::{
self, create_action, create_multi_action, create_server_action,

View File

@@ -36,7 +36,7 @@ use std::rc::Rc;
/// <div>
/// <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }>
/// {move || {
/// cats.get().map(|data| match data {
/// cats.read().map(|data| match data {
/// None => view! { <pre>"Error"</pre> }.into_view(),
/// Some(cats) => cats
/// .iter()
@@ -245,6 +245,7 @@ where
HydrationCtx::continue_from(current_id);
HydrationCtx::next_component();
HydrationCtx::next_component();
leptos_dom::View::Suspense(current_id, core_component)
}

View File

@@ -155,9 +155,7 @@ fn is_first_run(
first_run: RwSignal<bool>,
suspense_context: &SuspenseContext,
) -> bool {
if cfg!(feature = "csr")
|| (cfg!(feature = "hydrate") && !HydrationCtx::is_hydrating())
{
if cfg!(feature = "csr") {
false
} else {
match (

View File

@@ -9,7 +9,7 @@ description = "Configuration for the Leptos web framework."
readme = "../README.md"
[dependencies]
config = { version = "0.14", default-features = false, features = ["toml"] }
config = { version = "0.13.3", default-features = false, features = ["toml"] }
regex = "1.7.0"
serde = { version = "1.0.151", features = ["derive"] }
thiserror = "1.0.38"

View File

@@ -103,25 +103,6 @@ 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.
@@ -147,7 +128,7 @@ pub fn request_animation_frame_with_handle(
.map(AnimationFrameRequestHandle)
}
raf(closure_once(cb))
raf(Closure::once_into_js(cb))
}
/// Handle that is generated by [request_idle_callback_with_handle] and can be
@@ -256,7 +237,7 @@ pub fn set_timeout_with_handle(
.map(TimeoutHandle)
}
st(closure_once(cb), duration)
st(Closure::once_into_js(cb), duration)
}
/// "Debounce" a callback function. This will cause it to wait for a period of `delay`

View File

@@ -870,7 +870,6 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// - `name`: sets the identifier for the server functions 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`)
@@ -884,11 +883,6 @@ 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(
@@ -897,7 +891,6 @@ 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!()
@@ -907,17 +900,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`](../server_fn/codec/index.html) module.
/// of which can be found in the [`server_fn::codec`] 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`](../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.
/// 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.
///
/// Whatever encoding is provided to `input` should implement `IntoReq` and `FromReq`. Whatever encoding is provided
/// to `output` should implement `IntoRes` and `FromRes`.
@@ -939,8 +932,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 cant fail, the processes of serialization/deserialization and the
/// network call are fallible.
/// - [`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
/// - [`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
/// 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

View File

@@ -86,7 +86,7 @@ use std::any::{Any, TypeId};
///
/// ### Solution
///
/// If you are using the full Leptos framework, you can use the [`Provider`](../leptos/fn.Provider.html)
/// If you are using the full Leptos framework, you can use the [`Provider`](leptos::Provider)
/// component to solve this issue.
///
/// ```rust

View File

@@ -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, try_batch,
spawn_local_with_current_owner, spawn_local_with_owner,
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,8 +143,7 @@ pub use suspense::{GlobalSuspenseContext, SuspenseContext};
pub use trigger::*;
pub use watch::*;
#[doc(hidden)]
pub fn console_warn(s: &str) {
pub(crate) 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));

View File

@@ -86,88 +86,7 @@ pub fn create_memo<T>(f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
where
T: PartialEq + 'static,
{
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: PartialEq + 'static,
{
Runtime::current().create_owning_memo(f)
Runtime::current().create_memo(f)
}
/// An efficient derived reactive value based on other reactive values.
@@ -297,65 +216,6 @@ 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: PartialEq + 'static,
{
create_owning_memo(f)
}
}
impl<T> Clone for Memo<T>
@@ -664,8 +524,8 @@ impl_get_fn_traits![Memo];
pub(crate) struct MemoState<T, F>
where
T: 'static,
F: Fn(Option<T>) -> (T, bool),
T: PartialEq + 'static,
F: Fn(Option<&T>) -> T,
{
pub f: F,
pub t: PhantomData<T>,
@@ -675,8 +535,8 @@ where
impl<T, F> AnyComputation for MemoState<T, F>
where
T: 'static,
F: Fn(Option<T>) -> (T, bool),
T: PartialEq + 'static,
F: Fn(Option<&T>) -> T,
{
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
@@ -691,16 +551,24 @@ where
)
)]
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool {
let mut value = value.borrow_mut();
let curr_value = value
.downcast_mut::<Option<T>>()
.expect("to downcast memo value");
let (new_value, is_different) = {
let value = value.borrow();
let curr_value = value
.downcast_ref::<Option<T>>()
.expect("to downcast memo value");
// run the memo
let (new_value, is_different) = (self.f)(curr_value.take());
// set new value
*curr_value = Some(new_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);
}
is_different
}

View File

@@ -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}")]

View File

@@ -1201,12 +1201,12 @@ impl RuntimeId {
#[track_caller]
#[inline(always)]
pub(crate) fn create_owning_memo<T>(
pub(crate) fn create_memo<T>(
self,
f: impl Fn(Option<T>) -> (T, bool) + 'static,
f: impl Fn(Option<&T>) -> T + 'static,
) -> Memo<T>
where
T: 'static,
T: PartialEq + Any + 'static,
{
Memo {
id: self.create_concrete_memo(
@@ -1397,30 +1397,12 @@ 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);
@@ -1433,6 +1415,7 @@ pub fn try_batch<T>(f: impl FnOnce() -> T) -> Result<T, ReactiveSystemError> {
runtime.run_effects();
val
})
.expect("tried to run a batched update in a runtime that has been disposed")
}
struct SetBatchingOnDrop(bool);

View File

@@ -212,84 +212,3 @@ 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();
}

View File

@@ -1,10 +1,7 @@
//use crate::{ServerFn, ServerFnError};
#[cfg(debug_assertions)]
use leptos_reactive::console_warn;
use leptos_reactive::{
create_rw_signal, is_suppressing_resource_load, signal_prelude::*,
spawn_local, store_value, try_batch, use_context, ReadSignal, RwSignal,
StoredValue,
batch, create_rw_signal, is_suppressing_resource_load, signal_prelude::*,
spawn_local, store_value, use_context, ReadSignal, RwSignal, StoredValue,
};
use server_fn::{error::ServerFnUrlError, ServerFn, ServerFnError};
use std::{cell::Cell, future::Future, pin::Pin, rc::Rc};
@@ -96,24 +93,14 @@ where
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
#[track_caller]
pub fn dispatch(&self, input: I) {
#[cfg(debug_assertions)]
let loc = std::panic::Location::caller();
self.0.with_value(|a| {
a.dispatch(
input,
#[cfg(debug_assertions)]
loc,
)
})
self.0.with_value(|a| a.dispatch(input))
}
/// Create an [Action].
///
/// [Action] is a type of [Signal] which represent imperative calls to
/// an asynchronous function. Where a [Resource](leptos_reactive::Resource) is driven as a function
/// an asynchronous function. Where a [Resource] is driven as a function
/// of a [Signal], [Action]s are [Action::dispatch]ed by events or handlers.
///
/// ```rust
@@ -242,7 +229,7 @@ impl<I> Action<I, Result<I::Output, ServerFnError<I::Error>>>
where
I: ServerFn + 'static,
{
/// Create an [Action] to imperatively call a [server](leptos_macro::server) function.
/// Create an [Action] to imperatively call a [server_fn::server] function.
///
/// The struct representing your server function's arguments should be
/// provided to the [Action]. Unless specified as an argument to the server
@@ -379,11 +366,7 @@ where
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn dispatch(
&self,
input: I,
#[cfg(debug_assertions)] loc: &'static std::panic::Location<'static>,
) {
pub fn dispatch(&self, input: I) {
if !is_suppressing_resource_load() {
let fut = (self.action_fn)(&input);
self.input.set(Some(input));
@@ -396,7 +379,7 @@ where
pending_dispatches.set(pending_dispatches.get().saturating_sub(1));
spawn_local(async move {
let new_value = fut.await;
let res = try_batch(move || {
batch(move || {
value.set(Some(new_value));
input.set(None);
version.update(|n| *n += 1);
@@ -406,18 +389,6 @@ 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`."
));
}
})
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.6.6"
version = "0.6.5"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -16,9 +16,6 @@
//!
//! #[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! {

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.6.6"
version = "0.6.5"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -1,6 +1,6 @@
use crate::{
hooks::has_router, resolve_redirect_url, use_navigate, use_resolved_path,
NavigateOptions, ToHref, Url,
hooks::has_router, use_navigate, use_resolved_path, NavigateOptions,
ToHref, Url,
};
use leptos::{
html::form,
@@ -447,10 +447,8 @@ where
{
let has_router = has_router();
if !has_router {
_ = server_fn::redirect::set_redirect_hook(|loc: &str| {
if let Some(url) = resolve_redirect_url(loc) {
_ = window().location().set_href(&url.href());
}
_ = server_fn::redirect::set_redirect_hook(|path: &str| {
_ = window().location().set_href(path);
});
}
let action_url = if let Some(url) = action.url() {
@@ -480,10 +478,6 @@ 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(),
@@ -547,10 +541,8 @@ where
{
let has_router = has_router();
if !has_router {
_ = server_fn::redirect::set_redirect_hook(|loc: &str| {
if let Some(url) = resolve_redirect_url(loc) {
_ = window().location().set_href(&url.href());
}
_ = server_fn::redirect::set_redirect_hook(|path: &str| {
_ = window().location().set_href(path);
});
}
let action_url = if let Some(url) = action.url() {

View File

@@ -1,7 +1,7 @@
use crate::{
create_location, matching::resolve_path, resolve_redirect_url,
scroll_to_el, use_location, use_navigate, Branch, History, Location,
LocationChange, RouteContext, RouterIntegrationContext, State,
create_location, matching::resolve_path, scroll_to_el, use_location,
use_navigate, Branch, History, Location, LocationChange, RouteContext,
RouterIntegrationContext, State,
};
#[cfg(not(feature = "ssr"))]
use crate::{unescape, Url};
@@ -24,7 +24,6 @@ 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);
@@ -57,24 +56,15 @@ pub fn Router(
// set server function redirect hook
let navigate = use_navigate();
let navigate = SendWrapper::new(navigate);
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 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 navigate = navigate.clone();
// delay by a tick here, so that the Action updates *before* the redirect
request_animation_frame(move || {
navigate(&url.pathname(), 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:#?}");
}
move || {
navigate(&path, Default::default());
}
});
}) as RedirectHook;
_ = server_fn::redirect::set_redirect_hook(router_hook);

View File

@@ -3,8 +3,8 @@ use crate::{
RouterContext,
};
use leptos::{
create_memo, request_animation_frame, signal_prelude::*, use_context,
window, Memo, Oco,
create_memo, request_animation_frame, signal_prelude::*, use_context, Memo,
Oco,
};
use std::{rc::Rc, str::FromStr};
@@ -216,28 +216,3 @@ 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
}
}
}

View File

@@ -27,9 +27,7 @@ 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 }
@@ -75,6 +73,8 @@ url = "2"
[features]
default = ["json", "cbor"]
form-redirects = []
actix = ["ssr", "dep:actix-web", "dep:send_wrapper"]
axum-no-default = [
"ssr",
"dep:axum",
@@ -83,9 +83,10 @@ axum-no-default = [
"dep:tower",
"dep:tower-layer",
]
form-redirects = []
actix = ["ssr", "dep:actix-web", "dep:send_wrapper"]
axum = ["axum/default", "axum-no-default"]
axum = [
"axum/default",
"axum-no-default",
]
browser = [
"dep:gloo-net",
"dep:js-sys",
@@ -111,21 +112,7 @@ all-features = true
# disables some feature combos for testing in CI
[package.metadata.cargo-all-features]
denylist = [
"rustls",
"default-tls",
"form-redirects",
"gloo-net",
"js-sys",
"wasm-bindgen",
"web-sys",
"tower",
"tower-layer",
"send_wrapper",
"ciborium",
"hyper",
"inventory",
]
denylist = ["rustls", "default-tls", "form-redirects"]
skip_feature_sets = [
[
"actix",
@@ -143,48 +130,4 @@ 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",
],
]

View File

@@ -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]: ../leptos/attr.server.html
//! [server]: <https://docs.rs/server_fn/latest/server_fn/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` crate won't work in.).
/// `inventory`).
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` crate won't work in.).
/// `inventory`).
pub fn register_explicit<T>()
where
T: ServerFn<

View File

@@ -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 `loc`.
pub fn call_redirect_hook(loc: &str) {
/// Calls the hook that has been set by [`set_redirect_hook`] to redirect to `path`.
pub fn call_redirect_hook(path: &str) {
if let Some(hook) = REDIRECT_HOOK.get() {
hook(loc)
hook(path)
}
}

View File

@@ -119,7 +119,6 @@ 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(""));
@@ -207,11 +206,8 @@ pub fn server_macro_impl(
FnArg::Receiver(_) => None,
FnArg::Typed(t) => Some((&t.pat, &t.ty)),
});
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 from_impl =
(body.inputs.len() == 1 && first_field.is_some()).then(|| {
let field = first_field.unwrap();
let (name, ty) = field;
quote! {
@@ -438,7 +434,7 @@ pub fn server_macro_impl(
quote! {
#server_fn_path::request::BrowserMockReq
}
} else if cfg!(feature = "axum") {
} else if cfg!(feature = "axum-no-default") {
quote! {
#server_fn_path::axum_export::http::Request<#server_fn_path::axum_export::body::Body>
}
@@ -462,7 +458,7 @@ pub fn server_macro_impl(
quote! {
#server_fn_path::response::BrowserMockRes
}
} else if cfg!(feature = "axum") {
} else if cfg!(feature = "axum-no-default") {
quote! {
#server_fn_path::axum_export::http::Response<#server_fn_path::axum_export::body::Body>
}
@@ -642,7 +638,7 @@ fn err_type(return_ty: &Type) -> Result<Option<&GenericArgument>> {
{
if let Some(segment) = pat.path.segments.last() {
if segment.ident == "ServerFnError" {
let args = &segment.arguments;
let args = &pat.path.segments[0].arguments;
match args {
// Result<T, ServerFnError>
PathArguments::None => return Ok(None),
@@ -680,7 +676,6 @@ struct ServerFnArgs {
client: Option<Type>,
custom_wrapper: Option<Path>,
builtin_encoding: bool,
impl_from: Option<LitBool>,
}
impl Parse for ServerFnArgs {
@@ -698,7 +693,6 @@ 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;
@@ -806,14 +800,6 @@ 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());
}
@@ -909,7 +895,6 @@ impl Parse for ServerFnArgs {
res_ty,
client,
custom_wrapper,
impl_from,
})
}
}