Compare commits

..

1 Commits

Author SHA1 Message Date
Greg Johnston
50a1da08eb change: allow IntoFuture for Suspend::new() (closes #3509) 2025-01-31 12:33:26 -05:00
222 changed files with 5174 additions and 14174 deletions

View File

@@ -28,6 +28,5 @@ jobs:
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
erased_mode: ${{ matrix.erased_mode }}
cargo_make_task: "ci"
toolchain: stable

View File

@@ -25,6 +25,5 @@ jobs:
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
erased_mode: ${{ matrix.erased_mode }}
cargo_make_task: "ci"
toolchain: stable

View File

@@ -18,7 +18,7 @@ jobs:
test:
needs: [get-leptos-changed]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
name: Run semver check (nightly-2025-03-05)
name: Run semver check (nightly-2024-08-01)
runs-on: ubuntu-latest
steps:
- name: Install Glib
@@ -30,4 +30,4 @@ jobs:
- name: Semver Checks
uses: obi1kenobi/cargo-semver-checks-action@v2
with:
rust-toolchain: nightly-2025-03-05
rust-toolchain: nightly-2024-08-01

View File

@@ -25,6 +25,5 @@ jobs:
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
erased_mode: ${{ matrix.erased_mode }}
cargo_make_task: "ci"
toolchain: nightly-2025-03-05
toolchain: nightly-2024-08-01

View File

@@ -50,5 +50,5 @@ jobs:
echo "matrix={\"directory\":${{ steps.changed-dirs.outputs.all_changed_files }}}" >> "$GITHUB_OUTPUT"
else
# Create matrix with one item to prevent an empty vector error
echo "matrix={\"directory\":[\"NO_CHANGE\"], \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
echo "matrix={\"directory\":[\"NO_CHANGE\"]}" >> "$GITHUB_OUTPUT"
fi

View File

@@ -28,7 +28,7 @@ jobs:
sed 's/\/$//' |
jq -R -s -c 'split("\n")[:-1]')
echo "Example Directories: $examples"
echo "matrix={\"directory\":$examples, \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
- name: Print Location Info
run: |
echo "Workspace: ${{ github.workspace }}"

View File

@@ -24,7 +24,7 @@ jobs:
sed "s|$(pwd)/||" |
jq -R -s -c 'split("\n")[:-1]')
echo "Leptos Directories: $crates"
echo "matrix={\"directory\":$crates, \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
echo "matrix={\"directory\":$crates}" >> "$GITHUB_OUTPUT"
- name: Print Location Info
run: |
echo "Workspace: ${{ github.workspace }}"

View File

@@ -5,9 +5,6 @@ on:
directory:
required: true
type: string
erased_mode:
required: true
type: boolean
cargo_make_task:
required: true
type: string
@@ -18,10 +15,9 @@ env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
DEBIAN_FRONTEND: noninteractive
RUSTFLAGS: ${{ inputs.erased_mode && '--cfg erase_components' || '' }}
jobs:
test:
name: "Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }}) (erased_mode: ${{ inputs.erased_mode }})"
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
runs-on: ubuntu-latest
steps:
- name: Free Disk Space
@@ -59,7 +55,7 @@ jobs:
- name: Install wasm-bindgen
run: cargo binstall wasm-bindgen-cli --no-confirm
- name: Install cargo-leptos
run: cargo binstall cargo-leptos --locked --no-confirm
run: cargo binstall cargo-leptos --no-confirm
- name: Install Trunk
uses: jetli/trunk-action@v0.5.0
with:

1103
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,39 +40,36 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.8.0-beta"
version = "0.7.5"
edition = "2021"
rust-version = "1.76"
[workspace.dependencies]
throw_error = { path = "./any_error/", version = "0.3.0" }
throw_error = { path = "./any_error/", version = "0.2.0" }
any_spawner = { path = "./any_spawner/", version = "0.2.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
either_of = { path = "./either_of/", version = "0.1.5" }
hydration_context = { path = "./hydration_context", version = "0.3.0" }
itertools = "0.14.0"
leptos = { path = "./leptos", version = "0.8.0-beta" }
leptos_config = { path = "./leptos_config", version = "0.8.0-beta" }
leptos_dom = { path = "./leptos_dom", version = "0.8.0-beta" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-beta" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-beta" }
leptos_macro = { path = "./leptos_macro", version = "0.8.0-beta" }
leptos_router = { path = "./router", version = "0.8.0-beta" }
leptos_router_macro = { path = "./router_macro", version = "0.8.0-beta" }
leptos_server = { path = "./leptos_server", version = "0.8.0-beta" }
leptos_meta = { path = "./meta", version = "0.8.0-beta" }
either_of = { path = "./either_of/", version = "0.1.0" }
hydration_context = { path = "./hydration_context", version = "0.2.0" }
leptos = { path = "./leptos", version = "0.7.5" }
leptos_config = { path = "./leptos_config", version = "0.7.5" }
leptos_dom = { path = "./leptos_dom", version = "0.7.5" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.5" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.5" }
leptos_macro = { path = "./leptos_macro", version = "0.7.5" }
leptos_router = { path = "./router", version = "0.7.5" }
leptos_router_macro = { path = "./router_macro", version = "0.7.5" }
leptos_server = { path = "./leptos_server", version = "0.7.5" }
leptos_meta = { path = "./meta", version = "0.7.5" }
next_tuple = { path = "./next_tuple", version = "0.1.0" }
oco_ref = { path = "./oco", version = "0.2.0" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.2.0-beta" }
reactive_stores = { path = "./reactive_stores", version = "0.2.0-beta" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0-beta" }
serde_json = "1.0.0"
server_fn = { path = "./server_fn", version = "0.8.0-beta" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-beta" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-beta" }
tachys = { path = "./tachys", version = "0.2.0-beta" }
wasm-bindgen = { version = "0.2.100" }
reactive_graph = { path = "./reactive_graph", version = "0.1.5" }
reactive_stores = { path = "./reactive_stores", version = "0.1.3" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0" }
server_fn = { path = "./server_fn", version = "0.7.5" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.5" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.5" }
tachys = { path = "./tachys", version = "0.1.5" }
[profile.release]
codegen-units = 1

View File

@@ -21,7 +21,7 @@ use leptos::*;
#[component]
pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
// create a reactive signal with the initial value
let (value, set_value) = signal(initial_value);
let (value, set_value) = create_signal(initial_value);
// create event handlers for our buttons
// note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
@@ -46,7 +46,7 @@ pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
pub fn SimpleCounterWithBuilder(initial_value: i32) -> impl IntoView {
use leptos::html::*;
let (value, set_value) = signal(initial_value);
let (value, set_value) = create_signal(initial_value);
let clear = move |_| set_value(0);
let decrement = move |_| set_value.update(|value| *value -= 1);
let increment = move |_| set_value.update(|value| *value += 1);

View File

@@ -1,6 +1,6 @@
[package]
name = "throw_error"
version = "0.3.0"
version = "0.2.0"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -9,7 +9,7 @@ use std::{
error,
fmt::{self, Display},
future::Future,
ops,
mem, ops,
pin::Pin,
sync::Arc,
task::{Context, Poll},
@@ -17,6 +17,11 @@ use std::{
/* Wrapper Types */
/// This is a result type into which any error can be converted.
///
/// Results are stored as [`Error`].
pub type Result<T, E = Error> = core::result::Result<T, E>;
/// A generic wrapper for any error.
#[derive(Debug, Clone)]
#[repr(transparent)]
@@ -104,7 +109,7 @@ pub fn get_error_hook() -> Option<Arc<dyn ErrorHook>> {
/// Sets the current thread-local error hook, which will be invoked when [`throw`] is called.
pub fn set_error_hook(hook: Arc<dyn ErrorHook>) -> ResetErrorHookOnDrop {
ResetErrorHookOnDrop(
ERROR_HOOK.with_borrow_mut(|this| Option::replace(this, hook)),
ERROR_HOOK.with_borrow_mut(|this| mem::replace(this, Some(hook))),
)
}

View File

@@ -17,7 +17,7 @@ tokio = { version = "1.41", optional = true, default-features = false, features
"rt",
] }
tracing = { version = "0.1.41", optional = true }
wasm-bindgen-futures = { version = "0.4.50", optional = true }
wasm-bindgen-futures = { version = "0.4.47", optional = true }
[features]
async-executor = ["dep:async-executor"]

View File

@@ -23,7 +23,7 @@ tokio-test = "0.4.0"
miniserde = "0.1.0"
gloo = "0.8.0"
uuid = { version = "1.0", features = ["serde", "v4", "wasm-bindgen"] }
wasm-bindgen = "0.2.100"
wasm-bindgen = "0.2.0"
lazy_static = "1.0"
log = "0.4.0"
strum = "0.24.0"

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.8.1", optional = true }
axum = { version = "0.7.5", optional = true }
console_error_panic_hook = "0.1.7"
console_log = "1.0"
gloo-utils = "0.2.0"
@@ -20,27 +20,18 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
tokio = { version = "1.39", features = [
"rt-multi-thread",
"macros",
"time",
], optional = true }
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
wasm-bindgen = "0.2.92"
web-sys = { version = "0.3.69", features = [
"AddEventListenerOptions",
"Document",
"Element",
"Event",
"EventListener",
"EventTarget",
"Performance",
"Window",
], optional = true }
web-sys = { version = "0.3.69", features = [ "AddEventListenerOptions", "Document", "Element", "Event", "EventListener", "EventTarget", "Performance", "Window" ], optional = true }
[features]
hydrate = ["leptos/hydrate", "dep:js-sys", "dep:web-sys"]
hydrate = [
"leptos/hydrate",
"dep:js-sys",
"dep:web-sys",
]
ssr = [
"dep:axum",
"dep:http-body-util",

View File

@@ -1,6 +1,5 @@
[tasks.install-cargo-leptos]
install_crate = { crate_name = "cargo-leptos", binary = "cargo-leptos", test_arg = "--help" }
args = ["--locked"]
[tasks.cargo-leptos-e2e]
command = "cargo"

View File

@@ -13,7 +13,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.8.1", optional = true }
axum = { version = "0.7.5", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }

View File

@@ -45,7 +45,7 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route("/special/{id}", get(custom_handler))
.route("/special/:id", get(custom_handler))
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())

View File

@@ -1,4 +1,5 @@
use leptos::prelude::*;
use leptos::tachys::html::style::style;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -15,7 +16,7 @@ pub enum CatError {
type CatCount = usize;
async fn fetch_cats(count: CatCount) -> Result<Vec<String>, Error> {
async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
if count > 0 {
gloo_timers::future::TimeoutFuture::new(1000).await;
// make the request
@@ -41,7 +42,11 @@ async fn fetch_cats(count: CatCount) -> Result<Vec<String>, Error> {
pub fn fetch_example() -> impl IntoView {
let (cat_count, set_cat_count) = signal::<CatCount>(1);
let cats = LocalResource::new(move || fetch_cats(cat_count.get()));
// we use new_unsync here because the reqwasm request type isn't Send
// if we were doing SSR, then
// 1) we'd want to use a Resource, so the data would be serialized to the client
// 2) we'd need to make sure there was a thread-local spawner set up
let cats = AsyncDerived::new_unsync(move || fetch_cats(cat_count.get()));
let fallback = move |errors: ArcRwSignal<Errors>| {
let error_list = move || {
@@ -61,6 +66,8 @@ pub fn fetch_example() -> impl IntoView {
}
};
let spreadable = style(("background-color", "AliceBlue"));
view! {
<div>
<label>
@@ -75,7 +82,7 @@ pub fn fetch_example() -> impl IntoView {
/>
</label>
<Transition fallback=|| view! { <div>"Loading..."</div> }>
<Transition fallback=|| view! { <div>"Loading..."</div> } {..spreadable}>
<ErrorBoundary fallback>
<ul>
{move || Suspend::new(async move {
@@ -85,7 +92,7 @@ pub fn fetch_example() -> impl IntoView {
.map(|s| {
view! {
<li>
<img src=s.clone() />
<img src=s.clone()/>
</li>
}
})

View File

@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
tracing = "0.1.40"
gloo-net = { version = "0.6.0", features = ["http"] }
reqwest = { version = "0.12.5", features = ["json"] }
axum = { version = "0.8.1", optional = true }
axum = { version = "0.7.5", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }

View File

@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
tracing = "0.1.40"
gloo-net = { version = "0.6.0", features = ["http"] }
reqwest = { version = "0.12.5", features = ["json"] }
axum = { version = "0.8.1", optional = true, features = ["http2"] }
axum = { version = "0.7.5", optional = true, features = ["http2"] }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = [
"fs",

View File

@@ -23,7 +23,7 @@ serde = { version = "1.0", features = ["derive"] }
tracing = "0.1.40"
gloo-net = { version = "0.6.0", features = ["http"] }
reqwest = { version = "0.12.5", features = ["json"] }
axum = { version = "0.8.1", default-features = false, optional = true }
axum = { version = "0.7.5", default-features = false, optional = true }
tower = { version = "0.4.13", optional = true }
http = { version = "1.1", optional = true }
web-sys = { version = "0.3.70", features = [

View File

@@ -10,12 +10,15 @@ crate-type = ["cdylib", "rlib"]
console_error_panic_hook = "0.1.7"
futures = "0.3.30"
http = "1.1"
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
leptos = { path = "../../leptos", features = [
"tracing",
"islands",
] }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.8.1", optional = true }
axum = { version = "0.7.5", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }

View File

@@ -10,20 +10,22 @@ crate-type = ["cdylib", "rlib"]
console_error_panic_hook = "0.1.7"
futures = "0.3.30"
http = "1.1"
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
leptos = { path = "../../leptos", features = [
"tracing",
"islands",
] }
leptos_router = { path = "../../router" }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", features = [
"islands-router",
"dont-use-islands-router",
], optional = true }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.8.1", optional = true }
axum = { version = "0.7.5", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }
wasm-bindgen = "0.2.100"
serde_json = "1.0.133"
wasm-bindgen = "0.2.93"
[features]
hydrate = ["leptos/hydrate"]
@@ -56,11 +58,11 @@ site-root = "target/site"
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "style.css"
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3009"
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# The browserlist query used for optimizing the CSS.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,140 @@
window.addEventListener("click", async (ev) => {
// confirm that this is an <a> that meets our requirements
if (
ev.defaultPrevented ||
ev.button !== 0 ||
ev.metaKey ||
ev.altKey ||
ev.ctrlKey ||
ev.shiftKey
)
return;
/** @type HTMLAnchorElement | undefined;*/
const a = ev
.composedPath()
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
if (!a) return;
const svg = a.namespaceURI === "http://www.w3.org/2000/svg";
const href = svg ? a.href.baseVal : a.href;
const target = svg ? a.target.baseVal : a.target;
if (target || (!href && !a.hasAttribute("state"))) return;
const rel = (a.getAttribute("rel") || "").split(/\s+/);
if (a.hasAttribute("download") || (rel && rel.includes("external"))) return;
const url = svg ? new URL(href, document.baseURI) : new URL(href);
if (
url.origin !== window.location.origin // ||
// TODO base
//(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase()))
)
return;
ev.preventDefault();
// fetch the new page
const resp = await fetch(url);
const htmlString = await resp.text();
// Use DOMParser to parse the HTML string
const parser = new DOMParser();
// TODO parse from the request stream instead?
const doc = parser.parseFromString(htmlString, 'text/html');
// The 'doc' variable now contains the parsed DOM
const transition = async () => {
const oldDocWalker = document.createTreeWalker(document);
const newDocWalker = doc.createTreeWalker(doc);
let oldNode = oldDocWalker.currentNode;
let newNode = newDocWalker.currentNode;
while(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
oldNode = oldDocWalker.currentNode;
newNode = newDocWalker.currentNode;
// if the nodes are different, we need to replace the old with the new
// because of the typed view tree, this should never actually happen
if (oldNode.nodeType !== newNode.nodeType) {
oldNode.replaceWith(newNode);
}
// if it's a text node, just update the text with the new text
else if (oldNode.nodeType === Node.TEXT_NODE) {
oldNode.textContent = newNode.textContent;
}
// if it's an element, replace if it's a different tag, or update attributes
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
/** @type Element */
const oldEl = oldNode;
/** @type Element */
const newEl = newNode;
if (oldEl.tagName !== newEl.tagName) {
oldEl.replaceWith(newEl);
}
else {
for(const attr of newEl.attributes) {
oldEl.setAttribute(attr.name, attr.value);
}
}
}
// we use comment "branch marker" nodes to distinguish between different branches in the statically-typed view tree
// if one of these marker is hit, then there are two options
// 1) it's the same branch, and we just keep walking until the end
// 2) it's a different branch, in which case the old can be replaced with the new wholesale
else if (oldNode.nodeType === Node.COMMENT_NODE) {
const oldText = oldNode.textContent;
const newText = newNode.textContent;
if(oldText.startsWith("bo") && newText !== oldText) {
oldDocWalker.nextNode();
newDocWalker.nextNode();
const oldRange = new Range();
const newRange = new Range();
let oldBranches = 1;
let newBranches = 1;
while(oldBranches > 0 && newBranches > 0) {
if(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
console.log(oldDocWalker.currentNode, newDocWalker.currentNode);
if(oldDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
if(oldDocWalker.currentNode.textContent.startsWith("bo")) {
oldBranches += 1;
} else if(oldDocWalker.currentNode.textContent.startsWith("bc")) {
oldBranches -= 1;
}
}
if(newDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
if(newDocWalker.currentNode.textContent.startsWith("bo")) {
newBranches += 1;
} else if(newDocWalker.currentNode.textContent.startsWith("bc")) {
newBranches -= 1;
}
}
}
}
try {
oldRange.setStartAfter(oldNode);
oldRange.setEndBefore(oldDocWalker.currentNode);
newRange.setStartAfter(newNode);
newRange.setEndBefore(newDocWalker.currentNode);
const newContents = newRange.extractContents();
oldRange.deleteContents();
oldRange.insertNode(newContents);
oldNode.replaceWith(newNode);
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
} catch (e) {
console.error(e);
}
} }
}
};
// Not all browsers support startViewTransition; see https://caniuse.com/?search=startViewTransition
if (document.startViewTransition) {
await document.startViewTransition(transition);
} else {
await transition()
}
window.history.pushState(undefined, null, url);
});

View File

@@ -1,13 +1,8 @@
use leptos::{
either::{Either, EitherOf3},
prelude::*,
};
use leptos::prelude::*;
use leptos_router::{
components::{Route, Router, Routes},
hooks::{use_params_map, use_query_map},
path,
components::{FlatRoutes, Route, Router},
StaticSegment,
};
use serde::{Deserialize, Serialize};
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
@@ -17,7 +12,7 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone()/>
<HydrationScripts options=options islands=true islands_router=true/>
<HydrationScripts options=options islands=true/>
<link rel="stylesheet" id="leptos" href="/pkg/islands.css"/>
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
</head>
@@ -31,180 +26,34 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
#[component]
pub fn App() -> impl IntoView {
view! {
<script src="/routing.js"></script>
<Router>
<header>
<h1>"My Contacts"</h1>
<h1>"My Application"</h1>
</header>
<nav>
<a href="/">"Home"</a>
<a href="/about">"About"</a>
<a href="/">"Page A"</a>
<a href="/b">"Page B"</a>
</nav>
<main>
<Routes fallback=|| "Not found.">
<Route path=path!("") view=Home/>
<Route path=path!("user/:id") view=Details/>
<Route path=path!("about") view=About/>
</Routes>
<p>
<label>"Home Checkbox" <input type="checkbox"/></label>
</p>
<FlatRoutes fallback=|| "Not found.">
<Route path=StaticSegment("") view=PageA/>
<Route path=StaticSegment("b") view=PageB/>
</FlatRoutes>
</main>
</Router>
}
}
#[server]
pub async fn search(query: String) -> Result<Vec<User>, ServerFnError> {
let users = tokio::fs::read_to_string("./mock_data.json").await?;
let data: Vec<User> = serde_json::from_str(&users)?;
let query = query.to_ascii_lowercase();
Ok(data
.into_iter()
.filter(|user| {
user.first_name.to_ascii_lowercase().contains(&query)
|| user.last_name.to_ascii_lowercase().contains(&query)
|| user.email.to_ascii_lowercase().contains(&query)
})
.collect())
}
#[server]
pub async fn delete_user(id: u32) -> Result<(), ServerFnError> {
let users = tokio::fs::read_to_string("./mock_data.json").await?;
let mut data: Vec<User> = serde_json::from_str(&users)?;
data.retain(|user| user.id != id);
let new_json = serde_json::to_string(&data)?;
tokio::fs::write("./mock_data.json", &new_json).await?;
Ok(())
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct User {
id: u32,
first_name: String,
last_name: String,
email: String,
#[component]
pub fn PageA() -> impl IntoView {
view! { <label>"Page A" <input type="checkbox"/></label> }
}
#[component]
pub fn Home() -> impl IntoView {
let q = use_query_map();
let q = move || q.read().get("q");
let data = Resource::new(q, |q| async move {
if let Some(q) = q {
search(q).await
} else {
Ok(vec![])
}
});
let delete_user_action = ServerAction::<DeleteUser>::new();
let view = move || {
Suspend::new(async move {
let users = data.await.unwrap();
if q().is_none() {
EitherOf3::A(view! {
<p class="note">"Enter a search to begin viewing contacts."</p>
})
} else if users.is_empty() {
EitherOf3::B(view! {
<p class="note">"No users found matching that search."</p>
})
} else {
EitherOf3::C(view! {
<table>
<tbody>
<For
each=move || users.clone()
key=|user| user.id
let:user
>
<tr>
<td>{user.first_name}</td>
<td>{user.last_name}</td>
<td>{user.email}</td>
<td>
<a href=format!("/user/{}", user.id)>"Details"</a>
<input type="checkbox"/>
<ActionForm action=delete_user_action>
<input type="hidden" name="id" value=user.id/>
<input type="submit" value="Delete"/>
</ActionForm>
</td>
</tr>
</For>
</tbody>
</table>
})
}
})
};
view! {
<section class="page">
<form method="GET" class="search">
<input type="search" name="q" value=q autofocus oninput="this.form.requestSubmit()"/>
<input type="submit"/>
</form>
<Suspense fallback=|| view! { <p>"Loading..."</p> }>{view}</Suspense>
</section>
}
}
#[component]
pub fn Details() -> impl IntoView {
#[server]
pub async fn get_user(id: u32) -> Result<Option<User>, ServerFnError> {
let users = tokio::fs::read_to_string("./mock_data.json").await?;
let data: Vec<User> = serde_json::from_str(&users)?;
Ok(data.iter().find(|user| user.id == id).cloned())
}
let params = use_params_map();
let id = move || {
params
.read()
.get("id")
.and_then(|id| id.parse::<u32>().ok())
};
let user = Resource::new(id, |id| async move {
match id {
None => Ok(None),
Some(id) => get_user(id).await,
}
});
move || {
Suspend::new(async move {
user.await.map(|user| match user {
None => Either::Left(view! {
<section class="page">
<h2>"Not found."</h2>
<p>"Sorry — we couldnt find that user."</p>
</section>
}),
Some(user) => Either::Right(view! {
<section class="page">
<h2>{user.first_name} " " { user.last_name}</h2>
<p class="email">{user.email}</p>
</section>
}),
})
})
}
}
#[component]
pub fn About() -> impl IntoView {
view! {
<section class="page">
<h2>"About"</h2>
<p>"This demo is intended to show off an experimental “islands router” feature, which mimics the smooth transitions and user experience of client-side routing while minimizing the amount of code that actually runs in the browser."</p>
<p>"By default, all the content in this application is only rendered on the server. But you can add client-side interactivity via islands like this one:"</p>
<Counter/>
</section>
}
}
#[island]
pub fn Counter() -> impl IntoView {
let count = RwSignal::new(0);
view! {
<button class="counter" on:click=move |_| *count.write() += 1>{count}</button>
}
pub fn PageB() -> impl IntoView {
view! { <label>"Page B" <input type="checkbox"/></label> }
}

View File

@@ -1,52 +1,3 @@
body {
font-family: system-ui, sans-serif;
background-color: #f6f6fa;
}
h1, h2, h3, h4, h5, h6 {
font-family: ui-rounded, 'Hiragino Maru Gothic ProN', Quicksand, Comfortaa, Manjari, 'Arial Rounded MT', 'Arial Rounded MT Bold', Calibri, source-sans-pro, sans-serif;
text-align: center;
}
nav {
padding: 1rem;
text-align: center;
}
nav a {
margin: 1rem;
}
form.search {
display: flex;
margin: 2rem auto;
justify-content: center;
}
td {
min-width: 10rem;
width: 10rem;
}
table {
min-width: 100%;
}
.page {
width: 80%;
margin: auto;
}
td:last-child > * {
display: inline-block;
}
.note, .note {
text-align: center;
}
button.counter {
display: block;
font-size: 2rem;
margin: auto;
.pending {
color: purple;
}

View File

@@ -149,12 +149,12 @@ pub fn App() -> impl IntoView {
</div>
<div class="col-md-6">
<div class="row">
<Button id="run" text="Create 1,000 rows" on:click=run />
<Button id="runlots" text="Create 10,000 rows" on:click=run_lots />
<Button id="add" text="Append 1,000 rows" on:click=add />
<Button id="update" text="Update every 10th row" on:click=update />
<Button id="clear" text="Clear" on:click=clear />
<Button id="swaprows" text="Swap Rows" on:click=swap_rows />
<Button id="run" text="Create 1,000 rows" on:click=run/>
<Button id="runlots" text="Create 10,000 rows" on:click=run_lots/>
<Button id="add" text="Append 1,000 rows" on:click=add/>
<Button id="update" text="Update every 10th row" on:click=update/>
<Button id="clear" text="Clear" on:click=clear/>
<Button id="swaprows" text="Swap Rows" on:click=swap_rows/>
</div>
</div>
</div>

View File

@@ -9,6 +9,7 @@ use leptos_router::{
},
hooks::{use_navigate, use_params, use_query_map},
params::Params,
MatchNestedRoutes,
};
use leptos_router_macro::path;
use std::time::Duration;
@@ -24,7 +25,7 @@ pub fn RouterExample() -> impl IntoView {
// contexts are passed down through the route tree
provide_context(ExampleContext(0));
// this signal will be used to set whether we are allowed to access a protected route
// this signal will be ued to set whether we are allowed to access a protected route
let (logged_in, set_logged_in) = signal(true);
let (is_routing, set_is_routing) = signal(false);
@@ -32,7 +33,7 @@ pub fn RouterExample() -> impl IntoView {
<Router set_is_routing>
// shows a progress bar while async data are loading
<div class="routing-progress">
<RoutingProgress is_routing max_time=Duration::from_millis(250) />
<RoutingProgress is_routing max_time=Duration::from_millis(250)/>
</div>
<nav>
// ordinary <a> elements can be used for client-side navigation
@@ -52,15 +53,15 @@ pub fn RouterExample() -> impl IntoView {
<Routes transition=true fallback=|| "This page could not be found.">
// paths can be created using the path!() macro, or provided as types like
// StaticSegment("about")
<Route path=path!("about") view=About />
<Route path=path!("about") view=About/>
<ProtectedRoute
path=path!("settings")
condition=move || Some(logged_in.get())
redirect_path=|| "/"
view=Settings
/>
<Route path=path!("redirect-home") view=|| view! { <Redirect path="/" /> } />
<ContactRoutes />
<Route path=path!("redirect-home") view=|| view! { <Redirect path="/"/> }/>
<ContactRoutes/>
</Routes>
</main>
</Router>
@@ -70,11 +71,11 @@ pub fn RouterExample() -> impl IntoView {
// You can define other routes in their own component.
// Routes implement the MatchNestedRoutes
#[component(transparent)]
pub fn ContactRoutes() -> impl leptos_router::MatchNestedRoutes + Clone {
pub fn ContactRoutes() -> impl MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("") view=ContactList>
<Route path=path!("/") view=|| "Select a contact." />
<Route path=path!("/:id") view=Contact />
<Route path=path!("/") view=|| "Select a contact."/>
<Route path=path!("/:id") view=Contact/>
</ParentRoute>
}
.into_inner()
@@ -121,7 +122,7 @@ pub fn ContactList() -> impl IntoView {
<Suspense fallback=move || view! { <p>"Loading contacts..."</p> }>
<ul>{contacts}</ul>
</Suspense>
<Outlet />
<Outlet/>
</div>
}
}
@@ -165,7 +166,7 @@ pub fn Contact() -> impl IntoView {
Some(contact) => Either::Right(view! {
<section class="card">
<h1>{contact.first_name} " " {contact.last_name}</h1>
<p>{contact.address_1} <br /> {contact.address_2}</p>
<p>{contact.address_1} <br/> {contact.address_2}</p>
</section>
}),
}
@@ -223,10 +224,10 @@ pub fn Settings() -> impl IntoView {
<Form action="">
<fieldset>
<legend>"Name"</legend>
<input type="text" name="first_name" placeholder="First" />
<input type="text" name="last_name" placeholder="Last" />
<input type="text" name="first_name" placeholder="First"/>
<input type="text" name="last_name" placeholder="Last"/>
</fieldset>
<input type="submit" />
<input type="submit"/>
<p>
"This uses the " <code>"<Form/>"</code>
" component, which enhances forms by using client-side navigation for "

View File

@@ -21,21 +21,21 @@ server_fn = { path = "../../server_fn", features = [
log = "0.4.22"
simple_logger = "5.0"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.5.2", optional = true }
tower-http = { version = "0.6.2", features = [
axum = { version = "0.7.5", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = [
"fs",
"tracing",
"trace",
], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }
thiserror = "2.0.11"
thiserror = "1.0"
wasm-bindgen = "0.2.93"
serde_toml = "0.0.1"
toml = "0.8.19"
web-sys = { version = "0.3.70", features = ["FileList", "File"] }
strum = { version = "0.27.1", features = ["strum_macros", "derive"] }
notify = { version = "8.0", optional = true }
strum = { version = "0.26.3", features = ["strum_macros", "derive"] }
notify = { version = "6.1", optional = true }
pin-project-lite = "0.2.14"
dashmap = { version = "6.0", optional = true }
once_cell = { version = "1.19", optional = true }

View File

@@ -1,4 +1,4 @@
use futures::{Sink, Stream, StreamExt};
use futures::StreamExt;
use http::Method;
use leptos::{html::Input, prelude::*, task::spawn_local};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
@@ -9,10 +9,8 @@ use server_fn::{
MultipartFormData, Postcard, Rkyv, SerdeLite, StreamingText,
TextStream,
},
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::{browser::BrowserRequest, ClientReq, Req},
response::{browser::BrowserResponse, ClientRes, TryRes},
ContentType,
response::{browser::BrowserResponse, ClientRes, Res},
};
use std::future::Future;
#[cfg(feature = "ssr")]
@@ -654,72 +652,32 @@ pub fn FileWatcher() -> impl IntoView {
/// implementations if you'd like. However, it's much lighter weight to use something like `strum`
/// simply to generate those trait implementations.
#[server]
pub async fn ascii_uppercase(text: String) -> Result<String, MyErrors> {
other_error()?;
Ok(ascii_uppercase_inner(text)?)
}
pub fn other_error() -> Result<(), String> {
Ok(())
}
pub fn ascii_uppercase_inner(text: String) -> Result<String, InvalidArgument> {
pub async fn ascii_uppercase(
text: String,
) -> Result<String, ServerFnError<InvalidArgument>> {
if text.len() < 5 {
Err(InvalidArgument::TooShort)
Err(InvalidArgument::TooShort.into())
} else if text.len() > 15 {
Err(InvalidArgument::TooLong)
Err(InvalidArgument::TooLong.into())
} else if text.is_ascii() {
Ok(text.to_ascii_uppercase())
} else {
Err(InvalidArgument::NotAscii)
Err(InvalidArgument::NotAscii.into())
}
}
#[server]
pub async fn ascii_uppercase_classic(
text: String,
) -> Result<String, ServerFnError<InvalidArgument>> {
Ok(ascii_uppercase_inner(text)?)
}
// The EnumString and Display derive macros are provided by strum
#[derive(Debug, Clone, Display, EnumString, Serialize, Deserialize)]
#[derive(Debug, Clone, EnumString, Display)]
pub enum InvalidArgument {
TooShort,
TooLong,
NotAscii,
}
#[derive(Debug, Clone, Display, Serialize, Deserialize)]
pub enum MyErrors {
InvalidArgument(InvalidArgument),
ServerFnError(ServerFnErrorErr),
Other(String),
}
impl From<InvalidArgument> for MyErrors {
fn from(value: InvalidArgument) -> Self {
MyErrors::InvalidArgument(value)
}
}
impl From<String> for MyErrors {
fn from(value: String) -> Self {
MyErrors::Other(value)
}
}
impl FromServerFnError for MyErrors {
fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
MyErrors::ServerFnError(value)
}
}
#[component]
pub fn CustomErrorTypes() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
let (result, set_result) = signal(None);
let (result_classic, set_result_classic) = signal(None);
view! {
<h3>Using custom error types</h3>
@@ -734,17 +692,14 @@ pub fn CustomErrorTypes() -> impl IntoView {
<button on:click=move |_| {
let value = input_ref.get().unwrap().value();
spawn_local(async move {
let data = ascii_uppercase(value.clone()).await;
let data_classic = ascii_uppercase_classic(value).await;
let data = ascii_uppercase(value).await;
set_result.set(Some(data));
set_result_classic.set(Some(data_classic));
});
}>
"Submit"
</button>
<p>{move || format!("{:?}", result.get())}</p>
<p>{move || format!("{:?}", result_classic.get())}</p>
}
}
@@ -762,11 +717,8 @@ pub struct Toml;
#[derive(Serialize, Deserialize)]
pub struct TomlEncoded<T>(T);
impl ContentType for Toml {
const CONTENT_TYPE: &'static str = "application/toml";
}
impl Encoding for Toml {
const CONTENT_TYPE: &'static str = "application/toml";
const METHOD: Method = Method::POST;
}
@@ -774,12 +726,14 @@ impl<T, Request, Err> IntoReq<Toml, Request, Err> for TomlEncoded<T>
where
Request: ClientReq<Err>,
T: Serialize,
Err: FromServerFnError,
{
fn into_req(self, path: &str, accepts: &str) -> Result<Request, Err> {
let data = toml::to_string(&self.0).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<Err>> {
let data = toml::to_string(&self.0)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Request::try_new_post(path, Toml::CONTENT_TYPE, accepts, data)
}
}
@@ -788,26 +742,23 @@ impl<T, Request, Err> FromReq<Toml, Request, Err> for TomlEncoded<T>
where
Request: Req<Err> + Send,
T: DeserializeOwned,
Err: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, Err> {
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
let string_data = req.try_into_string().await?;
toml::from_str::<T>(&string_data)
.map(TomlEncoded)
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
.map_err(|e| ServerFnError::Args(e.to_string()))
}
}
impl<T, Response, Err> IntoRes<Toml, Response, Err> for TomlEncoded<T>
where
Response: TryRes<Err>,
Response: Res<Err>,
T: Serialize + Send,
Err: FromServerFnError,
{
async fn into_res(self) -> Result<Response, Err> {
let data = toml::to_string(&self.0).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
async fn into_res(self) -> Result<Response, ServerFnError<Err>> {
let data = toml::to_string(&self.0)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Response::try_from_string(Toml::CONTENT_TYPE, data)
}
}
@@ -816,13 +767,12 @@ impl<T, Response, Err> FromRes<Toml, Response, Err> for TomlEncoded<T>
where
Response: ClientRes<Err> + Send,
T: DeserializeOwned,
Err: FromServerFnError,
{
async fn from_res(res: Response) -> Result<Self, Err> {
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
let data = res.try_into_string().await?;
toml::from_str(&data).map(TomlEncoded).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
})
toml::from_str(&data)
.map(TomlEncoded)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
}
}
@@ -885,10 +835,7 @@ pub fn CustomClientExample() -> impl IntoView {
pub struct CustomClient;
// Implement the `Client` trait for it.
impl<E> Client<E> for CustomClient
where
E: FromServerFnError,
{
impl<CustErr> Client<CustErr> for CustomClient {
// BrowserRequest and BrowserResponse are the defaults used by other server functions.
// They are wrappers for the underlying Web Fetch API types.
type Request = BrowserRequest;
@@ -897,7 +844,8 @@ pub fn CustomClientExample() -> impl IntoView {
// Our custom `send()` implementation does all the work.
fn send(
req: Self::Request,
) -> impl Future<Output = Result<Self::Response, E>> + Send {
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
+ Send {
// BrowserRequest derefs to the underlying Request type from gloo-net,
// so we can get access to the headers here
let headers = req.headers();
@@ -906,24 +854,6 @@ pub fn CustomClientExample() -> impl IntoView {
// delegate back out to BrowserClient to send the modified request
BrowserClient::send(req)
}
fn open_websocket(
path: &str,
) -> impl Future<
Output = Result<
(
impl Stream<Item = Result<server_fn::Bytes, E>> + Send + 'static,
impl Sink<Result<server_fn::Bytes, E>> + Send + 'static,
),
E,
>,
> + Send {
BrowserClient::open_websocket(path)
}
fn spawn(future: impl Future<Output = ()> + Send + 'static) {
<BrowserClient as Client<E>>::spawn(future)
}
}
// Specify our custom client with `client = `

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly-2025-03-05"
channel = "nightly-2024-01-29"

View File

@@ -20,7 +20,7 @@ leptos_router = { path = "../../router" }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
axum = { version = "0.8.1", optional = true }
axum = { version = "0.7.5", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = [

View File

@@ -18,7 +18,7 @@ leptos_router = { path = "../../router" }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
axum = { version = "0.8.1", optional = true }
axum = { version = "0.7.5", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = [
@@ -45,7 +45,7 @@ ssr = [
"dep:leptos_axum",
"leptos_router/ssr",
"dep:notify",
"dep:http",
"dep:http"
]
[profile.release]

View File

@@ -1,99 +0,0 @@
@check_instrumented_issue_3719
Feature: Using instrumented counters to test regression from #3502.
Check that the suspend/suspense and the underlying resources are
called with the expected number of times. If this was already in
place by #3502 (5c43c18) it should have caught this regression.
For a better minimum demonstration see #3719.
Background:
Given I see the app
And I select the mode Instrumented
Scenario: follow all paths via CSR avoids #3502
Given I select the following links
| Item Listing |
| Item 1 |
| Inspect path2 |
| Inspect path2/field3 |
And I click on Reset CSR Counters
When I select the following links
| Inspect path2/field1 |
| Inspect path2/field2 |
And I go check the Counters
Then I see the following counters under section
| Suspend Calls | |
| item_listing | 0 |
| item_overview | 0 |
| item_inspect | 2 |
And the following counters under section
| Server Calls (CSR) | |
| list_items | 0 |
| get_item | 0 |
| inspect_item_root | 0 |
| inspect_item_field | 2 |
# To show that starting directly from within a param will simply
# cause the problem.
Scenario: Quicker way to demonstrate regression caused by #3502
Given I select the link Target 123
# And I click on Reset CSR Counters
When I select the following links
| Inspect path2/field1 |
| Inspect path2/field2 |
And I go check the Counters
Then I see the following counters under section
| Suspend Calls | |
| item_listing | 0 |
| item_overview | 0 |
| item_inspect | 3 |
And the following counters under section
| Server Calls (CSR) | |
| list_items | 1 |
| get_item | 1 |
| inspect_item_root | 0 |
| inspect_item_field | 4 |
Scenario: Follow paths ordinarily down to a target
Given I select the following links
| Item Listing |
| Item 1 |
And I click on Reset CSR Counters
When I select the following links
| Target 4## |
| Target 3## |
And I go check the Counters
Then I see the following counters under section
| Suspend Calls | |
| item_listing | 0 |
| item_overview | 2 |
| item_inspect | 0 |
And the following counters under section
| Server Calls (CSR) | |
| list_items | 0 |
| get_item | 2 |
| inspect_item_root | 0 |
| inspect_item_field | 0 |
Scenario: Same as above, but add a refresh to test hydration
Given I select the following links
| Item Listing |
| Item 1 |
And I refresh the page
And I click on Reset CSR Counters
When I select the following links
| Target 4## |
| Target 3## |
And I go check the Counters
Then I see the following counters under section
| Suspend Calls | |
| item_listing | 0 |
| item_overview | 2 |
| item_inspect | 0 |
And the following counters under section
| Server Calls (CSR) | |
| list_items | 0 |
| get_item | 2 |
| inspect_item_root | 0 |
| inspect_item_field | 0 |

View File

@@ -3,28 +3,12 @@ mod fixtures;
use anyhow::Result;
use cucumber::World;
use fixtures::world::AppWorld;
use std::{ffi::OsStr, fs::read_dir};
#[tokio::main]
async fn main() -> Result<()> {
// Normally the below is done, but it's now gotten to the point of
// having a sufficient number of tests where the resource contention
// of the concurrently running browsers will cause failures on CI.
// AppWorld::cucumber()
// .fail_on_skipped()
// .run_and_exit("./features")
// .await;
// Mitigate the issue by manually stepping through each feature,
// rather than letting cucumber glob them and dispatch all at once.
for entry in read_dir("./features")? {
let path = entry?.path();
if path.extension() == Some(OsStr::new("feature")) {
AppWorld::cucumber()
.fail_on_skipped()
.run_and_exit(path)
.await;
}
}
AppWorld::cucumber()
.fail_on_skipped()
.run_and_exit("./features")
.await;
Ok(())
}

View File

@@ -4,7 +4,7 @@ use leptos_router::{
hooks::use_params,
nested_router::Outlet,
params::Params,
ParamSegment, SsrMode, StaticSegment, WildcardSegment,
MatchNestedRoutes, ParamSegment, SsrMode, StaticSegment, WildcardSegment,
};
#[cfg(feature = "ssr")]
@@ -21,7 +21,6 @@ pub(super) mod counter {
pub struct Counter(AtomicU32);
impl Counter {
#[allow(dead_code)]
pub const fn new() -> Self {
Self(AtomicU32::new(0))
}
@@ -204,20 +203,20 @@ pub struct SuspenseCounters {
}
#[component]
pub fn InstrumentedRoutes() -> impl leptos_router::MatchNestedRoutes + Clone {
pub fn InstrumentedRoutes() -> impl MatchNestedRoutes + Clone {
// TODO should make this mode configurable via feature flag?
let ssr = SsrMode::Async;
view! {
<ParentRoute path=StaticSegment("instrumented") view=InstrumentedRoot ssr>
<Route path=StaticSegment("/") view=InstrumentedTop />
<Route path=StaticSegment("/") view=InstrumentedTop/>
<ParentRoute path=StaticSegment("item") view=ItemRoot>
<Route path=StaticSegment("/") view=ItemListing />
<Route path=StaticSegment("/") view=ItemListing/>
<ParentRoute path=ParamSegment("id") view=ItemTop>
<Route path=StaticSegment("/") view=ItemOverview />
<Route path=WildcardSegment("path") view=ItemInspect />
<Route path=StaticSegment("/") view=ItemOverview/>
<Route path=WildcardSegment("path") view=ItemInspect/>
</ParentRoute>
</ParentRoute>
<Route path=StaticSegment("counters") view=ShowCounters />
<Route path=StaticSegment("counters") view=ShowCounters/>
</ParentRoute>
}
.into_inner()
@@ -280,41 +279,32 @@ fn InstrumentedRoot() -> impl IntoView {
<section id="instrumented">
<nav>
<a href="/">"Site Root"</a>
<A href="./" exact=true>
"Instrumented Root"
</A>
<A href="item/" strict_trailing_slash=true>
"Item Listing"
</A>
<A href="counters" strict_trailing_slash=true>
"Counters"
</A>
<A href="./" exact=true>"Instrumented Root"</A>
<A href="item/" strict_trailing_slash=true>"Item Listing"</A>
<A href="counters" strict_trailing_slash=true>"Counters"</A>
</nav>
<FieldNavPortlet />
<Outlet />
<Suspense>
{move || Suspend::new(async move {
<FieldNavPortlet/>
<Outlet/>
<Suspense>{
move || Suspend::new(async move {
let clear_suspense_counters = move |_| {
counters.update(|c| *c = SuspenseCounters::default());
};
csr_ticket
.get()
.map(|ticket| {
let ticket = ticket.0;
view! {
<ActionForm action=reset_counters>
<input type="hidden" name="ticket" value=format!("{ticket}") />
<input
id="reset-csr-counters"
type="submit"
value="Reset CSR Counters"
on:click=clear_suspense_counters
/>
</ActionForm>
}
})
})}
</Suspense>
csr_ticket.get().map(|ticket| {
let ticket = ticket.0;
view! {
<ActionForm action=reset_counters>
<input type="hidden" name="ticket" value=format!("{ticket}") />
<input
id="reset-csr-counters"
type="submit"
value="Reset CSR Counters"
on:click=clear_suspense_counters/>
</ActionForm>
}
})
})
}</Suspense>
<footer>
<nav>
<A href="item/3/">"Target 3##"</A>
@@ -333,17 +323,11 @@ fn InstrumentedRoot() -> impl IntoView {
fn InstrumentedTop() -> impl IntoView {
view! {
<h1>"Instrumented Tests"</h1>
<p>
"These tests validates the number of invocations of server functions and suspenses per access."
</p>
<p>"These tests validates the number of invocations of server functions and suspenses per access."</p>
<ul>
// not using `A` because currently some bugs with artix
<li>
<a href="item/">"Item Listing"</a>
</li>
<li>
<a href="item/4/path1/">"Target 41#"</a>
</li>
<li><a href="item/">"Item Listing"</a></li>
<li><a href="item/4/path1/">"Target 41#"</a></li>
</ul>
}
}
@@ -358,7 +342,7 @@ fn ItemRoot() -> impl IntoView {
view! {
<h2>"<ItemRoot/>"</h2>
<Outlet />
<Outlet/>
}
}
@@ -376,9 +360,7 @@ fn ItemListing() -> impl IntoView {
// adding an extra `/` in artix; manually construct `a` instead.
// <li><A href=format!("./{item}/")>"Item "{item}</A></li>
view! {
<li>
<a href=format!("/instrumented/item/{item}/")>"Item "{item}</a>
</li>
<li><a href=format!("/instrumented/item/{item}/")>"Item "{item}</a></li>
}
)
.collect_view()
@@ -391,7 +373,9 @@ fn ItemListing() -> impl IntoView {
view! {
<h3>"<ItemListing/>"</h3>
<ul>
<Suspense>{item_listing}</Suspense>
<Suspense>
{item_listing}
</Suspense>
</ul>
}
}
@@ -418,7 +402,7 @@ fn ItemTop() -> impl IntoView {
));
view! {
<h4>"<ItemTop/>"</h4>
<Outlet />
<Outlet/>
}
}
@@ -428,29 +412,24 @@ fn ItemOverview() -> impl IntoView {
let resource = expect_context::<Resource<Option<GetItemResult>>>();
let item_view = move || {
Suspend::new(async move {
let result = resource.await.map(|GetItemResult(item, names)| {
view! {
<p>{format!("Viewing {item:?}")}</p>
<ul>
{names
.into_iter()
.map(|name| {
let id = item.id;
// FIXME seems like relative link isn't working, it is currently
// adding an extra `/` in artix; manually construct `a` instead.
// <li><A href=format!("./{name}/")>{format!("Inspect {name}")}</A></li>
view! {
<li>
<a href=format!(
"/instrumented/item/{id}/{name}/",
)>"Inspect "{name.clone()}</a>
</li>
}
})
.collect_view()}
</ul>
}
});
let result = resource.await.map(|GetItemResult(item, names)| view! {
<p>{format!("Viewing {item:?}")}</p>
<ul>{
names.into_iter()
.map(|name| {
// FIXME seems like relative link isn't working, it is currently
// adding an extra `/` in artix; manually construct `a` instead.
// <li><A href=format!("./{name}/")>{format!("Inspect {name}")}</A></li>
let id = item.id;
view! {
<li><a href=format!("/instrumented/item/{id}/{name}/")>
"Inspect "{name.clone()}
</a></li>
}
})
.collect_view()
}</ul>
});
suspense_counters.update_untracked(|c| c.item_overview += 1);
result
})
@@ -458,7 +437,9 @@ fn ItemOverview() -> impl IntoView {
view! {
<h5>"<ItemOverview/>"</h5>
<Suspense>{item_view}</Suspense>
<Suspense>
{item_view}
</Suspense>
}
}
@@ -492,9 +473,8 @@ fn ItemInspect() -> impl IntoView {
// result
},
);
let ws = use_context::<WriteSignal<Option<FieldNavCtx>>>();
on_cleanup(move || {
if let Some(c) = ws {
on_cleanup(|| {
if let Some(c) = use_context::<WriteSignal<Option<FieldNavCtx>>>() {
c.set(None);
}
});
@@ -516,26 +496,23 @@ fn ItemInspect() -> impl IntoView {
));
view! {
<p>{format!("Inspecting {item:?}")}</p>
<ul>
{fields
.iter()
<ul>{
fields.iter()
.map(|field| {
// FIXME seems like relative link to root for a wildcard isn't
// working as expected, so manually construct `a` instead.
// let text = format!("Inspect {name}/{field}");
// view! {
// <li><A href=format!("{field}")>{text}</A></li>
// <li><A href=format!("{field}")>{text}</A></li>
// }
view! {
<li>
<a href=format!(
"/instrumented/item/{id}/{name}/{field}",
)>{format!("Inspect {name}/{field}")}</a>
</li>
<li><a href=format!("/instrumented/item/{id}/{name}/{field}")>{
format!("Inspect {name}/{field}")
}</a></li>
}
})
.collect_view()}
</ul>
.collect_view()
}</ul>
}
});
suspense_counters.update_untracked(|c| c.item_inspect += 1);
@@ -550,7 +527,9 @@ fn ItemInspect() -> impl IntoView {
view! {
<h5>"<ItemInspect/>"</h5>
<Suspense>{inspect_view}</Suspense>
<Suspense>
{inspect_view}
</Suspense>
}
}
@@ -611,8 +590,7 @@ fn ShowCounters() -> impl IntoView {
id="reset-counters"
type="submit"
value="Reset Counters"
on:click=clear_suspense_counters
/>
on:click=clear_suspense_counters/>
</ActionForm>
}
})
@@ -623,23 +601,20 @@ fn ShowCounters() -> impl IntoView {
<h2>"Counters"</h2>
<h3 id="suspend-calls">"Suspend Calls"</h3>
{move || {
suspense_counters
.with(|c| {
view! {
<dl>
<dt>"item_listing"</dt>
<dd id="item_listing">{c.item_listing}</dd>
<dt>"item_overview"</dt>
<dd id="item_overview">{c.item_overview}</dd>
<dt>"item_inspect"</dt>
<dd id="item_inspect">{c.item_inspect}</dd>
</dl>
}
})
}}
{move || suspense_counters.with(|c| view! {
<dl>
<dt>"item_listing"</dt>
<dd id="item_listing">{c.item_listing}</dd>
<dt>"item_overview"</dt>
<dd id="item_overview">{c.item_overview}</dd>
<dt>"item_inspect"</dt>
<dd id="item_inspect">{c.item_inspect}</dd>
</dl>
})}
<Suspense>{counter_view}</Suspense>
<Suspense>
{counter_view}
</Suspense>
}
}
@@ -667,17 +642,17 @@ pub fn FieldNavPortlet() -> impl IntoView {
view! {
<div id="FieldNavPortlet">
<span>"FieldNavPortlet:"</span>
<nav>
{ctx
.0
.map(|ctx| {
ctx.into_iter()
.map(|FieldNavItem { href, text }| {
view! { <A href=href>{text}</A> }
})
.collect_view()
})}
</nav>
<nav>{
ctx.0.map(|ctx| {
ctx.into_iter()
.map(|FieldNavItem { href, text }| {
view! {
<A href=href>{text}</A>
}
})
.collect_view()
})
}</nav>
</div>
}
})

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.8.1", optional = true }
axum = { version = "0.7.5", optional = true }
console_error_panic_hook = "0.1.7"
leptos = { path = "../../leptos" }
leptos_meta = { path = "../../meta" }

View File

@@ -1,22 +0,0 @@
{
"name": "leptos-tailwind",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "leptos-tailwind",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"tailwindcss": "^4.0.0"
}
},
"node_modules/tailwindcss": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.0.tgz",
"integrity": "sha512-ULRPI3A+e39T7pSaf1xoi58AqqJxVCLg8F/uM5A3FadUbnyDTgltVnXJvdkTjwCOGA6NazqHVcwPJC5h2vRYVQ==",
"dev": true
}
}
}

View File

@@ -1,69 +0,0 @@
/*! tailwindcss v4.0.0 | MIT License | https://tailwindcss.com */
@tailwind base;
@tailwind components;
.relative {
position: relative;
}
.m-auto {
margin: auto;
}
.flex {
display: flex;
}
.min-h-screen {
min-height: 100vh;
}
.transform {
transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y);
}
.flex-col {
flex-direction: column;
}
.flex-row-reverse {
flex-direction: row-reverse;
}
.flex-wrap {
flex-wrap: wrap;
}
.border-b-4 {
border-bottom-style: var(--tw-border-style);
border-bottom-width: 4px;
}
.border-l-2 {
border-left-style: var(--tw-border-style);
border-left-width: 2px;
}
.bg-gradient-to-tl {
--tw-gradient-position: to top left in oklab,;
background-image: linear-gradient(var(--tw-gradient-stops));
}
@property --tw-rotate-x {
syntax: "*";
inherits: false;
initial-value: rotateX(0);
}
@property --tw-rotate-y {
syntax: "*";
inherits: false;
initial-value: rotateY(0);
}
@property --tw-rotate-z {
syntax: "*";
inherits: false;
initial-value: rotateZ(0);
}
@property --tw-skew-x {
syntax: "*";
inherits: false;
initial-value: skewX(0);
}
@property --tw-skew-y {
syntax: "*";
inherits: false;
initial-value: skewY(0);
}
@property --tw-border-style {
syntax: "*";
inherits: false;
initial-value: solid;
}

View File

@@ -16,7 +16,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
log = "0.4.22"
simple_logger = "5.0"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.8.1", optional = true }
axum = { version = "0.7.5", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }

View File

@@ -1,4 +1,4 @@
#[cfg(feature = "ssr")]
use crate::todo::*;
use axum::{
body::Body,
extract::Path,
@@ -8,9 +8,10 @@ use axum::{
Router,
};
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use todo_app_sqlite_axum::*;
//Define a handler to test extractor with state
#[cfg(feature = "ssr")]
async fn custom_handler(
Path(id): Path<String>,
req: Request<Body>,
@@ -19,16 +20,14 @@ async fn custom_handler(
move || {
provide_context(id.clone());
},
todo::TodoApp,
TodoApp,
);
handler(req).await.into_response()
}
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use crate::todo::{ssr::db, *};
use leptos_axum::{generate_route_list, LeptosRoutes};
use crate::todo::ssr::db;
simple_logger::init_with_level(log::Level::Error)
.expect("couldn't initialize logging");
@@ -46,7 +45,7 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route("/special/{id}", get(custom_handler))
.route("/special/:id", get(custom_handler))
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
@@ -62,12 +61,3 @@ async fn main() {
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
use leptos::mount::mount_to_body;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(todo::TodoApp);
}

View File

@@ -15,7 +15,7 @@ leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
leptos_integration_utils = { path = "../../integrations/utils", optional = true }
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.8.1", optional = true }
axum = { version = "0.7.5", optional = true }
tower = { version = "0.5.1", features = ["util"], optional = true }
tower-http = { version = "0.6.1", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }

View File

@@ -34,7 +34,7 @@ async fn main() {
// here, we're not actually doing server side rendering, so we set up a manual
// handler for the server fns
// this should include a get() handler if you have any GetUrl-based server fns
.route("/api/{*fn_name}", post(leptos_axum::handle_server_fns))
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.fallback(file_or_index_handler)
.with_state(leptos_options);

View File

@@ -1,6 +1,6 @@
[package]
name = "hydration_context"
version = "0.3.0"
version = "0.2.1"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -14,7 +14,7 @@ throw_error = { workspace = true }
or_poisoned = { workspace = true }
futures = "0.3.31"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = { version = "0.2.100", optional = true }
wasm-bindgen = { version = "0.2.97", optional = true }
js-sys = { version = "0.3.74", optional = true }
once_cell = "1.20"
pin-project-lite = "0.2.15"

View File

@@ -21,11 +21,10 @@ leptos_macro = { workspace = true, features = ["actix"] }
leptos_meta = { workspace = true, features = ["nonce"] }
leptos_router = { workspace = true, features = ["ssr"] }
server_fn = { workspace = true, features = ["actix"] }
tachys = { workspace = true }
serde_json = { workspace = true }
serde_json = "1.0"
parking_lot = "0.12.3"
tracing = { version = "0.1", optional = true }
tokio = { version = "1.43", features = ["rt", "fs"] }
tokio = { version = "1.41", features = ["rt", "fs"] }
send_wrapper = "0.6.0"
dashmap = "6"
once_cell = "1"
@@ -34,7 +33,7 @@ once_cell = "1"
rustdoc-args = ["--generate-link-to-definition"]
[features]
islands-router = ["tachys/islands"]
dont-use-islands-router = []
tracing = ["dep:tracing"]
[package.metadata.cargo-all-features]

View File

@@ -23,7 +23,6 @@ use hydration_context::SsrSharedContext;
use leptos::{
config::LeptosOptions,
context::{provide_context, use_context},
hydration::IslandsRouterNavigation,
prelude::expect_context,
reactive::{computed::ScopedFuture, owner::Owner},
IntoView,
@@ -275,13 +274,14 @@ pub fn redirect(path: &str) {
///
/// This can then be set up at an appropriate route in your application:
///
/// ```no_run
/// ```
/// use actix_web::*;
///
/// fn register_server_functions() {
/// // call ServerFn::register() for each of the server functions you've defined
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// // make sure you actually register your server functions
@@ -297,6 +297,7 @@ pub fn redirect(path: &str) {
/// .run()
/// .await
/// }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -368,6 +369,7 @@ pub fn handle_server_fns_with_context(
// actually run the server fn
let mut res = ActixResponse(
service
.0
.run(ActixRequest::from((req, payload)))
.await
.take(),
@@ -431,7 +433,7 @@ pub fn handle_server_fns_with_context(
/// but requires some client-side JavaScript.
///
/// This can then be set up at an appropriate route in your application:
/// ```no_run
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::prelude::*;
/// use leptos_router::Method;
@@ -442,6 +444,7 @@ pub fn handle_server_fns_with_context(
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
@@ -461,6 +464,7 @@ pub fn handle_server_fns_with_context(
/// .run()
/// .await
/// }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -488,7 +492,7 @@ where
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// This can then be set up at an appropriate route in your application:
/// ```no_run
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::prelude::*;
/// use leptos_router::Method;
@@ -499,6 +503,7 @@ where
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
@@ -521,6 +526,7 @@ where
/// .run()
/// .await
/// }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -546,7 +552,7 @@ where
/// `async` resources have loaded.
///
/// This can then be set up at an appropriate route in your application:
/// ```no_run
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::prelude::*;
/// use leptos_router::Method;
@@ -557,6 +563,7 @@ where
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
@@ -576,6 +583,7 @@ where
/// .run()
/// .await
/// }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -655,27 +663,12 @@ where
IV: IntoView + 'static,
{
_ = replace_blocks; // TODO
handle_response(
method,
additional_context,
app_fn,
|app, chunks, supports_ooo| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
if supports_ooo {
app.to_html_stream_out_of_order_branching()
} else {
app.to_html_stream_in_order_branching()
}
} else if supports_ooo {
app.to_html_stream_out_of_order()
} else {
app.to_html_stream_in_order()
};
Box::pin(app.chain(chunks())) as PinnedStream<String>
})
},
)
handle_response(method, additional_context, app_fn, |app, chunks| {
Box::pin(async move {
Box::pin(app.to_html_stream_out_of_order().chain(chunks()))
as PinnedStream<String>
})
})
}
/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
@@ -701,21 +694,12 @@ pub fn render_app_to_stream_in_order_with_context<IV>(
where
IV: IntoView + 'static,
{
handle_response(
method,
additional_context,
app_fn,
|app, chunks, _supports_ooo| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
Box::pin(app.chain(chunks())) as PinnedStream<String>
})
},
)
handle_response(method, additional_context, app_fn, |app, chunks| {
Box::pin(async move {
Box::pin(app.to_html_stream_in_order().chain(chunks()))
as PinnedStream<String>
})
})
}
/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
@@ -747,13 +731,12 @@ where
fn async_stream_builder<IV>(
app: IV,
chunks: BoxedFnOnce<PinnedStream<String>>,
_supports_ooo: bool,
) -> PinnedFuture<PinnedStream<String>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -793,7 +776,6 @@ fn leptos_corrected_path(req: &HttpRequest) -> String {
}
}
#[allow(clippy::type_complexity)]
fn handle_response<IV>(
method: Method,
additional_context: impl Fn() + 'static + Clone + Send,
@@ -801,7 +783,6 @@ fn handle_response<IV>(
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
bool,
) -> PinnedFuture<PinnedStream<String>>,
) -> Route
where
@@ -812,9 +793,6 @@ where
let add_context = additional_context.clone();
async move {
let is_island_router_navigation = cfg!(feature = "islands-router")
&& req.headers().get("Islands-Router").is_some();
let res_options = ResponseOptions::default();
let (meta_context, meta_output) = ServerMetaContext::new();
@@ -825,10 +803,6 @@ where
move || {
provide_contexts(req, &meta_context, &res_options);
add_context();
if is_island_router_navigation {
provide_context(IslandsRouterNavigation);
}
}
};
@@ -838,7 +812,6 @@ where
additional_context,
res_options,
stream_builder,
!is_island_router_navigation,
)
.await;
@@ -1129,7 +1102,6 @@ impl StaticRouteGenerator {
app_fn.clone(),
additional_context,
async_stream_builder,
false,
);
let sc = owner.shared_context().unwrap();

View File

@@ -4,14 +4,14 @@ authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Axum integrations for the Leptos web framework."
version = "0.8.0-beta"
version = { workspace = true }
rust-version.workspace = true
edition.workspace = true
[dependencies]
any_spawner = { workspace = true, features = ["tokio"] }
hydration_context = { workspace = true }
axum = { version = "0.8.1", default-features = false, features = [
axum = { version = "0.7.9", default-features = false, features = [
"matched-path",
] }
dashmap = "6"
@@ -22,28 +22,21 @@ leptos_macro = { workspace = true, features = ["axum"] }
leptos_meta = { workspace = true, features = ["ssr", "nonce"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
tachys = { workspace = true }
once_cell = "1"
parking_lot = "0.12.3"
tokio = { version = "1.43", default-features = false }
tokio = { version = "1.41", default-features = false }
tower = { version = "0.5.1", features = ["util"] }
tower-http = "0.6.2"
tracing = { version = "0.1.41", optional = true }
[dev-dependencies]
axum = "0.8.1"
tokio = { version = "1.43", features = ["net", "rt-multi-thread"] }
axum = "0.7.9"
tokio = { version = "1.41", features = ["net", "rt-multi-thread"] }
[features]
wasm = []
default = [
"tokio/fs",
"tokio/sync",
"tower-http/fs",
"tower/util",
"server_fn/axum",
]
islands-router = ["tachys/islands"]
default = ["tokio/fs", "tokio/sync", "tower-http/fs", "tower/util"]
dont-use-islands-router = []
tracing = ["dep:tracing"]
[package.metadata.docs.rs]

View File

@@ -1,6 +1,5 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![allow(clippy::type_complexity)]
//! Provides functions to easily integrate Leptos with Axum.
//!
@@ -279,11 +278,12 @@ pub fn generate_request_and_parts(
///
/// This can then be set up at an appropriate route in your application:
///
/// ```no_run
/// ```
/// use axum::{handler::Handler, routing::post, Router};
/// use leptos::prelude::*;
/// use std::net::SocketAddr;
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[cfg(feature = "default")]
/// #[tokio::main]
/// async fn main() {
@@ -299,9 +299,7 @@ pub fn generate_request_and_parts(
/// .await
/// .unwrap();
/// }
///
/// # #[cfg(not(feature = "default"))]
/// # fn main() { }
/// # }
/// ```
/// Leptos provides a generic implementation of `handle_server_fns`. If access to more specific parts of the Request is desired,
/// you can specify your own server fn handler based on this one and give it it's own route in the server macro.
@@ -370,6 +368,8 @@ async fn handle_server_fns_inner(
additional_context: impl Fn() + 'static + Clone + Send,
req: Request<Body>,
) -> impl IntoResponse {
use server_fn::middleware::Service;
let method = req.method().clone();
let path = req.uri().path().to_string();
let (req, parts) = generate_request_and_parts(req);
@@ -442,7 +442,7 @@ pub type PinnedHtmlStream =
/// to route it using [leptos_router], serving an HTML stream of your application.
///
/// This can then be set up at an appropriate route in your application:
/// ```no_run
/// ```
/// use axum::{handler::Handler, Router};
/// use leptos::{config::get_configuration, prelude::*};
/// use std::{env, net::SocketAddr};
@@ -452,6 +452,7 @@ pub type PinnedHtmlStream =
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[cfg(feature = "default")]
/// #[tokio::main]
/// async fn main() {
@@ -470,9 +471,7 @@ pub type PinnedHtmlStream =
/// .await
/// .unwrap();
/// }
///
/// # #[cfg(not(feature = "default"))]
/// # fn main() { }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -485,7 +484,7 @@ pub type PinnedHtmlStream =
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream<IV>(
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -509,7 +508,7 @@ where
)]
pub fn render_route<S, IV>(
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
State<S>,
Request<Body>,
@@ -531,7 +530,7 @@ where
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// This can then be set up at an appropriate route in your application:
/// ```no_run
/// ```
/// use axum::{handler::Handler, Router};
/// use leptos::{config::get_configuration, prelude::*};
/// use std::{env, net::SocketAddr};
@@ -541,6 +540,7 @@ where
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[cfg(feature = "default")]
/// #[tokio::main]
/// async fn main() {
@@ -559,9 +559,7 @@ where
/// .await
/// .unwrap();
/// }
///
/// # #[cfg(not(feature = "default"))]
/// # fn main() { }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -574,7 +572,7 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order<IV>(
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -627,14 +625,13 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ Sync
+ 'static
where
IV: IntoView + 'static,
@@ -657,8 +654,8 @@ where
)]
pub fn render_route_with_context<S, IV>(
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
State<S>,
Request<Body>,
@@ -759,32 +756,25 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
replace_blocks: bool,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ Sync
+ 'static
where
IV: IntoView + 'static,
{
_ = replace_blocks; // TODO
handle_response(additional_context, app_fn, |app, chunks, supports_ooo| {
handle_response(additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
if supports_ooo {
app.to_html_stream_out_of_order_branching()
} else {
app.to_html_stream_in_order_branching()
}
} else if supports_ooo {
app.to_html_stream_out_of_order()
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_out_of_order_branching()
} else {
app.to_html_stream_in_order()
app.to_html_stream_out_of_order()
};
Box::pin(app.chain(chunks())) as PinnedStream<String>
})
@@ -833,8 +823,8 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -844,8 +834,8 @@ pub fn render_app_to_stream_in_order_with_context<IV>(
where
IV: IntoView + 'static,
{
handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| {
let app = if cfg!(feature = "islands-router") {
handle_response(additional_context, app_fn, |app, chunks| {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -857,18 +847,13 @@ where
}
fn handle_response<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
bool,
) -> PinnedFuture<PinnedStream<String>>,
) -> impl Fn(Request<Body>) -> PinnedFuture<Response<Body>>
+ Clone
+ Send
+ Sync
+ 'static
) -> impl Fn(Request<Body>) -> PinnedFuture<Response<Body>> + Clone + Send + 'static
where
IV: IntoView + 'static,
{
@@ -886,16 +871,12 @@ fn handle_response_inner<IV>(
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
bool,
) -> PinnedFuture<PinnedStream<String>>,
) -> PinnedFuture<Response<Body>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
let is_island_router_navigation = cfg!(feature = "islands-router")
&& req.headers().get("Islands-Router").is_some();
let add_context = additional_context.clone();
let res_options = ResponseOptions::default();
let (meta_context, meta_output) = ServerMetaContext::new();
@@ -917,10 +898,6 @@ where
res_options.clone(),
);
add_context();
if is_island_router_navigation {
provide_context(IslandsRouterNavigation);
}
}
};
@@ -930,7 +907,6 @@ where
additional_context,
res_options,
stream_builder,
!is_island_router_navigation,
)
.await;
@@ -961,7 +937,7 @@ fn provide_contexts(
/// `async` resources have loaded.
///
/// This can then be set up at an appropriate route in your application:
/// ```no_run
/// ```
/// use axum::{handler::Handler, Router};
/// use leptos::{config::get_configuration, prelude::*};
/// use std::{env, net::SocketAddr};
@@ -971,6 +947,7 @@ fn provide_contexts(
/// view! { <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[cfg(feature = "default")]
/// #[tokio::main]
/// async fn main() {
@@ -990,9 +967,7 @@ fn provide_contexts(
/// .await
/// .unwrap();
/// }
///
/// # #[cfg(not(feature = "default"))]
/// # fn main() { }
/// # }
/// ```
///
/// ## Provided Context Types
@@ -1005,7 +980,7 @@ fn provide_contexts(
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async<IV>(
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -1059,8 +1034,8 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async_stream_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -1070,9 +1045,9 @@ pub fn render_app_async_stream_with_context<IV>(
where
IV: IntoView + 'static,
{
handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| {
handle_response(additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -1126,8 +1101,8 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -1143,13 +1118,12 @@ where
fn async_stream_builder<IV>(
app: IV,
chunks: BoxedFnOnce<PinnedStream<String>>,
_supports_ooo: bool,
) -> PinnedFuture<PinnedStream<String>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -1422,7 +1396,6 @@ impl StaticRouteGenerator {
app_fn.clone(),
additional_context,
async_stream_builder,
false,
);
let sc = owner.shared_context().unwrap();
@@ -1668,7 +1641,7 @@ where
self,
options: &S,
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
@@ -1683,8 +1656,8 @@ where
self,
options: &S,
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
@@ -1717,15 +1690,12 @@ impl AxumPath for Vec<PathSegment> {
match segment {
PathSegment::Static(s) => path.push_str(s),
PathSegment::Param(s) => {
path.push('{');
path.push(':');
path.push_str(s);
path.push('}');
}
PathSegment::Splat(s) => {
path.push('{');
path.push('*');
path.push_str(s);
path.push('}');
}
PathSegment::Unit => {}
PathSegment::OptionalParam(_) => {
@@ -1757,7 +1727,7 @@ where
self,
state: &S,
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
@@ -1773,8 +1743,8 @@ where
self,
state: &S,
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
@@ -1855,64 +1825,64 @@ where
}
} else {
router.route(
path,
match listing.mode() {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
path,
match listing.mode() {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
SsrMode::PartiallyBlocked => {
let s = render_app_to_stream_with_context_and_replace_blocks(
cx_with_state_and_method.clone(),
app_fn.clone(),
true
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
}
SsrMode::PartiallyBlocked => {
let s = render_app_to_stream_with_context_and_replace_blocks(
cx_with_state_and_method.clone(),
app_fn.clone(),
true
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
}
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
SsrMode::Async => {
let s = render_app_async_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
}
SsrMode::Async => {
let s = render_app_async_with_context(
cx_with_state_and_method.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
_ => unreachable!()
},
)
}
_ => unreachable!()
},
)
};
}
}
@@ -2011,9 +1981,8 @@ where
/// This is provided as a convenience, but is a fairly simple function. If you need to adapt it,
/// simply reuse the source code of this function in your own application.
#[cfg(feature = "default")]
pub fn file_and_error_handler_with_context<S, IV>(
additional_context: impl Fn() + 'static + Clone + Send,
shell: impl Fn(LeptosOptions) -> IV + 'static + Clone + Send,
pub fn file_and_error_handler<S, IV>(
shell: fn(LeptosOptions) -> IV,
) -> impl Fn(
Uri,
State<S>,
@@ -2028,69 +1997,40 @@ where
LeptosOptions: FromRef<S>,
{
move |uri: Uri, State(state): State<S>, req: Request<Body>| {
Box::pin({
let additional_context = additional_context.clone();
let shell = shell.clone();
async move {
let options = LeptosOptions::from_ref(&state);
let res =
get_static_file(uri, &options.site_root, req.headers());
let res = res.await.unwrap();
Box::pin(async move {
let options = LeptosOptions::from_ref(&state);
let res = get_static_file(uri, &options.site_root, req.headers());
let res = res.await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else {
let mut res = handle_response_inner(
move || {
additional_context();
provide_context(state.clone());
},
move || shell(options),
req,
|app, chunks, _supports_ooo| {
Box::pin(async move {
let app = app
.to_html_stream_in_order()
.collect::<String>()
.await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>
})
},
)
.await;
*res.status_mut() = StatusCode::NOT_FOUND;
res
}
if res.status() == StatusCode::OK {
res.into_response()
} else {
let mut res = handle_response_inner(
move || {
provide_context(state.clone());
},
move || shell(options),
req,
|app, chunks| {
Box::pin(async move {
let app = app
.to_html_stream_in_order()
.collect::<String>()
.await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>
})
},
)
.await;
*res.status_mut() = StatusCode::NOT_FOUND;
res
}
})
}
}
/// A reasonable handler for serving static files (like JS/WASM/CSS) and 404 errors.
///
/// This is provided as a convenience, but is a fairly simple function. If you need to adapt it,
/// simply reuse the source code of this function in your own application.
#[cfg(feature = "default")]
pub fn file_and_error_handler<S, IV>(
shell: impl Fn(LeptosOptions) -> IV + 'static + Clone + Send,
) -> impl Fn(
Uri,
State<S>,
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ 'static
where
IV: IntoView + 'static,
S: Send + Sync + Clone + 'static,
LeptosOptions: FromRef<S>,
{
file_and_error_handler_with_context(move || (), shell)
}
#[cfg(feature = "default")]
async fn get_static_file(
uri: Uri,

View File

@@ -1,5 +1,3 @@
#![allow(clippy::type_complexity)]
use futures::{stream::once, Stream, StreamExt};
use hydration_context::{SharedContext, SsrSharedContext};
use leptos::{
@@ -33,20 +31,14 @@ pub trait ExtendResponse: Sized {
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
bool,
) -> PinnedFuture<PinnedStream<String>>,
supports_ooo: bool,
) -> impl Future<Output = Self> + Send
where
IV: IntoView + 'static,
{
async move {
let (owner, stream) = build_response(
app_fn,
additional_context,
stream_builder,
supports_ooo,
);
let (owner, stream) =
build_response(app_fn, additional_context, stream_builder);
let sc = owner.shared_context().unwrap();
@@ -102,11 +94,7 @@ pub fn build_response<IV>(
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
// this argument indicates whether a request wants to support out-of-order streaming
// responses
bool,
) -> PinnedFuture<PinnedStream<String>>,
is_islands_router_navigation: bool,
) -> (Owner, PinnedFuture<PinnedStream<String>>)
where
IV: IntoView + 'static,
@@ -150,7 +138,7 @@ where
//
// we also don't actually start hydrating until after the whole stream is complete,
// so it's not useful to send those scripts down earlier.
stream_builder(app, chunks, is_islands_router_navigation)
stream_builder(app, chunks)
});
stream.await

View File

@@ -42,17 +42,22 @@ typed-builder = "0.20.0"
typed-builder-macro = "0.20.0"
serde = "1.0"
serde_json = { version = "1.0", optional = true }
server_fn = { workspace = true, features = ["form-redirects", "browser"] }
server_fn = { workspace = true, features = [
"form-redirects",
"browser",
"url",
] }
web-sys = { version = "0.3.72", features = [
"ShadowRoot",
"ShadowRootInit",
"ShadowRootMode",
] }
wasm-bindgen = { workspace = true }
wasm-bindgen = "0.2.97"
serde_qs = "0.13.0"
slotmap = "1.0"
futures = "0.3.31"
send_wrapper = "0.6.0"
getrandom = { version = "0.2", features = ["js"], optional = true }
[features]
hydration = [
@@ -61,12 +66,13 @@ hydration = [
"hydration_context/browser",
"leptos_dom/hydration",
]
csr = ["leptos_macro/csr", "reactive_graph/effects"]
csr = ["leptos_macro/csr", "reactive_graph/effects", "dep:getrandom"]
hydrate = [
"leptos_macro/hydrate",
"hydration",
"tachys/hydrate",
"reactive_graph/effects",
"dep:getrandom",
]
default-tls = ["server_fn/default-tls"]
rustls = ["server_fn/rustls"]
@@ -78,7 +84,10 @@ ssr = [
"tachys/ssr",
]
nightly = ["leptos_macro/nightly", "reactive_graph/nightly", "tachys/nightly"]
rkyv = ["server_fn/rkyv", "leptos_server/rkyv"]
rkyv = [
"server_fn/rkyv",
"leptos_server/rkyv"
]
tracing = [
"dep:tracing",
"reactive_graph/tracing",
@@ -96,15 +105,6 @@ trace-component-props = [
]
delegation = ["tachys/delegation"]
# Having an erasure feature rather than normal --cfg erase_components for the proc macro crate is a workaround for this rust issue:
# https://github.com/rust-lang/cargo/issues/4423
# TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command.
# This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't.
# This is automatic as long as the leptos crate is depended upon,
# downstream usage should never manually enable this feature.
[target.'cfg(erase_components)'.dependencies]
leptos_macro = { workspace = true, features = ["__internal_erase_components"] }
[package.metadata.cargo-all-features]
denylist = [
"nightly",

View File

@@ -43,7 +43,7 @@ pub fn AttributeInterceptor<Chil, T>(
) -> impl IntoView
where
Chil: Fn(AnyAttribute) -> T + Send + Sync + 'static,
T: IntoView + 'static,
T: IntoView,
{
AttributeInterceptorInner::new(children)
}
@@ -86,7 +86,7 @@ impl<T: IntoView, A: Attribute> Render for AttributeInterceptorInner<T, A> {
}
}
impl<T: IntoView + 'static, A> AddAnyAttr for AttributeInterceptorInner<T, A>
impl<T: IntoView, A> AddAnyAttr for AttributeInterceptorInner<T, A>
where
A: Attribute,
{
@@ -114,11 +114,8 @@ where
}
}
impl<T: IntoView + 'static, A: Attribute> RenderHtml
for AttributeInterceptorInner<T, A>
{
impl<T: IntoView, A: Attribute> RenderHtml for AttributeInterceptorInner<T, A> {
type AsyncOutput = T::AsyncOutput;
type Owned = AttributeInterceptorInner<T, A::CloneableOwned>;
const MIN_LENGTH: usize = T::MIN_LENGTH;
@@ -138,15 +135,9 @@ impl<T: IntoView + 'static, A: Attribute> RenderHtml
position: &mut leptos::tachys::view::Position,
escape: bool,
mark_branches: bool,
_extra_attrs: Vec<AnyAttribute>,
) {
self.children.to_html_with_buf(
buf,
position,
escape,
mark_branches,
vec![],
)
self.children
.to_html_with_buf(buf, position, escape, mark_branches)
}
fn hydrate<const FROM_SERVER: bool>(
@@ -156,12 +147,4 @@ impl<T: IntoView + 'static, A: Attribute> RenderHtml
) -> Self::State {
self.children.hydrate::<FROM_SERVER>(cursor, position)
}
fn into_owned(self) -> Self::Owned {
AttributeInterceptorInner {
children_builder: self.children_builder,
children: self.children,
attributes: self.attributes.into_cloneable_owned(),
}
}
}

View File

@@ -43,20 +43,13 @@
use reactive_graph::{
owner::{LocalStorage, StoredValue},
traits::{Dispose, WithValue},
traits::WithValue,
};
use std::{fmt, rc::Rc, sync::Arc};
/// A wrapper trait for calling callbacks.
pub trait Callable<In: 'static, Out: 'static = ()> {
/// calls the callback with the specified argument.
///
/// Returns None if the callback has been disposed
fn try_run(&self, input: In) -> Option<Out>;
/// calls the callback with the specified argument.
///
/// # Panics
/// Panics if you try to run a callback that has been disposed
fn run(&self, input: In) -> Out;
}
@@ -79,12 +72,6 @@ impl<In, Out> Clone for UnsyncCallback<In, Out> {
}
}
impl<In, Out> Dispose for UnsyncCallback<In, Out> {
fn dispose(self) {
self.0.dispose();
}
}
impl<In, Out> UnsyncCallback<In, Out> {
/// Creates a new callback from the given function.
pub fn new<F>(f: F) -> UnsyncCallback<In, Out>
@@ -106,10 +93,6 @@ impl<In, Out> UnsyncCallback<In, Out> {
}
impl<In: 'static, Out: 'static> Callable<In, Out> for UnsyncCallback<In, Out> {
fn try_run(&self, input: In) -> Option<Out> {
self.0.try_with_value(|fun| fun(input))
}
fn run(&self, input: In) -> Out {
self.0.with_value(|fun| fun(input))
}
@@ -185,12 +168,10 @@ impl<In, Out> fmt::Debug for Callback<In, Out> {
}
impl<In, Out> Callable<In, Out> for Callback<In, Out> {
fn try_run(&self, input: In) -> Option<Out> {
self.0.try_with_value(|fun| fun(input))
}
fn run(&self, input: In) -> Out {
self.0.with_value(|f| f(input))
self.0
.try_with_value(|f| f(input))
.expect("called a callback that has been disposed")
}
}
@@ -200,12 +181,6 @@ impl<In, Out> Clone for Callback<In, Out> {
}
}
impl<In, Out> Dispose for Callback<In, Out> {
fn dispose(self) {
self.0.dispose();
}
}
impl<In, Out> Copy for Callback<In, Out> {}
macro_rules! impl_callable_from_fn {
@@ -264,9 +239,7 @@ impl<In: 'static, Out: 'static> Callback<In, Out> {
#[cfg(test)]
mod tests {
use super::Callable;
use crate::callback::{Callback, UnsyncCallback};
use reactive_graph::traits::Dispose;
struct NoClone {}
@@ -297,26 +270,10 @@ mod tests {
(|num, s| format!("{num} {s}")).into();
}
#[test]
fn sync_callback_try_run() {
let callback = Callback::new(move |arg| arg);
assert_eq!(callback.try_run((0,)), Some((0,)));
callback.dispose();
assert_eq!(callback.try_run((0,)), None);
}
#[test]
fn unsync_callback_try_run() {
let callback = UnsyncCallback::new(move |arg| arg);
assert_eq!(callback.try_run((0,)), Some((0,)));
callback.dispose();
assert_eq!(callback.try_run((0,)), None);
}
#[test]
fn callback_matches_same() {
let callback1 = Callback::new(|x: i32| x * 2);
let callback2 = callback1;
let callback2 = callback1.clone();
assert!(callback1.matches(&callback2));
}
@@ -330,7 +287,7 @@ mod tests {
#[test]
fn unsync_callback_matches_same() {
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
let callback2 = callback1;
let callback2 = callback1.clone();
assert!(callback1.matches(&callback2));
}

View File

@@ -91,9 +91,6 @@ pub trait ToChildren<F> {
fn to_children(f: F) -> Self;
}
/// Compiler optimisation, can be used with certain type to avoid unique closures in the view!{} macro.
pub struct ChildrenOptContainer<T>(pub T);
impl<F, C> ToChildren<F> for Children
where
F: FnOnce() -> C + Send + 'static,
@@ -105,16 +102,6 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for Children
where
T: IntoAny + Send + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
Box::new(move || t.0.into_any())
}
}
impl<F, C> ToChildren<F> for ChildrenFn
where
F: Fn() -> C + Send + Sync + 'static,
@@ -126,16 +113,6 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFn
where
T: IntoAny + Clone + Send + Sync + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
Arc::new(move || t.0.clone().into_any())
}
}
impl<F, C> ToChildren<F> for ChildrenFnMut
where
F: Fn() -> C + Send + 'static,
@@ -147,16 +124,6 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFnMut
where
T: IntoAny + Clone + Send + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
Box::new(move || t.0.clone().into_any())
}
}
impl<F, C> ToChildren<F> for BoxedChildrenFn
where
F: Fn() -> C + Send + 'static,
@@ -168,16 +135,6 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for BoxedChildrenFn
where
T: IntoAny + Clone + Send + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
Box::new(move || t.0.clone().into_any())
}
}
impl<F, C> ToChildren<F> for ChildrenFragment
where
F: FnOnce() -> C + Send + 'static,
@@ -189,16 +146,6 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFragment
where
T: IntoAny + Send + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
Box::new(move || Fragment::new(vec![t.0.into_any()]))
}
}
impl<F, C> ToChildren<F> for ChildrenFragmentFn
where
F: Fn() -> C + Send + 'static,
@@ -210,16 +157,6 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFragmentFn
where
T: IntoAny + Clone + Send + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
Arc::new(move || Fragment::new(vec![t.0.clone().into_any()]))
}
}
impl<F, C> ToChildren<F> for ChildrenFragmentMut
where
F: FnMut() -> C + Send + 'static,
@@ -231,16 +168,6 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFragmentMut
where
T: IntoAny + Clone + Send + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
Box::new(move || Fragment::new(vec![t.0.clone().into_any()]))
}
}
/// New-type wrapper for a function that returns a view with `From` and `Default` traits implemented
/// to enable optional props in for example `<Show>` and `<Suspense>`.
#[derive(Clone)]
@@ -319,16 +246,6 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for TypedChildren<T>
where
T: IntoView + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
TypedChildren(Box::new(move || t.0.into_view()))
}
}
/// A typed equivalent to [`ChildrenFnMut`], which takes a generic but preserves type information to
/// allow the compiler to optimize the view more effectively.
pub struct TypedChildrenMut<T>(Box<dyn FnMut() -> View<T> + Send>);
@@ -358,16 +275,6 @@ where
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for TypedChildrenMut<T>
where
T: IntoView + Clone + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
TypedChildrenMut(Box::new(move || t.0.clone().into_view()))
}
}
/// A typed equivalent to [`ChildrenFn`], which takes a generic but preserves type information to
/// allow the compiler to optimize the view more effectively.
pub struct TypedChildrenFn<T>(Arc<dyn Fn() -> View<T> + Send + Sync>);
@@ -403,13 +310,3 @@ where
TypedChildrenFn(Arc::new(move || f().into_view()))
}
}
impl<T> ToChildren<ChildrenOptContainer<T>> for TypedChildrenFn<T>
where
T: IntoView + Clone + Sync + 'static,
{
#[inline]
fn to_children(t: ChildrenOptContainer<T>) -> Self {
TypedChildrenFn(Arc::new(move || t.0.clone().into_view()))
}
}

View File

@@ -11,7 +11,7 @@ use reactive_graph::{
use rustc_hash::FxHashMap;
use std::{fmt::Debug, sync::Arc};
use tachys::{
html::attribute::{any_attribute::AnyAttribute, Attribute},
html::attribute::Attribute,
hydration::Cursor,
reactive_graph::OwnedView,
ssr::StreamBuilder,
@@ -163,14 +163,6 @@ where
self.children.insert_before_this(child)
}
}
fn elements(&self) -> Vec<tachys::renderer::types::Element> {
if let Some(fallback) = &self.fallback {
fallback.elements()
} else {
self.children.elements()
}
}
}
impl<Chil, FalFn, Fal> Render for ErrorBoundaryView<Chil, FalFn>
@@ -276,7 +268,6 @@ where
Fal: RenderHtml + Send + 'static,
{
type AsyncOutput = ErrorBoundaryView<Chil::AsyncOutput, FalFn>;
type Owned = Self;
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
@@ -310,7 +301,6 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
// first, attempt to serialize the children to HTML, then check for errors
let _hook = throw_error::set_error_hook(self.hook);
@@ -321,7 +311,6 @@ where
&mut new_pos,
escape,
mark_branches,
extra_attrs.clone(),
);
// any thrown errors would've been caught here
@@ -334,7 +323,6 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
}
@@ -345,7 +333,6 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
@@ -358,7 +345,6 @@ where
&mut new_pos,
escape,
mark_branches,
extra_attrs.clone(),
);
// any thrown errors would've been caught here
@@ -372,7 +358,6 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
buf.push_sync(&fallback);
}
@@ -438,10 +423,6 @@ where
},
)
}
fn into_owned(self) -> Self::Owned {
self
}
}
#[derive(Debug)]

View File

@@ -6,10 +6,7 @@ use reactive_graph::{
traits::Set,
};
use std::hash::Hash;
use tachys::{
reactive_graph::OwnedView,
view::keyed::{keyed, SerializableKey},
};
use tachys::{reactive_graph::OwnedView, view::keyed::keyed};
/// Iterates over children and displays them, keyed by the `key` function given.
///
@@ -124,7 +121,7 @@ where
EF: Fn(T) -> N + Send + Clone + 'static,
N: IntoView + 'static,
KF: Fn(&T) -> K + Send + Clone + 'static,
K: Eq + Hash + SerializableKey + 'static,
K: Eq + Hash + 'static,
T: Send + 'static,
{
// this takes the owner of the For itself
@@ -198,7 +195,7 @@ where
EF: Fn(ReadSignal<usize>, T) -> N + Send + Clone + 'static,
N: IntoView + 'static,
KF: Fn(&T) -> K + Send + Clone + 'static,
K: Eq + Hash + SerializableKey + 'static,
K: Eq + Hash + 'static,
T: Send + 'static,
{
// this takes the owner of the For itself
@@ -221,7 +218,6 @@ where
};
move || keyed(each(), key.clone(), children.clone())
}
/*
#[cfg(test)]
mod tests {

View File

@@ -3,11 +3,7 @@ use leptos_dom::helpers::window;
use leptos_server::{ServerAction, ServerMultiAction};
use serde::de::DeserializeOwned;
use server_fn::{
client::Client,
codec::PostUrl,
error::{IntoAppError, ServerFnErrorErr},
request::ClientReq,
Http, ServerFn,
client::Client, codec::PostUrl, request::ClientReq, ServerFn, ServerFnError,
};
use tachys::{
either::Either,
@@ -75,7 +71,7 @@ use web_sys::{
/// ```
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
#[component]
pub fn ActionForm<ServFn, OutputProtocol>(
pub fn ActionForm<ServFn>(
/// The action from which to build the form.
action: ServerAction<ServFn>,
/// A [`NodeRef`] in which the `<form>` element should be stored.
@@ -86,7 +82,7 @@ pub fn ActionForm<ServFn, OutputProtocol>(
) -> impl IntoView
where
ServFn: DeserializeOwned
+ ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
+ ServerFn<InputEncoding = PostUrl>
+ Clone
+ Send
+ Sync
@@ -125,10 +121,9 @@ where
"Error converting form field into server function \
arguments: {err:?}"
);
value.set(Some(Err(ServerFnErrorErr::Serialization(
value.set(Some(Err(ServerFnError::Serialization(
err.to_string(),
)
.into_app_error())));
))));
version.update(|n| *n += 1);
}
}
@@ -151,7 +146,7 @@ where
/// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)
/// progressively enhanced to use client-side routing.
#[component]
pub fn MultiActionForm<ServFn, OutputProtocol>(
pub fn MultiActionForm<ServFn>(
/// The action from which to build the form.
action: ServerMultiAction<ServFn>,
/// A [`NodeRef`] in which the `<form>` element should be stored.
@@ -165,7 +160,7 @@ where
+ Sync
+ Clone
+ DeserializeOwned
+ ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
+ ServerFn<InputEncoding = PostUrl>
+ 'static,
ServFn::Output: Send + Sync + 'static,
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<
@@ -192,10 +187,9 @@ where
action.dispatch(new_input);
}
Err(err) => {
action.dispatch_sync(Err(ServerFnErrorErr::Serialization(
action.dispatch_sync(Err(ServerFnError::Serialization(
err.to_string(),
)
.into_app_error()));
)));
}
}
};

View File

@@ -52,8 +52,6 @@
mod.hydrate();
hydrateIslands(document.body, mod);
});
window.__hydrateIsland = (el, id) => hydrateIsland(el, id, mod);
})
});
})

View File

@@ -1,378 +0,0 @@
let NAVIGATION = 0;
window.addEventListener("click", async (ev) => {
const req = clickToReq(ev);
if(!req) {
return;
}
ev.preventDefault();
await navigateToPage(req, true);
});
window.addEventListener("popstate", async (ev) => {
const req = new Request(window.location);
ev.preventDefault();
await navigateToPage(req, true, true);
});
window.addEventListener("submit", async (ev) => {
const req = submitToReq(ev);
if(!req) {
return;
}
ev.preventDefault();
await navigateToPage(req, true);
});
async function navigateToPage(
/** @type Request */
req,
/** @type bool */
useViewTransition,
/** @type bool */
replace
) {
NAVIGATION += 1;
const currentNav = NAVIGATION;
// add a custom header to indicate that we're on a subsequent navigation
req.headers.append("Islands-Router", "true");
// fetch the new page
const resp = await fetch(req);
const redirected = resp.redirected;
const htmlString = await resp.text();
if(NAVIGATION === currentNav) {
// The 'doc' variable now contains the parsed DOM
const transition = async () => {
try {
diffPages(htmlString);
for(const island of document.querySelectorAll("leptos-island")) {
if(!island.$$hydrated) {
__hydrateIsland(island, island.dataset.component);
island.$$hydrated = true;
}
}
} catch(e) {
console.error(e);
}
};
// Not all browsers support startViewTransition; see https://caniuse.com/?search=startViewTransition
if (useViewTransition && document.startViewTransition) {
await document.startViewTransition(transition);
} else {
await transition()
}
const url = redirected ? resp.url : req.url;
if(replace) {
window.history.replaceState(undefined, null, url);
} else {
window.history.pushState(undefined, null, url);
}
}
}
function clickToReq(ev) {
// confirm that this is an <a> that meets our requirements
if (
ev.defaultPrevented ||
ev.button !== 0 ||
ev.metaKey ||
ev.altKey ||
ev.ctrlKey ||
ev.shiftKey
)
return;
/** @type HTMLAnchorElement | undefined;*/
const a = ev
.composedPath()
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
if (!a) return;
const svg = a.namespaceURI === "http://www.w3.org/2000/svg";
const href = svg ? a.href.baseVal : a.href;
const target = svg ? a.target.baseVal : a.target;
if (target || (!href && !a.hasAttribute("state"))) return;
const rel = (a.getAttribute("rel") || "").split(/\s+/);
if (a.hasAttribute("download") || (rel?.includes("external"))) return;
const url = svg ? new URL(href, document.baseURI) : new URL(href);
if (
url.origin !== window.location.origin // ||
// TODO base
//(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase()))
)
return;
return new Request(url);
}
function submitToReq(ev) {
event.preventDefault();
const target = ev.target;
/** @type HTMLFormElement */
let form;
if(target instanceof HTMLFormElement) {
form = target;
} else {
if(!target.form) {
return;
}
form = target.form;
}
const method = form.method.toUpperCase();
if(method !== "GET" && method !== "POST") {
return;
}
const url = new URL(form.action);
let path = url.pathname;
const requestInit = {};
const data = new FormData(form);
const params = new URLSearchParams();
for (const [key, value] of data.entries()) {
params.append(key, value);
}
requestInit.headers = {
Accept: "text/html"
};
if(method === "GET") {
path += `?${params.toString()}`;
}
else {
requestInit.method = "POST";
requestInit.body = params;
}
return new Request(
path,
requestInit
);
}
function diffPages(htmlString) {
// Use DOMParser to parse the HTML string
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/html');
diffRange(document, document, doc, doc);
}
function diffRange(oldDocument, oldRoot, newDocument, newRoot, oldEnd, newEnd) {
const oldDocWalker = oldDocument.createTreeWalker(oldRoot);
const newDocWalker = newDocument.createTreeWalker(newRoot);
let oldNode = oldDocWalker.currentNode;
let newNode = newDocWalker.currentNode;
while (oldDocWalker.nextNode() && newDocWalker.nextNode()) {
oldNode = oldDocWalker.currentNode;
newNode = newDocWalker.currentNode;
if (oldNode == oldEnd || newNode == newEnd) {
break;
}
// if the nodes are different, we need to replace the old with the new
// because of the typed view tree, this should never actually happen
if (oldNode.nodeType !== newNode.nodeType) {
oldNode.replaceWith(newNode);
}
// if it's a text node, just update the text with the new text
else if (oldNode.nodeType === Node.TEXT_NODE) {
oldNode.textContent = newNode.textContent;
}
// if it's an element, replace if it's a different tag, or update attributes
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
diffElement(oldNode, newNode);
}
// we use comment "branch marker" nodes to distinguish between different branches in the statically-typed view tree
// if one of these marker is hit, then there are two options
// 1) it's the same branch, and we just keep walking until the end
// 2) it's a different branch, in which case the old can be replaced with the new wholesale
else if (oldNode.nodeType === Node.COMMENT_NODE) {
const oldText = oldNode.textContent;
const newText = newNode.textContent;
if(oldText.startsWith("bo-for")) {
replaceFor(oldDocument, oldDocWalker, newDocument, newDocWalker, oldNode, newNode);
}
else if (oldText.startsWith("bo-item")) {
// skip, this means we're diffing a new item within a For
}
else if(oldText.startsWith("bo") && newText !== oldText) {
replaceBranch(oldDocWalker, newDocWalker, oldNode, newNode);
}
}
}
}
function replaceFor(oldDocument, oldDocWalker, newDocument, newDocWalker, oldNode, newNode) {
oldDocWalker.nextNode();
newDocWalker.nextNode();
const oldRange = new Range();
const newRange = new Range();
let oldBranches = 1;
let newBranches = 1;
const oldKeys = {};
const newKeys = {};
while(oldBranches > 0) {
const c = oldDocWalker.currentNode;
if(c.nodeType === Node.COMMENT_NODE) {
const t = c.textContent;
if(t.startsWith("bo-for")) {
oldBranches += 1;
} else if(t.startsWith("bc-for")) {
oldBranches -= 1;
} else if (t.startsWith("bo-item")) {
const k = t.replace("bo-item-", "");
oldKeys[k] = { open: c, close: null };
} else if (t.startsWith("bc-item")) {
const k = t.replace("bc-item-", "");
oldKeys[k].close = c;
}
}
oldDocWalker.nextNode();
}
while(newBranches > 0) {
const c = newDocWalker.currentNode;
if(c.nodeType === Node.COMMENT_NODE) {
const t = c.textContent;
if(t.startsWith("bo-for")) {
newBranches += 1;
} else if(t.startsWith("bc-for")) {
newBranches -= 1;
} else if (t.startsWith("bo-item")) {
const k = t.replace("bo-item-", "");
newKeys[k] = { open: c, close: null };
} else if (t.startsWith("bc-item")) {
const k = t.replace("bc-item-", "");
newKeys[k].close = c;
}
}
newDocWalker.nextNode();
}
for(const key in oldKeys) {
if(newKeys[key]) {
const oldOne = oldKeys[key];
const newOne = newKeys[key];
const oldRange = new Range();
const newRange = new Range();
// then replace the item in the *new* list with the *old* DOM elements
oldRange.setStartAfter(oldOne.open);
oldRange.setEndBefore(oldOne.close);
newRange.setStartAfter(newOne.open);
newRange.setEndBefore(newOne.close);
const oldContents = oldRange.extractContents();
const newContents = newRange.extractContents();
// patch the *old* DOM elements with the new ones
diffRange(oldDocument, oldContents, newDocument, newContents, oldOne.close, newOne.close);
// then insert the old DOM elements into the new tree
// this means you'll end up with any new attributes or content from the server,
// but with any old DOM state (because they are the old elements)
newRange.insertNode(oldContents);
newOne.open.replaceWith(oldOne.open);
newOne.close.replaceWith(oldOne.close);
}
}
try {
oldRange.setStartAfter(oldNode);
oldRange.setEndBefore(oldDocWalker.currentNode);
newRange.setStartAfter(newNode);
newRange.setEndAfter(newDocWalker.currentNode);
const newContents = newRange.extractContents();
oldRange.deleteContents();
oldRange.insertNode(newContents);
oldNode.replaceWith(newNode);
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
} catch (e) {
console.error(e);
}
}
function replaceBranch(oldDocWalker, newDocWalker, oldNode, newNode) {
oldDocWalker.nextNode();
newDocWalker.nextNode();
const oldRange = new Range();
const newRange = new Range();
let oldBranches = 1;
let newBranches = 1;
while(oldBranches > 0) {
if(oldDocWalker.nextNode()) {
if(oldDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
if(oldDocWalker.currentNode.textContent.startsWith("bo")) {
oldBranches += 1;
} else if(oldDocWalker.currentNode.textContent.startsWith("bc")) {
oldBranches -= 1;
}
}
}
}
while(newBranches > 0) {
if(newDocWalker.nextNode()) {
if(newDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
if(newDocWalker.currentNode.textContent.startsWith("bo")) {
newBranches += 1;
} else if(newDocWalker.currentNode.textContent.startsWith("bc")) {
newBranches -= 1;
}
}
}
}
try {
oldRange.setStartAfter(oldNode);
oldRange.setEndBefore(oldDocWalker.currentNode);
newRange.setStartAfter(newNode);
newRange.setEndAfter(newDocWalker.currentNode);
const newContents = newRange.extractContents();
oldRange.deleteContents();
oldRange.insertNode(newContents);
oldNode.replaceWith(newNode);
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
} catch (e) {
console.error(e);
}
}
function diffElement(oldNode, newNode) {
/** @type Element */
const oldEl = oldNode;
/** @type Element */
const newEl = newNode;
if (oldEl.tagName !== newEl.tagName) {
oldEl.replaceWith(newEl);
}
else {
for(const attr of newEl.attributes) {
oldEl.setAttribute(attr.name, attr.value);
}
}
}
for(const island of document.querySelectorAll("leptos-island")) {
island.$$hydrated = true;
}

View File

@@ -50,10 +50,6 @@ pub fn HydrationScripts(
/// Should be `true` to hydrate in `islands` mode.
#[prop(optional)]
islands: bool,
/// Should be `true` to add the “islands router,” which enables limited client-side routing
/// when running in islands mode.
#[prop(optional)]
islands_router: bool,
/// A base url, not including a trailing slash
#[prop(optional, into)]
root: Option<String>,
@@ -102,36 +98,18 @@ pub fn HydrationScripts(
include_str!("./hydration_script.js")
};
let islands_router = islands_router
.then_some(include_str!("./islands_routing.js"))
.unwrap_or_default();
let root = root.unwrap_or_default();
use_context::<IslandsRouterNavigation>().is_none().then(|| {
view! {
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
<link
rel="preload"
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
r#as="fetch"
r#type="application/wasm"
crossorigin=nonce.clone().unwrap_or_default()
/>
<script type="module" nonce=nonce>
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?});{islands_router}")}
</script>
}
})
view! {
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
<link
rel="preload"
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
r#as="fetch"
r#type="application/wasm"
crossorigin=nonce.clone().unwrap_or_default()
/>
<script type="module" nonce=nonce>
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?})")}
</script>
}
}
/// If this is provided via context, it means that you are using the islands router and
/// this is a subsequent navigation, made from the client.
///
/// This should be provided automatically by a server integration if it detects that the
/// header `Islands-Router` is present in the request.
///
/// This is used to determine how much of the hydration script to include in the page.
/// If it is present, then the contents of the `<HydrationScripts>` component will not be
/// included, as they only need to be sent to the client once.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IslandsRouterNavigation;

View File

@@ -1,6 +1,6 @@
use std::borrow::Cow;
use tachys::{
html::attribute::{any_attribute::AnyAttribute, Attribute},
html::attribute::Attribute,
hydration::Cursor,
ssr::StreamBuilder,
view::{
@@ -87,7 +87,6 @@ impl<T: Render> Render for View<T> {
impl<T: RenderHtml> RenderHtml for View<T> {
type AsyncOutput = T::AsyncOutput;
type Owned = View<T::Owned>;
const MIN_LENGTH: usize = <T as RenderHtml>::MIN_LENGTH;
@@ -105,7 +104,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
#[cfg(debug_assertions)]
let vm = self.view_marker.to_owned();
@@ -114,13 +112,8 @@ impl<T: RenderHtml> RenderHtml for View<T> {
buf.push_str(&format!("<!--hot-reload|{vm}|open-->"));
}
self.inner.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
self.inner
.to_html_with_buf(buf, position, escape, mark_branches);
#[cfg(debug_assertions)]
if let Some(vm) = vm.as_ref() {
@@ -134,7 +127,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
@@ -150,7 +142,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
position,
escape,
mark_branches,
extra_attrs,
);
#[cfg(debug_assertions)]
@@ -166,14 +157,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
) -> Self::State {
self.inner.hydrate::<FROM_SERVER>(cursor, position)
}
fn into_owned(self) -> Self::Owned {
View {
inner: self.inner.into_owned(),
#[cfg(debug_assertions)]
view_marker: self.view_marker,
}
}
}
impl<T: ToTemplate> ToTemplate for View<T> {

View File

@@ -172,10 +172,12 @@ pub mod prelude {
actions::*, computed::*, effect::*, graph::untrack, owner::*,
signal::*, wrappers::read::*,
};
pub use server_fn::{self, error::ServerFnError};
pub use server_fn::{self, ServerFnError};
pub use tachys::{
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
view::{fragment::Fragment, template::ViewTemplate},
view::{
any_view::AnyView, fragment::Fragment, template::ViewTemplate,
},
};
}
pub use export_types::*;

View File

@@ -19,7 +19,7 @@ use slotmap::{DefaultKey, SlotMap};
use std::sync::Arc;
use tachys::{
either::Either,
html::attribute::{any_attribute::AnyAttribute, Attribute},
html::attribute::Attribute,
hydration::Cursor,
reactive_graph::{OwnedView, OwnedViewState},
ssr::StreamBuilder,
@@ -247,7 +247,6 @@ where
// i.e., if this is the child of another Suspense during SSR, don't wait for it: it will handle
// itself
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
@@ -263,15 +262,9 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
self.fallback.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
self.fallback
.to_html_with_buf(buf, position, escape, mark_branches);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@@ -280,7 +273,6 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
@@ -311,13 +303,11 @@ where
let eff = reactive_graph::effect::Effect::new_isomorphic({
move |_| {
tasks.track();
if let Some(tasks) = tasks.try_read() {
if tasks.is_empty() {
if let Some(tx) = tasks_tx.take() {
// If the receiver has dropped, it means the ScopedFuture has already
// dropped, so it doesn't matter if we manage to send this.
_ = tx.send(());
}
if tasks.read().is_empty() {
if let Some(tx) = tasks_tx.take() {
// If the receiver has dropped, it means the ScopedFuture has already
// dropped, so it doesn't matter if we manage to send this.
_ = tx.send(());
}
}
}
@@ -379,7 +369,6 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
Some(None) => {
@@ -389,7 +378,6 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
None => {
@@ -403,14 +391,12 @@ where
self.fallback,
&mut fallback_position,
mark_branches,
extra_attrs.clone(),
);
buf.push_async_out_of_order_with_nonce(
fut,
position,
mark_branches,
nonce_or_not(),
extra_attrs,
);
} else {
buf.push_async({
@@ -426,7 +412,6 @@ where
&mut position,
escape,
mark_branches,
extra_attrs,
);
builder.finish().take_chunks()
}
@@ -476,10 +461,6 @@ where
}
})
}
fn into_owned(self) -> Self::Owned {
self
}
}
/// A wrapper that prevents [`Suspense`] from waiting for any resource reads that happen inside
@@ -532,7 +513,6 @@ where
T: RenderHtml + 'static,
{
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = T::MIN_LENGTH;
@@ -548,15 +528,8 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
(self.0)().to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
(self.0)().to_html_with_buf(buf, position, escape, mark_branches);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@@ -565,7 +538,6 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
@@ -574,7 +546,6 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -585,8 +556,4 @@ where
) -> Self::State {
(self.0)().hydrate::<FROM_SERVER>(cursor, position)
}
fn into_owned(self) -> Self::Owned {
self
}
}

View File

@@ -76,9 +76,9 @@ impl Default for TextProp {
}
impl IntoAttributeValue for TextProp {
type Output = Arc<dyn Fn() -> Oco<'static, str> + Send + Sync>;
type Output = Oco<'static, str>;
fn into_attribute_value(self) -> Self::Output {
self.0
self.get()
}
}

View File

@@ -10,7 +10,7 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
config = { version = "0.15.8", default-features = false, features = [
config = { version = "0.14.1", default-features = false, features = [
"toml",
"convert-case",
] }
@@ -20,7 +20,7 @@ thiserror = "2.0"
typed-builder = "0.20.0"
[dev-dependencies]
tokio = { version = "1.43", features = ["rt", "macros"] }
tokio = { version = "1.41", features = ["rt", "macros"] }
tempfile = "3.14"
temp-env = { version = "0.3.6", features = ["async_closure"] }
@@ -28,4 +28,4 @@ temp-env = { version = "0.3.6", features = ["async_closure"] }
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -12,9 +12,8 @@ use typed_builder::TypedBuilder;
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
/// occur with LeptosOptions
#[derive(Clone, Debug, serde::Deserialize)]
#[derive(Clone, Debug, serde::Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct ConfFile {
pub leptos_options: LeptosOptions,
}
@@ -25,14 +24,9 @@ pub struct ConfFile {
/// It shares keys with cargo-leptos, to allow for easy interoperability
#[derive(TypedBuilder, Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct LeptosOptions {
/// The name of the WASM and JS files generated by wasm-bindgen.
///
/// This should match the name that will be output when building your application.
///
/// You can easily set this using `env!("CARGO_CRATE_NAME")`.
#[builder(setter(into))]
/// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes
#[builder(setter(into), default=default_output_name())]
pub output_name: Arc<str>,
/// The path of the all the files generated by cargo-leptos. This defaults to '.' for convenience when integrating with other
/// tools.
@@ -84,40 +78,6 @@ pub struct LeptosOptions {
#[builder(default = default_hash_files())]
#[serde(default = "default_hash_files")]
pub hash_files: bool,
/// The default prefix to use for server functions when generating API routes. Can be
/// overridden for individual functions using `#[server(prefix = "...")]` as usual.
///
/// This is useful to override the default prefix (`/api`) for all server functions without
/// needing to manually specify via `#[server(prefix = "...")]` on every server function.
#[builder(default, setter(strip_option))]
#[serde(default)]
pub server_fn_prefix: Option<String>,
/// Whether to disable appending the server functions' hashes to the end of their API names.
///
/// This is useful when an app's client side needs a stable server API. For example, shipping
/// the CSR WASM binary in a Tauri app. Tauri app releases are dependent on each platform's
/// distribution method (e.g., the Apple App Store or the Google Play Store), which typically
/// are much slower than the frequency at which a website can be updated. In addition, it's
/// common for users to not have the latest app version installed. In these cases, the CSR WASM
/// app would need to be able to continue calling the backend server function API, so the API
/// path needs to be consistent and not have a hash appended.
///
/// Note that the hash suffixes is intended as a way to ensure duplicate API routes are created.
/// Without the hash, server functions will need to have unique names to avoid creating
/// duplicate routes. Axum will throw an error if a duplicate route is added to the router, but
/// Actix will not.
#[builder(default)]
#[serde(default)]
pub disable_server_fn_hash: bool,
/// Include the module path of the server function in the API route. This is an alternative
/// strategy to prevent duplicate server function API routes (the default strategy is to add
/// a hash to the end of the route). Each element of the module path will be separated by a `/`.
/// For example, a server function with a fully qualified name of `parent::child::server_fn`
/// would have an API route of `/api/parent/child/server_fn` (possibly with a
/// different prefix and a hash suffix depending on the values of the other server fn configs).
#[builder(default)]
#[serde(default)]
pub server_fn_mod_path: bool,
}
impl LeptosOptions {
@@ -160,14 +120,20 @@ impl LeptosOptions {
hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?
.into(),
hash_files: env_w_default("LEPTOS_HASH_FILES", "false")?.parse()?,
server_fn_prefix: env_wo_default("SERVER_FN_PREFIX")?,
disable_server_fn_hash: env_wo_default("DISABLE_SERVER_FN_HASH")?
.is_some(),
server_fn_mod_path: env_wo_default("SERVER_FN_MOD_PATH")?.is_some(),
})
}
}
impl Default for LeptosOptions {
fn default() -> Self {
LeptosOptions::builder().build()
}
}
fn default_output_name() -> Arc<str> {
env!("CARGO_CRATE_NAME").replace('-', "_").into()
}
fn default_site_root() -> Arc<str> {
".".into()
}

View File

@@ -15,7 +15,7 @@ or_poisoned = { workspace = true }
js-sys = "0.3.74"
send_wrapper = "0.6.0"
tracing = { version = "0.1.41", optional = true }
wasm-bindgen = { workspace = true }
wasm-bindgen = "0.2.97"
serde_json = { version = "1.0", optional = true }
serde = { version = "1.0", optional = true }
@@ -39,4 +39,4 @@ rustdoc-args = ["--generate-link-to-definition"]
denylist = ["tracing"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = { workspace = true }
version = "0.7.5"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -16,7 +16,7 @@ proc-macro = true
attribute-derive = { version = "0.10.3", features = ["syn-full"] }
cfg-if = "1.0"
html-escape = "0.2.13"
itertools = { workspace = true }
itertools = "0.13.0"
prettyplease = "0.2.25"
proc-macro-error2 = { version = "2.0", default-features = false }
proc-macro2 = "1.0"
@@ -25,7 +25,7 @@ syn = { version = "2.0", features = ["full"] }
rstml = "0.12.0"
leptos_hot_reload = { workspace = true }
server_fn_macro = { workspace = true }
convert_case = "0.7"
convert_case = "0.6.0"
uuid = { version = "1.11", features = ["v4"] }
tracing = { version = "0.1.41", optional = true }
@@ -34,7 +34,7 @@ log = "0.4.22"
typed-builder = "0.20.0"
trybuild = "1.0"
leptos = { path = "../leptos" }
leptos_router = { path = "../router", features = ["ssr"] }
leptos_router = { path = "../router", features= ["ssr"] }
server_fn = { path = "../server_fn", features = ["cbor"] }
insta = "1.41"
serde = "1.0"
@@ -51,42 +51,41 @@ trace-component-props = []
actix = ["server_fn_macro/actix"]
axum = ["server_fn_macro/axum"]
generic = ["server_fn_macro/generic"]
# Having an erasure feature rather than normal --cfg erase_components for the proc macro crate is a workaround for this rust issue:
# https://github.com/rust-lang/cargo/issues/4423
# TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command.
# This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't.
# This is automatic as long as the leptos crate is depended upon,
# downstream usage should never manually enable this feature.
__internal_erase_components = []
[package.metadata.cargo-all-features]
denylist = ["nightly", "tracing", "trace-component-props"]
skip_feature_sets = [
[
"csr",
"hydrate",
],
[
"hydrate",
"csr",
],
[
"hydrate",
"ssr",
],
[
"actix",
"axum",
],
[
"actix",
"generic",
],
[
"generic",
"axum",
],
[
"csr",
"hydrate",
],
[
"hydrate",
"csr",
],
[
"hydrate",
"ssr",
],
[
"actix",
"axum",
],
[
"actix",
"generic",
],
[
"generic",
"axum",
],
]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(leptos_debuginfo)',
'cfg(erase_components)',
] }

View File

@@ -11,13 +11,13 @@ dependencies = [
[tasks.test-leptos_macro-example]
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly-2025-03-05", "test", "--doc"]
args = ["+nightly-2024-08-01", "test", "--doc"]
cwd = "example"
install_crate = false
[tasks.doc-leptos_macro-example]
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly-2025-03-05", "doc"]
args = ["+nightly-2024-08-01", "doc"]
cwd = "example"
install_crate = false

View File

@@ -32,8 +32,6 @@ pub struct Model {
impl Parse for Model {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut item = ItemFn::parse(input)?;
maybe_modify_return_type(&mut item.sig.output);
convert_impl_trait_to_generic(&mut item.sig);
let docs = Docs::new(&item.attrs);
@@ -78,39 +76,6 @@ impl Parse for Model {
}
}
/// Exists to fix nested routes defined in a separate component in erased mode,
/// by replacing the return type with AnyNestedRoute, which is what it'll be, but is required as the return type for compiler inference.
fn maybe_modify_return_type(ret: &mut ReturnType) {
#[cfg(feature = "__internal_erase_components")]
{
if let ReturnType::Type(_, ty) = ret {
if let Type::ImplTrait(TypeImplTrait { bounds, .. }) = ty.as_ref() {
// If one of the bounds is MatchNestedRoutes, we need to replace the return type with AnyNestedRoute:
if bounds.iter().any(|bound| {
if let syn::TypeParamBound::Trait(trait_bound) = bound {
if trait_bound.path.segments.iter().any(
|path_segment| {
path_segment.ident == "MatchNestedRoutes"
},
) {
return true;
}
}
false
}) {
*ty = parse_quote!(
::leptos_router::any_nested_route::AnyNestedRoute
);
}
}
}
}
#[cfg(not(feature = "__internal_erase_components"))]
{
let _ = ret;
}
}
// implemented manually because Vec::drain_filter is nightly only
// follows std recommended parallel
pub fn drain_filter<T>(
@@ -179,6 +144,8 @@ impl ToTokens for Model {
let (impl_generics, generics, where_clause) =
body.sig.generics.split_for_impl();
let lifetimes = body.sig.generics.lifetimes();
let props_name = format_ident!("{name}Props");
let props_builder_name = format_ident!("{name}PropsBuilder");
let props_serialized_name = format_ident!("{name}PropsSerialized");
@@ -331,9 +298,9 @@ impl ToTokens for Model {
let component = if *is_transparent {
body_expr
} else if cfg!(feature = "__internal_erase_components") {
} else if cfg!(erase_components) {
quote! {
::leptos::prelude::IntoMaybeErased::into_maybe_erased(
::leptos::prelude::IntoAny::into_any(
::leptos::reactive::graph::untrack_with_diagnostics(
move || {
#tracing_guard_expr
@@ -603,7 +570,7 @@ impl ToTokens for Model {
#tracing_instrument_attr
#vis fn #name #impl_generics (
#props_arg
) #ret
) #ret #(+ #lifetimes)*
#where_clause
{
#body
@@ -648,8 +615,7 @@ impl Parse for DummyModel {
drain_filter(&mut attrs, |attr| !attr.path().is_ident("doc"));
let vis: Visibility = input.parse()?;
let mut sig: Signature = input.parse()?;
maybe_modify_return_type(&mut sig.output);
let sig: Signature = input.parse()?;
// The body is left untouched, so it will not cause an error
// even if the syntax is invalid.

View File

@@ -281,11 +281,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
#[proc_macro]
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
pub fn template(tokens: TokenStream) -> TokenStream {
if cfg!(feature = "__internal_erase_components") {
view(tokens)
} else {
view_macro_impl(tokens, true)
}
view_macro_impl(tokens, true)
}
fn view_macro_impl(tokens: TokenStream, template: bool) -> TokenStream {
@@ -681,21 +677,17 @@ fn component_macro(
#[allow(non_snake_case, dead_code, clippy::too_many_arguments, clippy::needless_lifetimes)]
#unexpanded
}
} else {
match dummy {
Ok(mut dummy) => {
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
quote! {
#[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments, clippy::needless_lifetimes)]
#dummy
}
}
Err(e) => {
proc_macro_error2::abort!(e.span(), e);
}
} else if let Ok(mut dummy) = dummy {
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
quote! {
#[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments, clippy::needless_lifetimes)]
#dummy
}
}.into()
} else {
quote! {}
}
.into()
}
/// Annotates a struct so that it can be used with your Component as a `slot`.
@@ -927,7 +919,7 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
args.into(),
s.into(),
Some(syn::parse_quote!(::leptos::server_fn)),
option_env!("SERVER_FN_PREFIX").unwrap_or("/api"),
"/api",
None,
None,
) {

View File

@@ -1,19 +1,15 @@
use super::{
fragment_to_tokens, utils::is_nostrip_optional_and_update_key, TagType,
};
use crate::view::{
attribute_absolute, text_to_tokens, utils::filter_prefixed_attrs,
};
use crate::view::{attribute_absolute, utils::filter_prefixed_attrs};
use proc_macro2::{Ident, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned};
use rstml::node::{
CustomNode, KeyedAttributeValue, Node, NodeAttribute, NodeBlock,
NodeElement, NodeName,
CustomNode, KeyedAttributeValue, NodeAttribute, NodeBlock, NodeElement,
NodeName,
};
use std::collections::HashMap;
use syn::{
spanned::Spanned, Expr, ExprPath, ExprRange, Item, RangeLimits, Stmt,
};
use syn::{spanned::Spanned, Expr, ExprPath, ExprRange, RangeLimits, Stmt};
pub(crate) fn component_to_tokens(
node: &mut NodeElement<impl CustomNode>,
@@ -174,14 +170,8 @@ pub(crate) fn component_to_tokens(
.collect::<Vec<_>>();
let spreads = (!(spreads.is_empty())).then(|| {
if cfg!(feature = "__internal_erase_components") {
quote! {
.add_any_attr(vec![#(#spreads.into_any_attr(),)*])
}
} else {
quote! {
.add_any_attr((#(#spreads,)*))
}
quote! {
.add_any_attr((#(#spreads,)*).into_attr())
}
});
@@ -201,12 +191,6 @@ pub(crate) fn component_to_tokens(
let mut slots = HashMap::new();
let children = if node.children.is_empty() {
quote! {}
} else if let Some(children) = maybe_optimised_component_children(
&node.children,
&items_to_bind,
&items_to_clone,
) {
children
} else {
let children = fragment_to_tokens(
&mut node.children,
@@ -235,7 +219,10 @@ pub(crate) fn component_to_tokens(
let bindables =
items_to_bind.iter().map(|ident| quote! { #ident, });
let clonables = items_to_clone_to_tokens(&items_to_clone);
let clonables = items_to_clone.iter().map(|ident| {
let ident_ref = quote_spanned!(ident.span()=> &#ident);
quote! { let #ident = ::core::clone::Clone::clone(#ident_ref); }
});
if bindables.len() > 0 {
quote_spanned! {children.span()=>
@@ -326,111 +313,3 @@ fn is_attr_let(key: &NodeName) -> bool {
false
}
}
pub fn items_to_clone_to_tokens<'a>(
items_to_clone: &'a [Ident],
) -> impl Iterator<Item = TokenStream> + 'a {
items_to_clone.iter().map(|ident| {
let ident_ref = quote_spanned!(ident.span()=> &#ident);
quote! { let #ident = ::core::clone::Clone::clone(#ident_ref); }
})
}
/// By default all children are placed in an outer closure || #children.
/// This is to work with all the variants of the leptos::children::ToChildren::to_children trait.
/// Strings are optimised to be passed without the wrapping closure, providing significant compile time and binary size improvements.
pub fn maybe_optimised_component_children(
children: &[Node<impl CustomNode>],
items_to_bind: &[TokenStream],
items_to_clone: &[Ident],
) -> Option<TokenStream> {
// If there are bindables will have to be in a closure:
if !items_to_bind.is_empty() {
return None;
}
// Filter out comments:
let mut children_iter = children
.iter()
.filter(|child| !matches!(child, Node::Comment(_)));
let children = if let Some(child) = children_iter.next() {
// If more than one child after filtering out comments, don't think we can optimise:
if children_iter.next().is_some() {
return None;
}
match child {
Node::Text(text) => text_to_tokens(&text.value),
Node::RawText(raw) => {
let text = raw.to_string_best();
let text = syn::LitStr::new(&text, raw.span());
text_to_tokens(&text)
}
// Specifically allow std macros that produce strings:
Node::Block(NodeBlock::ValidBlock(block)) => {
fn is_supported(mac: &syn::Macro) -> bool {
for string_macro in ["format", "include_str"] {
if mac.path.is_ident(string_macro) {
return true;
}
}
false
}
if block.stmts.len() > 1 {
return None;
} else if let Some(stmt) = block.stmts.first() {
match stmt {
Stmt::Macro(mac) => {
// eprintln!("Macro: {:?}", mac.mac.path);
if is_supported(&mac.mac) {
quote! { #block }
} else {
return None;
}
}
Stmt::Item(Item::Macro(mac)) => {
// eprintln!("Item Macro: {:?}", mac.mac.path);
if is_supported(&mac.mac) {
quote! { #block }
} else {
return None;
}
}
Stmt::Expr(Expr::Macro(mac), _) => {
// eprintln!("Expr Macro: {:?}", mac.mac.path);
if is_supported(&mac.mac) {
quote! { #block }
} else {
return None;
}
}
_ => return None,
}
} else {
return Some(quote! {});
}
}
_ => return None,
}
} else {
return None;
};
// // Debug check to see how many use this optimisation:
// static COUNT: std::sync::atomic::AtomicUsize =
// std::sync::atomic::AtomicUsize::new(0);
// COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
// eprintln!(
// "Optimised children: {}",
// COUNT.load(std::sync::atomic::Ordering::Relaxed)
// );
let clonables = items_to_clone_to_tokens(items_to_clone);
Some(quote_spanned! {children.span()=>
.children({
#(#clonables)*
::leptos::children::ToChildren::to_children(::leptos::children::ChildrenOptContainer(#children))
})
})
}

View File

@@ -154,12 +154,7 @@ fn is_inert_element(orig_node: &Node<impl CustomNode>) -> bool {
Some(value) => {
matches!(&value.value, KVAttributeValue::Expr(expr) if {
if let Expr::Lit(lit) = expr {
let key = attr.key.to_string();
if key.starts_with("style:") || key.starts_with("prop:") || key.starts_with("on:") || key.starts_with("use:") || key.starts_with("bind") {
false
} else {
matches!(&lit.lit, Lit::Str(_))
}
matches!(&lit.lit, Lit::Str(_))
} else {
false
}
@@ -428,12 +423,6 @@ fn element_children_to_tokens(
{ #child }
)
})
} else if cfg!(feature = "__internal_erase_components") {
Some(quote! {
.child(
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
)
})
} else if children.len() > 16 {
// implementations of various traits used in routing and rendering are implemented for
// tuples of sizes 0, 1, 2, 3, ... N. N varies but is > 16. The traits are also implemented
@@ -479,10 +468,6 @@ fn fragment_to_tokens(
None
} else if children.len() == 1 {
children.into_iter().next()
} else if cfg!(feature = "__internal_erase_components") {
Some(quote! {
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
})
} else if children.len() > 16 {
// implementations of various traits used in routing and rendering are implemented for
// tuples of sizes 0, 1, 2, 3, ... N. N varies but is > 16. The traits are also implemented
@@ -767,18 +752,10 @@ pub(crate) fn element_to_tokens(
}
}
}
if cfg!(feature = "__internal_erase_components") {
Some(quote! {
vec![#(#attributes.into_any_attr(),)*]
#(.add_any_attr(#additions))*
})
} else {
Some(quote! {
(#(#attributes,)*)
#(.add_any_attr(#additions))*
})
}
Some(quote! {
(#(#attributes,)*)
#(.add_any_attr(#additions))*
})
} else {
let tag = name.to_string();
// collect close_tag name to emit semantic information for IDE.
@@ -1175,14 +1152,8 @@ pub(crate) fn two_way_binding_to_tokens(
let ident =
format_ident!("{}", name.to_case(UpperCamel), span = node.key.span());
if name == "group" {
quote! {
.bind(leptos::tachys::reactive_graph::bind::#ident, #value)
}
} else {
quote! {
.bind(::leptos::attr::#ident, #value)
}
quote! {
.bind(::leptos::attr::#ident, #value)
}
}
@@ -1203,7 +1174,8 @@ pub(crate) fn event_type_and_handler(
) -> (TokenStream, TokenStream, TokenStream) {
let handler = attribute_value(node, false);
let (event_type, is_custom, options) = parse_event_name(name);
let (event_type, is_custom, is_force_undelegated, is_targeted) =
parse_event_name(name);
let event_name_ident = match &node.key {
NodeName::Punctuated(parts) => {
@@ -1221,17 +1193,11 @@ pub(crate) fn event_type_and_handler(
}
_ => unreachable!(),
};
let capture_ident = match &node.key {
NodeName::Punctuated(parts) => {
parts.iter().find(|part| part.to_string() == "capture")
}
_ => unreachable!(),
};
let on = match &node.key {
NodeName::Punctuated(parts) => &parts[0],
_ => unreachable!(),
};
let on = if options.targeted {
let on = if is_targeted {
Ident::new("on_target", on.span()).to_token_stream()
} else {
on.to_token_stream()
@@ -1244,29 +1210,15 @@ pub(crate) fn event_type_and_handler(
event_type
};
let event_type = quote! {
::leptos::tachys::html::event::#event_type
};
let event_type = if options.captured {
let capture = if let Some(capture) = capture_ident {
quote! { #capture }
} else {
quote! { capture }
};
quote! { ::leptos::tachys::html::event::#capture(#event_type) }
} else {
event_type
};
let event_type = if options.undelegated {
let event_type = if is_force_undelegated {
let undelegated = if let Some(undelegated) = undelegated_ident {
quote! { #undelegated }
} else {
quote! { undelegated }
};
quote! { ::leptos::tachys::html::event::#undelegated(#event_type) }
quote! { ::leptos::tachys::html::event::#undelegated(::leptos::tachys::html::event::#event_type) }
} else {
event_type
quote! { ::leptos::tachys::html::event::#event_type }
};
(on, event_type, handler)
@@ -1472,22 +1424,13 @@ fn is_ambiguous_element(tag: &str) -> bool {
tag == "a" || tag == "script" || tag == "title"
}
fn parse_event(event_name: &str) -> (String, EventNameOptions) {
let undelegated = event_name.contains(":undelegated");
let targeted = event_name.contains(":target");
let captured = event_name.contains(":capture");
fn parse_event(event_name: &str) -> (String, bool, bool) {
let is_undelegated = event_name.contains(":undelegated");
let is_targeted = event_name.contains(":target");
let event_name = event_name
.replace(":undelegated", "")
.replace(":target", "")
.replace(":capture", "");
(
event_name,
EventNameOptions {
undelegated,
targeted,
captured,
},
)
.replace(":target", "");
(event_name, is_undelegated, is_targeted)
}
/// Escapes Rust keywords that are also HTML attribute names
@@ -1679,17 +1622,8 @@ const TYPED_EVENTS: [&str; 126] = [
const CUSTOM_EVENT: &str = "Custom";
#[derive(Debug)]
pub(crate) struct EventNameOptions {
undelegated: bool,
targeted: bool,
captured: bool,
}
pub(crate) fn parse_event_name(
name: &str,
) -> (TokenStream, bool, EventNameOptions) {
let (name, options) = parse_event(name);
pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool, bool) {
let (name, is_force_undelegated, is_targeted) = parse_event(name);
let (event_type, is_custom) = TYPED_EVENTS
.binary_search(&name.as_str())
@@ -1705,7 +1639,7 @@ pub(crate) fn parse_event_name(
} else {
event_type
};
(event_type, is_custom, options)
(event_type, is_custom, is_force_undelegated, is_targeted)
}
fn convert_to_snake_case(name: String) -> String {

View File

@@ -1,7 +1,4 @@
use super::{
component_builder::maybe_optimised_component_children,
convert_to_snake_case, ident_from_tag_name,
};
use super::{convert_to_snake_case, ident_from_tag_name};
use crate::view::{fragment_to_tokens, utils::filter_prefixed_attrs, TagType};
use proc_macro2::{Ident, TokenStream, TokenTree};
use quote::{quote, quote_spanned};
@@ -73,10 +70,7 @@ pub(crate) fn slot_to_tokens(
}
});
let items_to_bind = filter_prefixed_attrs(attrs.iter(), "let:")
.into_iter()
.map(|ident| quote! { #ident })
.collect::<Vec<_>>();
let items_to_bind = filter_prefixed_attrs(attrs.iter(), "let:");
let items_to_clone = filter_prefixed_attrs(attrs.iter(), "clone:");
@@ -89,7 +83,7 @@ pub(crate) fn slot_to_tokens(
let value = attr.value().map(|v| {
quote! { #v }
})?;
Some(quote! { (#name, #value) })
Some(quote! { (#name, ::leptos::IntoAttribute::into_attribute(#value)) })
})
.collect::<Vec<_>>();
@@ -102,12 +96,6 @@ pub(crate) fn slot_to_tokens(
let mut slots = HashMap::new();
let children = if node.children.is_empty() {
quote! {}
} else if let Some(children) = maybe_optimised_component_children(
&node.children,
&items_to_bind,
&items_to_clone,
) {
children
} else {
let children = fragment_to_tokens(
&mut node.children,

View File

@@ -104,18 +104,3 @@ fn component_nostrip() {
/>
};
}
#[component]
fn WithLifetime<'a>(data: &'a str) -> impl IntoView {
_ = data;
"static lifetime"
}
#[test]
fn returns_static_lifetime() {
#[allow(unused)]
fn can_return_impl_intoview_from_body() -> impl IntoView {
let val = String::from("non_static_lifetime");
WithLifetime(WithLifetimeProps::builder().data(&val).build())
}
}

View File

@@ -1,8 +1,9 @@
#[cfg(not(feature = "ssr"))]
pub mod tests {
use leptos::{
server,
server_fn::{codec, Http, ServerFn, ServerFnError},
server_fn::{codec, ServerFn, ServerFnError},
};
use std::any::TypeId;
@@ -18,8 +19,8 @@ pub mod tests {
"/api/my_server_action"
);
assert_eq!(
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
);
}
@@ -31,8 +32,8 @@ pub mod tests {
}
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::Cbor, codec::Cbor>>()
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::Cbor>()
);
}
@@ -44,8 +45,8 @@ pub mod tests {
}
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::Cbor, codec::Cbor>>()
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::Cbor>()
);
}
@@ -57,8 +58,8 @@ pub mod tests {
}
assert_eq!(<FooBar as ServerFn>::PATH, "/api/my_path");
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
);
}
@@ -73,8 +74,8 @@ pub mod tests {
"/api/my_server_action"
);
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
);
}
@@ -90,8 +91,8 @@ pub mod tests {
"/foo/bar/my_server_action"
);
assert_eq!(
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
);
}
@@ -107,8 +108,8 @@ pub mod tests {
"/api/my_server_action"
);
assert_eq!(
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::GetUrl, codec::Json>>()
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
TypeId::of::<codec::GetUrl>()
);
}
@@ -123,8 +124,8 @@ pub mod tests {
"/api/path/to/my/endpoint"
);
assert_eq!(
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
);
}
}

View File

@@ -1,4 +1,4 @@
#[cfg(not(feature = "__internal_erase_components"))]
#[cfg(not(erase_components))]
#[test]
fn ui() {
let t = trybuild::TestCases::new();

View File

@@ -1,8 +1,6 @@
[package]
name = "leptos_server"
# TODO revert to { workspace = true } before 0.8.0 release
# this is a hack because I missing bumping the hydration_context version number before publishing
version = "0.8.0-beta"
version = { workspace = true }
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -28,8 +26,8 @@ send_wrapper = "0.6"
# serialization formats
serde = { version = "1.0" }
js-sys = { version = "0.3.74", optional = true }
wasm-bindgen = { version = "0.2.100", optional = true }
serde_json = { workspace = true }
wasm-bindgen = { version = "0.2.97", optional = true }
serde_json = { version = "1.0" }
[features]
ssr = []
@@ -48,4 +46,4 @@ denylist = ["tracing"]
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -3,7 +3,7 @@ use reactive_graph::{
owner::use_context,
traits::DefinedAt,
};
use server_fn::{error::FromServerFnError, ServerFn};
use server_fn::{error::ServerFnErrorSerde, ServerFn, ServerFnError};
use std::{ops::Deref, panic::Location, sync::Arc};
/// An error that can be caused by a server action.
@@ -42,7 +42,7 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: ArcAction<S, Result<S::Output, S::Error>>,
inner: ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -52,14 +52,13 @@ where
S: ServerFn + Clone + Send + Sync + 'static,
S::Output: Send + Sync + 'static,
S::Error: Send + Sync + 'static,
S::Error: FromServerFnError,
{
/// Creates a new [`ArcAction`] that will call the server function `S` when dispatched.
#[track_caller]
pub fn new() -> Self {
let err = use_context::<ServerActionError>().and_then(|error| {
(error.path() == S::PATH)
.then(|| S::Error::de(error.err()))
.then(|| ServerFnError::<S::Error>::de(error.err()))
.map(Err)
});
Self {
@@ -77,7 +76,7 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
type Target = ArcAction<S, Result<S::Output, S::Error>>;
type Target = ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>;
fn deref(&self) -> &Self::Target {
&self.inner
@@ -132,7 +131,7 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: Action<S, Result<S::Output, S::Error>>,
inner: Action<S, Result<S::Output, ServerFnError<S::Error>>>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -147,7 +146,7 @@ where
pub fn new() -> Self {
let err = use_context::<ServerActionError>().and_then(|error| {
(error.path() == S::PATH)
.then(|| S::Error::de(error.err()))
.then(|| ServerFnError::<S::Error>::de(error.err()))
.map(Err)
});
Self {
@@ -183,14 +182,15 @@ where
S::Output: Send + Sync + 'static,
S::Error: Send + Sync + 'static,
{
type Target = Action<S, Result<S::Output, S::Error>>;
type Target = Action<S, Result<S::Output, ServerFnError<S::Error>>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<S> From<ServerAction<S>> for Action<S, Result<S::Output, S::Error>>
impl<S> From<ServerAction<S>>
for Action<S, Result<S::Output, ServerFnError<S::Error>>>
where
S: ServerFn + 'static,
S::Output: 'static,

View File

@@ -79,7 +79,7 @@ mod view_implementations {
use reactive_graph::traits::Read;
use std::future::Future;
use tachys::{
html::attribute::{any_attribute::AnyAttribute, Attribute},
html::attribute::Attribute,
hydration::Cursor,
reactive_graph::{RenderEffectState, Suspend, SuspendState},
ssr::StreamBuilder,
@@ -135,7 +135,6 @@ mod view_implementations {
Ser: Send + 'static,
{
type AsyncOutput = Option<T>;
type Owned = Self;
const MIN_LENGTH: usize = 0;
@@ -153,14 +152,12 @@ mod view_implementations {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
(move || Suspend::new(async move { self.await })).to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -170,7 +167,6 @@ mod view_implementations {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) where
Self: Sized,
{
@@ -180,7 +176,6 @@ mod view_implementations {
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -192,9 +187,5 @@ mod view_implementations {
(move || Suspend::new(async move { self.await }))
.hydrate::<FROM_SERVER>(cursor, position)
}
fn into_owned(self) -> Self::Owned {
self
}
}
}

View File

@@ -8,15 +8,13 @@ use reactive_graph::{
ToAnySource, ToAnySubscriber,
},
owner::use_context,
send_wrapper_ext::MaybeSendWrapperOption,
signal::{
guards::{AsyncPlain, Mapped, ReadGuard},
guards::{AsyncPlain, ReadGuard},
ArcRwSignal, RwSignal,
},
traits::{
DefinedAt, IsDisposed, ReadUntracked, Track, Update, With, Write,
},
traits::{DefinedAt, IsDisposed, ReadUntracked, Track, Update, Write},
};
use send_wrapper::SendWrapper;
use std::{
future::{pending, Future, IntoFuture},
panic::Location,
@@ -24,7 +22,7 @@ use std::{
/// A reference-counted resource that only loads its data locally on the client.
pub struct ArcLocalResource<T> {
data: ArcAsyncDerived<T>,
data: ArcAsyncDerived<SendWrapper<T>>,
refetch: ArcRwSignal<usize>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
@@ -71,12 +69,14 @@ impl<T> ArcLocalResource<T> {
}
}
};
let fetcher = SendWrapper::new(fetcher);
let refetch = ArcRwSignal::new(0);
let data = {
let refetch = refetch.clone();
ArcAsyncDerived::new_unsync(move || {
ArcAsyncDerived::new(move || {
refetch.track();
fetcher()
let fut = fetcher();
SendWrapper::new(async move { SendWrapper::new(fut.await) })
})
};
Self {
@@ -91,34 +91,6 @@ impl<T> ArcLocalResource<T> {
pub fn refetch(&self) {
*self.refetch.write() += 1;
}
/// Synchronously, reactively reads the current value of the resource and applies the function
/// `f` to its value if it is `Some(_)`.
#[track_caller]
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U>
where
T: 'static,
{
self.data.try_with(|n| n.as_ref().map(f))?
}
}
impl<T, E> ArcLocalResource<Result<T, E>>
where
T: 'static,
E: Clone + 'static,
{
/// Applies the given function when a resource that returns `Result<T, E>`
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
/// calls over the `Option<Result<_, _>>` returned by the resource.
///
/// This is useful when used with features like server functions, in conjunction
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
/// left to handle the `None` and `Err(_)` states.
#[track_caller]
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
}
}
impl<T> IntoFuture for ArcLocalResource<T>
@@ -126,9 +98,14 @@ where
T: Clone + 'static,
{
type Output = T;
type IntoFuture = AsyncDerivedFuture<T>;
type IntoFuture = futures::future::Map<
AsyncDerivedFuture<SendWrapper<T>>,
fn(SendWrapper<T>) -> T,
>;
fn into_future(self) -> Self::IntoFuture {
use futures::FutureExt;
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
notifier.notify();
} else if cfg!(feature = "ssr") {
@@ -138,7 +115,7 @@ where
always pending on the server."
);
}
self.data.into_future()
self.data.into_future().map(|value| (*value).clone())
}
}
@@ -159,14 +136,18 @@ impl<T> ReadUntracked for ArcLocalResource<T>
where
T: 'static,
{
type Value = ReadGuard<
Option<T>,
Mapped<AsyncPlain<MaybeSendWrapperOption<T>>, Option<T>>,
>;
type Value =
ReadGuard<Option<SendWrapper<T>>, AsyncPlain<Option<SendWrapper<T>>>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
notifier.notify();
} else if cfg!(feature = "ssr") {
panic!(
"Reading from a LocalResource outside Suspense in `ssr` mode \
will cause the response to hang, because LocalResources are \
always pending on the server."
);
}
self.data.try_read_untracked()
}
@@ -235,7 +216,7 @@ impl<T> Subscriber for ArcLocalResource<T> {
/// A resource that only loads its data locally on the client.
pub struct LocalResource<T> {
data: AsyncDerived<T>,
data: AsyncDerived<SendWrapper<T>>,
refetch: RwSignal<usize>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
@@ -285,9 +266,11 @@ impl<T> LocalResource<T> {
data: if cfg!(feature = "ssr") {
AsyncDerived::new_mock(fetcher)
} else {
AsyncDerived::new_unsync_threadsafe_storage(move || {
let fetcher = SendWrapper::new(fetcher);
AsyncDerived::new(move || {
refetch.track();
fetcher()
let fut = fetcher();
SendWrapper::new(async move { SendWrapper::new(fut.await) })
})
},
refetch,
@@ -307,9 +290,14 @@ where
T: Clone + 'static,
{
type Output = T;
type IntoFuture = AsyncDerivedFuture<T>;
type IntoFuture = futures::future::Map<
AsyncDerivedFuture<SendWrapper<T>>,
fn(SendWrapper<T>) -> T,
>;
fn into_future(self) -> Self::IntoFuture {
use futures::FutureExt;
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
notifier.notify();
} else if cfg!(feature = "ssr") {
@@ -319,7 +307,7 @@ where
always pending on the server."
);
}
self.data.into_future()
self.data.into_future().map(|value| (*value).clone())
}
}
@@ -340,14 +328,18 @@ impl<T> ReadUntracked for LocalResource<T>
where
T: 'static,
{
type Value = ReadGuard<
Option<T>,
Mapped<AsyncPlain<MaybeSendWrapperOption<T>>, Option<T>>,
>;
type Value =
ReadGuard<Option<SendWrapper<T>>, AsyncPlain<Option<SendWrapper<T>>>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
notifier.notify();
} else if cfg!(feature = "ssr") {
panic!(
"Reading from a LocalResource outside Suspense in `ssr` mode \
will cause the response to hang, because LocalResources are \
always pending on the server."
);
}
self.data.try_read_untracked()
}

View File

@@ -2,7 +2,7 @@ use reactive_graph::{
actions::{ArcMultiAction, MultiAction},
traits::DefinedAt,
};
use server_fn::ServerFn;
use server_fn::{ServerFn, ServerFnError};
use std::{ops::Deref, panic::Location};
/// An [`ArcMultiAction`] that can be used to call a server function.
@@ -11,7 +11,7 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: ArcMultiAction<S, Result<S::Output, S::Error>>,
inner: ArcMultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -40,7 +40,7 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
type Target = ArcMultiAction<S, Result<S::Output, S::Error>>;
type Target = ArcMultiAction<S, Result<S::Output, ServerFnError<S::Error>>>;
fn deref(&self) -> &Self::Target {
&self.inner
@@ -95,13 +95,13 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: MultiAction<S, Result<S::Output, S::Error>>,
inner: MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
impl<S> From<ServerMultiAction<S>>
for MultiAction<S, Result<S::Output, S::Error>>
for MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>
where
S: ServerFn + 'static,
S::Output: 'static,
@@ -152,7 +152,7 @@ where
S::Output: 'static,
S::Error: 'static,
{
type Target = MultiAction<S, Result<S::Output, S::Error>>;
type Target = MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>;
fn deref(&self) -> &Self::Target {
&self.inner

View File

@@ -168,41 +168,6 @@ where
data
}
/// Synchronously, reactively reads the current value of the resource and applies the function
/// `f` to its value if it is `Some(_)`.
#[track_caller]
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U>
where
T: Send + Sync + 'static,
{
self.try_with(|n| n.as_ref().map(f))?
}
}
impl<T, E, Ser> ArcOnceResource<Result<T, E>, Ser>
where
Ser: Encoder<Result<T, E>> + Decoder<Result<T, E>>,
<Ser as Encoder<Result<T, E>>>::Error: Debug,
<Ser as Decoder<Result<T, E>>>::Error: Debug,
<<Ser as Decoder<Result<T, E>>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<Ser as Encoder<Result<T, E>>>::Encoded: IntoEncodedString,
<Ser as Decoder<Result<T, E>>>::Encoded: FromEncodedStr,
T: Send + Sync + 'static,
E: Send + Sync + Clone + 'static,
{
/// Applies the given function when a resource that returns `Result<T, E>`
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
/// calls over the `Option<Result<_, _>>` returned by the resource.
///
/// This is useful when used with features like server functions, in conjunction
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
/// left to handle the `None` and `Err(_)` states.
#[track_caller]
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
}
}
impl<T, Ser> ArcOnceResource<T, Ser> {
@@ -569,37 +534,6 @@ where
defined_at,
}
}
/// Synchronously, reactively reads the current value of the resource and applies the function
/// `f` to its value if it is `Some(_)`.
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
self.try_with(|n| n.as_ref().map(|n| Some(f(n))))?.flatten()
}
}
impl<T, E, Ser> OnceResource<Result<T, E>, Ser>
where
Ser: Encoder<Result<T, E>> + Decoder<Result<T, E>>,
<Ser as Encoder<Result<T, E>>>::Error: Debug,
<Ser as Decoder<Result<T, E>>>::Error: Debug,
<<Ser as Decoder<Result<T, E>>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<Ser as Encoder<Result<T, E>>>::Encoded: IntoEncodedString,
<Ser as Decoder<Result<T, E>>>::Encoded: FromEncodedStr,
T: Send + Sync + 'static,
E: Send + Sync + Clone + 'static,
{
/// Applies the given function when a resource that returns `Result<T, E>`
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
/// calls over the `Option<Result<_, _>>` returned by the resource.
///
/// This is useful when used with features like server functions, in conjunction
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
/// left to handle the `None` and `Err(_)` states.
#[track_caller]
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
}
}
impl<T, Ser> OnceResource<T, Ser>

View File

@@ -215,11 +215,16 @@ where
None
}
Ok(encoded) => {
let decoded = Ser::decode(encoded.borrow());
#[cfg(feature = "tracing")]
let decoded = decoded
.inspect_err(|e| tracing::error!("{e:?}"));
decoded.ok()
match Ser::decode(encoded.borrow()) {
#[allow(unused_variables)]
// used in tracing
Err(e) => {
#[cfg(feature = "tracing")]
tracing::error!("{e:?}");
None
}
Ok(value) => Some(value),
}
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.8.0-beta"
version = "0.7.5"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -15,7 +15,7 @@ or_poisoned = { workspace = true }
indexmap = "2.6"
send_wrapper = "0.6.0"
tracing = { version = "0.1.41", optional = true }
wasm-bindgen = { workspace = true }
wasm-bindgen = "0.2.97"
futures = "0.3.31"
[dependencies.web-sys]

View File

@@ -1,6 +1,6 @@
use crate::ServerMetaContext;
use leptos::{
attr::{any_attribute::AnyAttribute, NextAttribute},
attr::NextAttribute,
component, html,
reactive::owner::use_context,
tachys::{
@@ -103,7 +103,6 @@ where
At: Attribute,
{
type AsyncOutput = BodyView<At::AsyncOutput>;
type Owned = BodyView<At::CloneableOwned>;
const MIN_LENGTH: usize = At::MIN_LENGTH;
@@ -123,14 +122,10 @@ where
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
if let Some(meta) = use_context::<ServerMetaContext>() {
let mut buf = String::new();
_ = html::attributes_to_html(
(self.attributes, extra_attrs),
&mut buf,
);
_ = html::attributes_to_html(self.attributes, &mut buf);
if !buf.is_empty() {
_ = meta.body.send(buf);
}
@@ -147,12 +142,6 @@ where
BodyViewState { attributes }
}
fn into_owned(self) -> Self::Owned {
BodyView {
attributes: self.attributes.into_cloneable_owned(),
}
}
}
impl<At> Mountable for BodyViewState<At>
@@ -171,11 +160,4 @@ where
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
false
}
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
vec![document()
.body()
.expect("there to be a <body> element")
.into()]
}
}

View File

@@ -1,6 +1,6 @@
use crate::ServerMetaContext;
use leptos::{
attr::{any_attribute::AnyAttribute, NextAttribute},
attr::NextAttribute,
component, html,
reactive::owner::use_context,
tachys::{
@@ -103,7 +103,6 @@ where
At: Attribute,
{
type AsyncOutput = HtmlView<At::AsyncOutput>;
type Owned = HtmlView<At::CloneableOwned>;
const MIN_LENGTH: usize = At::MIN_LENGTH;
@@ -123,14 +122,10 @@ where
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
if let Some(meta) = use_context::<ServerMetaContext>() {
let mut buf = String::new();
_ = html::attributes_to_html(
(self.attributes, extra_attrs),
&mut buf,
);
_ = html::attributes_to_html(self.attributes, &mut buf);
if !buf.is_empty() {
_ = meta.html.send(buf);
}
@@ -150,12 +145,6 @@ where
HtmlViewState { attributes }
}
fn into_owned(self) -> Self::Owned {
HtmlView {
attributes: self.attributes.into_cloneable_owned(),
}
}
}
impl<At> Mountable for HtmlViewState<At>
@@ -176,10 +165,4 @@ where
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
false
}
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
vec![document()
.document_element()
.expect("there to be a <html> element")]
}
}

View File

@@ -44,7 +44,7 @@
use futures::{Stream, StreamExt};
use leptos::{
attr::{any_attribute::AnyAttribute, NextAttribute},
attr::NextAttribute,
component,
logging::debug_warn,
oco::Oco,
@@ -323,13 +323,37 @@ pub(crate) fn register<E, At, Ch>(
where
HtmlElement<E, At, Ch>: RenderHtml,
{
#[allow(unused_mut)] // used for `ssr`
let mut el = Some(el);
#[cfg(feature = "ssr")]
if let Some(cx) = use_context::<ServerMetaContext>() {
let mut buf = String::new();
el.take().unwrap().to_html_with_buf(
&mut buf,
&mut Position::NextChild,
false,
false,
);
_ = cx.elements.send(buf); // fails only if the receiver is already dropped
} else {
let msg = "tried to use a leptos_meta component without \
`ServerMetaContext` provided";
#[cfg(feature = "tracing")]
tracing::warn!("{}", msg);
#[cfg(not(feature = "tracing"))]
eprintln!("{}", msg);
}
RegisteredMetaTag { el }
}
struct RegisteredMetaTag<E, At, Ch> {
// this is `None` if we've already taken it out to render to HTML on the server
// we don't render it in place in RenderHtml, so it's fine
el: HtmlElement<E, At, Ch>,
el: Option<HtmlElement<E, At, Ch>>,
}
struct RegisteredMetaTagState<E, At, Ch>
@@ -367,12 +391,12 @@ where
type State = RegisteredMetaTagState<E, At, Ch>;
fn build(self) -> Self::State {
let state = self.el.build();
let state = self.el.unwrap().build();
RegisteredMetaTagState { state }
}
fn rebuild(self, state: &mut Self::State) {
self.el.rebuild(&mut state.state);
self.el.unwrap().rebuild(&mut state.state);
}
}
@@ -393,7 +417,7 @@ where
Self::Output<NewAttr>: RenderHtml,
{
RegisteredMetaTag {
el: self.el.add_any_attr(attr),
el: self.el.map(|inner| inner.add_any_attr(attr)),
}
}
}
@@ -405,7 +429,6 @@ where
Ch: RenderHtml + Send,
{
type AsyncOutput = Self;
type Owned = RegisteredMetaTag<E, At::CloneableOwned, Ch::Owned>;
const MIN_LENGTH: usize = 0;
@@ -423,31 +446,9 @@ where
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
_extra_attrs: Vec<AnyAttribute>,
) {
// meta tags are rendered into the buffer stored into the context
// the value has already been taken out, when we're on the server
#[cfg(feature = "ssr")]
if let Some(cx) = use_context::<ServerMetaContext>() {
let mut buf = String::new();
self.el.to_html_with_buf(
&mut buf,
&mut Position::NextChild,
false,
false,
vec![],
);
_ = cx.elements.send(buf); // fails only if the receiver is already dropped
} else {
let msg = "tried to use a leptos_meta component without \
`ServerMetaContext` provided";
#[cfg(feature = "tracing")]
tracing::warn!("{}", msg);
#[cfg(not(feature = "tracing"))]
eprintln!("{}", msg);
}
}
fn hydrate<const FROM_SERVER: bool>(
@@ -461,18 +462,12 @@ where
MetaContext provided",
)
.cursor;
let state = self.el.hydrate::<FROM_SERVER>(
let state = self.el.unwrap().hydrate::<FROM_SERVER>(
&cursor,
&PositionState::new(Position::NextChild),
);
RegisteredMetaTagState { state }
}
fn into_owned(self) -> Self::Owned {
RegisteredMetaTag {
el: self.el.into_owned(),
}
}
}
impl<E, At, Ch> Mountable for RegisteredMetaTagState<E, At, Ch>
@@ -505,10 +500,6 @@ where
// we intended!
false
}
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
self.state.elements()
}
}
/// During server rendering, inserts the meta tags that have been generated by the other components
@@ -550,7 +541,6 @@ impl AddAnyAttr for MetaTagsView {
impl RenderHtml for MetaTagsView {
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = 0;
@@ -566,7 +556,6 @@ impl RenderHtml for MetaTagsView {
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
_extra_attrs: Vec<AnyAttribute>,
) {
buf.push_str("<!--HEAD-->");
}
@@ -577,10 +566,6 @@ impl RenderHtml for MetaTagsView {
_position: &PositionState,
) -> Self::State {
}
fn into_owned(self) -> Self::Owned {
self
}
}
pub(crate) trait OrDefaultNonce {

View File

@@ -36,10 +36,6 @@ pub fn Stylesheet(
}
/// Injects an [`HTMLLinkElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document head that loads a `cargo-leptos`-hashed stylesheet.
///
/// This should only be used in the applications server-side `shell` function, as
/// [`LeptosOptions`] is not available in the browser. Unlike other `leptos_meta` components, it
/// will render the `<link>` it creates exactly where it is called.
#[component]
pub fn HashedStylesheet(
/// Leptos options
@@ -78,9 +74,11 @@ pub fn HashedStylesheet(
css_file_name.push_str(".css");
let pkg_path = &options.site_pkg_dir;
let root = root.unwrap_or_default();
link()
.id(id)
.rel("stylesheet")
.href(format!("{root}/{pkg_path}/{css_file_name}"))
// TODO additional attributes
register(
link()
.id(id)
.rel("stylesheet")
.href(format!("{root}/{pkg_path}/{css_file_name}")),
)
}

View File

@@ -1,6 +1,6 @@
use crate::{use_head, MetaContext, ServerMetaContext};
use leptos::{
attr::{any_attribute::AnyAttribute, Attribute},
attr::Attribute,
component,
oco::Oco,
reactive::{
@@ -234,7 +234,6 @@ impl AddAnyAttr for TitleView {
impl RenderHtml for TitleView {
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = 0;
@@ -250,7 +249,6 @@ impl RenderHtml for TitleView {
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
_extra_attrs: Vec<AnyAttribute>,
) {
// meta tags are rendered into the buffer stored into the context
// the value has already been taken out, when we're on the server
@@ -284,10 +282,6 @@ impl RenderHtml for TitleView {
});
TitleViewState { effect }
}
fn into_owned(self) -> Self::Owned {
self
}
}
impl Mountable for TitleViewState {
@@ -305,8 +299,4 @@ impl Mountable for TitleViewState {
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
false
}
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
vec![]
}
}

View File

@@ -13,4 +13,4 @@ serde = "1.0"
thiserror = "2.0"
[dev-dependencies]
serde_json = { workspace = true }
serde_json = "1.0"

View File

@@ -35,7 +35,7 @@ pub trait OrPoisoned {
fn or_poisoned(self) -> Self::Inner;
}
impl<'a, T: ?Sized> OrPoisoned
impl<'a, T> OrPoisoned
for Result<RwLockReadGuard<'a, T>, PoisonError<RwLockReadGuard<'a, T>>>
{
type Inner = RwLockReadGuard<'a, T>;
@@ -45,7 +45,7 @@ impl<'a, T: ?Sized> OrPoisoned
}
}
impl<'a, T: ?Sized> OrPoisoned
impl<'a, T> OrPoisoned
for Result<RwLockWriteGuard<'a, T>, PoisonError<RwLockWriteGuard<'a, T>>>
{
type Inner = RwLockWriteGuard<'a, T>;
@@ -55,7 +55,7 @@ impl<'a, T: ?Sized> OrPoisoned
}
}
impl<'a, T: ?Sized> OrPoisoned for LockResult<MutexGuard<'a, T>> {
impl<'a, T> OrPoisoned for LockResult<MutexGuard<'a, T>> {
type Inner = MutexGuard<'a, T>;
fn or_poisoned(self) -> Self::Inner {

View File

@@ -8,19 +8,19 @@ codegen-units = 1
lto = true
[dependencies]
leptos = { version = "0.7.7", features = ["csr"] }
leptos_meta = { version = "0.7.7" }
leptos_router = { version = "0.7.7" }
leptos = { version = "0.6.13", features = ["csr"] }
leptos_meta = { version = "0.6.13", features = ["csr"] }
leptos_router = { version = "0.6.13", features = ["csr"] }
console_log = "1.0"
log = "0.4.22"
console_error_panic_hook = "0.1.7"
bevy = "0.15.2"
bevy = "0.14.1"
crossbeam-channel = "0.5.13"
[dev-dependencies]
wasm-bindgen = "0.2.100"
wasm-bindgen-test = "0.3.50"
web-sys = "0.3.77"
wasm-bindgen = "0.2.92"
wasm-bindgen-test = "0.3.42"
web-sys = "0.3.69"
[workspace]
# The empty workspace here is to keep rust-analyzer satisfied

View File

@@ -17,7 +17,7 @@ impl DuplexEventsPlugin {
let (bevy_sender, client_receiver) = crossbeam_channel::bounded(50);
// For sending message from the client to bevy
let (client_sender, bevy_receiver) = crossbeam_channel::bounded(50);
DuplexEventsPlugin {
let instance = DuplexEventsPlugin {
client_processor: EventProcessor {
sender: client_sender,
receiver: client_receiver,
@@ -26,7 +26,8 @@ impl DuplexEventsPlugin {
sender: bevy_sender,
receiver: bevy_receiver,
},
}
};
instance
}
/// Get the client event processor

View File

@@ -23,13 +23,14 @@ impl Scene {
/// Create a new instance
pub fn new(canvas_id: String) -> Scene {
let plugin = DuplexEventsPlugin::new();
Scene {
let instance = Scene {
is_setup: false,
canvas_id,
canvas_id: canvas_id,
evt_plugin: plugin.clone(),
shared_state: SharedState::new(),
processor: plugin.get_processor(),
}
};
instance
}
/// Get the shared state
@@ -46,7 +47,7 @@ impl Scene {
/// Setup and attach the bevy instance to the html canvas element
pub fn setup(&mut self) {
if self.is_setup {
if self.is_setup == true {
return;
};
App::new()
@@ -75,37 +76,40 @@ fn setup_scene(
) {
let name = resource.0.lock().unwrap().name.clone();
// circular base
commands.spawn((
Mesh3d(meshes.add(Circle::new(4.0))),
MeshMaterial3d(materials.add(Color::WHITE)),
Transform::from_rotation(Quat::from_rotation_x(
commands.spawn(PbrBundle {
mesh: meshes.add(Circle::new(4.0)),
material: materials.add(Color::WHITE),
transform: Transform::from_rotation(Quat::from_rotation_x(
-std::f32::consts::FRAC_PI_2,
)),
));
..default()
});
// cube
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
Transform::from_xyz(0.0, 0.5, 0.0),
PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
material: materials.add(Color::rgb_u8(124, 144, 255)),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default()
},
Cube,
));
// light
commands.spawn((
PointLight {
commands.spawn(PointLightBundle {
point_light: PointLight {
shadows_enabled: true,
..default()
},
Transform::from_xyz(4.0, 8.0, 4.0),
));
transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default()
});
// camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
));
commands.spawn((Text::new(name), TextFont::default()));
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-2.5, 4.5, 9.0)
.looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
commands.spawn(TextBundle::from_section(name, TextStyle::default()));
}
/// Move the Cube on event

View File

@@ -1,6 +1,6 @@
mod demos;
mod routes;
use leptos::prelude::*;
use leptos::*;
use routes::RootPage;
pub fn main() {

View File

@@ -2,7 +2,7 @@ use crate::demos::bevydemo1::eventqueue::events::{
ClientInEvents, CounterEvtData,
};
use crate::demos::bevydemo1::scene::Scene;
use leptos::prelude::*;
use leptos::*;
/// 3d view component
#[component]
@@ -10,18 +10,18 @@ pub fn Demo1() -> impl IntoView {
// Setup a Counter
let initial_value: i32 = 0;
let step: i32 = 1;
let (value, set_value) = signal(initial_value);
let (value, set_value) = create_signal(initial_value);
// Setup a bevy 3d scene
let scene = Scene::new("#bevy".to_string());
let sender = scene.get_processor().sender;
let (sender_sig, _set_sender_sig) = signal(sender);
let (scene_sig, _set_scene_sig) = signal(scene);
let (sender_sig, _set_sender_sig) = create_signal(sender);
let (scene_sig, _set_scene_sig) = create_signal(scene);
// We need to add the 3D view onto the canvas post render.
Effect::new(move |_| {
create_effect(move |_| {
request_animation_frame(move || {
scene_sig.get_untracked().setup();
scene_sig.get().setup();
});
});

View File

@@ -1,11 +1,9 @@
pub mod demo1;
use demo1::Demo1;
use leptos::prelude::*;
use leptos_meta::Meta;
use leptos_meta::Title;
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet};
use leptos_router::components::*;
use leptos_router::StaticSegment;
use leptos::*;
use leptos_meta::{provide_meta_context, Meta, Stylesheet, Title};
use leptos_router::*;
#[component]
pub fn RootPage() -> impl IntoView {
provide_meta_context();
@@ -15,12 +13,11 @@ pub fn RootPage() -> impl IntoView {
<Meta name="description" content="Leptonic CSR template"/>
<Meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<Meta name="theme-color" content="#e66956"/>
<Title text="Leptos Bevy3D Example"/>
<Stylesheet href="https://fonts.googleapis.com/css?family=Roboto&display=swap"/>
<MetaTags/>
<Title text="Leptos Bevy3D Example"/>
<Router>
<Routes fallback=move || "Not found.">
<Route path=StaticSegment("") view=Demo1 />
<Routes>
<Route path="" view=|| view! { <Demo1/> }/>
</Routes>
</Router>
}

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