mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
39 Commits
gbj-patch-
...
fix-issues
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dbe8b2d4b | ||
|
|
fe64f0d332 | ||
|
|
65b7603192 | ||
|
|
d4bdc36062 | ||
|
|
1b55227d10 | ||
|
|
a903e19eb2 | ||
|
|
38bf73947f | ||
|
|
e4b89ba243 | ||
|
|
701e3077fb | ||
|
|
aec4d680aa | ||
|
|
06721c5fcd | ||
|
|
1ddb39e9bd | ||
|
|
15d4ca0638 | ||
|
|
85c3755f6d | ||
|
|
66ea072bc0 | ||
|
|
b0b3c21285 | ||
|
|
56088a9ead | ||
|
|
69d25d9c63 | ||
|
|
5029b8f315 | ||
|
|
0cba7bc22b | ||
|
|
fb97c50886 | ||
|
|
f1bc734dcf | ||
|
|
f71b4aae69 | ||
|
|
a834c03974 | ||
|
|
595013579c | ||
|
|
8b1bd1ae9e | ||
|
|
6ef1531059 | ||
|
|
9f1406250e | ||
|
|
1f6a892291 | ||
|
|
0ff1e279a2 | ||
|
|
c6096cc2a0 | ||
|
|
8a2ae7fc7c | ||
|
|
9de34b74cf | ||
|
|
1b5961edaa | ||
|
|
26d1aee9ad | ||
|
|
2bf09384df | ||
|
|
ac12e1a411 | ||
|
|
b367b68a43 | ||
|
|
1f9dad421f |
2
.github/workflows/ci-changed-examples.yml
vendored
2
.github/workflows/ci-changed-examples.yml
vendored
@@ -29,4 +29,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
toolchain: nightly-2024-01-29
|
||||
|
||||
2
.github/workflows/ci-examples.yml
vendored
2
.github/workflows/ci-examples.yml
vendored
@@ -24,4 +24,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
toolchain: nightly-2024-01-29
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -40,4 +40,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
toolchain: nightly-2024-01-29
|
||||
|
||||
26
Cargo.toml
26
Cargo.toml
@@ -25,22 +25,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.6.0-rc1"
|
||||
version = "0.6.5"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.6.0-rc1" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.6.0-rc1" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.6.0-rc1" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.6.0-rc1" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.6.0-rc1" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.6.0-rc1" }
|
||||
server_fn = { path = "./server_fn", version = "0.6.0-rc1" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.6.0-rc1" }
|
||||
leptos = { path = "./leptos", version = "0.6.5" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.6.5" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.6.5" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.6.5" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.6.5" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.6.5" }
|
||||
server_fn = { path = "./server_fn", version = "0.6.5" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.6.5" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.6" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.6.0-rc1" }
|
||||
leptos_router = { path = "./router", version = "0.6.0-rc1" }
|
||||
leptos_meta = { path = "./meta", version = "0.6.0-rc1" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.6.0-rc1" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.6.5" }
|
||||
leptos_router = { path = "./router", version = "0.6.5" }
|
||||
leptos_meta = { path = "./meta", version = "0.6.5" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.6.5" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
44
README.md
44
README.md
@@ -40,6 +40,25 @@ pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
// we also support a builder syntax rather than the JSX-like `view` macro
|
||||
#[component]
|
||||
pub fn SimpleCounterWithBuilder(initial_value: i32) -> impl IntoView {
|
||||
use leptos::html::*;
|
||||
|
||||
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);
|
||||
|
||||
// the `view` macro above expands to this builder syntax
|
||||
div().child((
|
||||
button().on(ev::click, clear).child("Clear"),
|
||||
button().on(ev::click, decrement).child("-1"),
|
||||
span().child(("Value: ", value, "!")),
|
||||
button().on(ev::click, increment).child("+1")
|
||||
))
|
||||
}
|
||||
|
||||
// Easy to use with Trunk (trunkrs.dev) or with a simple wasm-bindgen setup
|
||||
pub fn main() {
|
||||
mount_to_body(|| view! {
|
||||
@@ -131,7 +150,7 @@ There are several people in the community using Leptos right now for internal ap
|
||||
|
||||
### Can I use this for native GUI?
|
||||
|
||||
Sure! Obviously the `view` macro is for generating DOM nodes but you can use the reactive system to drive native any GUI toolkit that uses the same kind of object-oriented, event-callback-based framework as the DOM pretty easily. The principles are the same:
|
||||
Sure! Obviously the `view` macro is for generating DOM nodes but you can use the reactive system to drive any native GUI toolkit that uses the same kind of object-oriented, event-callback-based framework as the DOM pretty easily. The principles are the same:
|
||||
|
||||
- Use signals, derived signals, and memos to create your reactive system
|
||||
- Create GUI widgets
|
||||
@@ -141,3 +160,26 @@ Sure! Obviously the `view` macro is for generating DOM nodes but you can use the
|
||||
I've put together a [very simple GTK example](https://github.com/leptos-rs/leptos/blob/main/examples/gtk/src/main.rs) so you can see what I mean.
|
||||
|
||||
The new rendering approach being developed for 0.7 supports “universal rendering,” i.e., it can use any rendering library that supports a small set of 6-8 functions. (This is intended as a layer over typical retained-mode, OOP-style GUI toolkits like the DOM, GTK, etc.) That future rendering work will allow creating native UI in a way that is much more similar to the declarative approach used by the web framework.
|
||||
|
||||
### How is this different from Yew?
|
||||
|
||||
Yew is the most-used library for Rust web UI development, but there are several differences between Yew and Leptos, in philosophy, approach, and performance.
|
||||
|
||||
- **VDOM vs. fine-grained:** Yew is built on the virtual DOM (VDOM) model: state changes cause components to re-render, generating a new virtual DOM tree. Yew diffs this against the previous VDOM, and applies those patches to the actual DOM. Component functions rerun whenever state changes. Leptos takes an entirely different approach. Components run once, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
|
||||
- **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is.
|
||||
- **Server integration:** Yew was created in an era in which browser-rendered single-page apps (SPAs) were the dominant paradigm. While Leptos supports client-side rendering, it also focuses on integrating with the server side of your application via server functions and multiple modes of serving HTML, including out-of-order streaming.
|
||||
|
||||
- ### How is this different from Dioxus?
|
||||
|
||||
Like Leptos, Dioxus is a framework for building UIs using web technologies. However, there are significant differences in approach and features.
|
||||
|
||||
- **VDOM vs. fine-grained:** While Dioxus has a performant virtual DOM (VDOM), it still uses coarse-grained/component-scoped reactivity: changing a stateful value reruns the component function and diffs the old UI against the new one. Leptos components use a different mental model, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
|
||||
- **Web vs. desktop priorities:** Dioxus uses Leptos server functions in its fullstack mode, but does not have the same `<Suspense>`-based support for things like streaming HTML rendering, or share the same focus on holistic web performance. Leptos tends to prioritize holistic web performance (streaming HTML rendering, smaller WASM binary sizes, etc.), whereas Dioxus has an unparalleled experience when building desktop apps, because your application logic runs as a native Rust binary.
|
||||
|
||||
- ### How is this different from Sycamore?
|
||||
|
||||
Sycamore and Leptos are both heavily influenced by SolidJS. At this point, Leptos has a larger community and ecosystem and is more actively developed. Other differences:
|
||||
|
||||
- **Templating DSLs:** Sycamore uses a custom templating language for its views, while Leptos uses a JSX-like template format.
|
||||
- **`'static` signals:** One of Leptos’s main innovations was the creation of `Copy + 'static` signals, which have excellent ergonomics. Sycamore is in the process of adopting the same pattern, but this is not yet released.
|
||||
- **Perseus vs. server functions:** The Perseus metaframework provides an opinionated way to build Sycamore apps that include server functionality. Leptos instead provides primitives like server functions in the core of the framework.
|
||||
|
||||
@@ -3,5 +3,5 @@ alias = "check-all"
|
||||
|
||||
[tasks.check-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
args = ["+nightly-2024-01-29", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -3,5 +3,5 @@ alias = "test-all"
|
||||
|
||||
[tasks.test-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "test-all-features"]
|
||||
args = ["+nightly-2024-01-29", "test-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -15,13 +15,13 @@ clear = true
|
||||
dependencies = ["check-debug", "check-release"]
|
||||
|
||||
[tasks.check-debug]
|
||||
toolchain = "nightly"
|
||||
toolchain = "nightly-2024-01-29"
|
||||
command = "cargo"
|
||||
args = ["check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-release]
|
||||
toolchain = "nightly"
|
||||
toolchain = "nightly-2024-01-29"
|
||||
command = "cargo"
|
||||
args = ["check-all-features", "--release"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
[tasks.build]
|
||||
toolchain = "nightly"
|
||||
toolchain = "nightly-2024-01-29"
|
||||
command = "cargo"
|
||||
args = ["build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
toolchain = "nightly"
|
||||
toolchain = "nightly-2024-01-29"
|
||||
command = "cargo"
|
||||
args = ["check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[tasks.pre-clippy]
|
||||
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
|
||||
env = { CARGO_MAKE_CLIPPY_ARGS = "--no-deps --all-targets --all-features -- -D warnings" }
|
||||
|
||||
[tasks.check-style]
|
||||
dependencies = ["check-format-flow", "clippy-flow"]
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -69,7 +69,7 @@ reload-port = 3001
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
|
||||
@@ -132,15 +132,6 @@ pub fn Counter() -> impl IntoView {
|
||||
|_| get_server_count(),
|
||||
);
|
||||
|
||||
let value =
|
||||
move || counter.get().map(|count| count.unwrap_or(0)).unwrap_or(0);
|
||||
let error_msg = move || {
|
||||
counter.get().and_then(|res| match res {
|
||||
Ok(_) => None,
|
||||
Err(e) => Some(e),
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<h2>"Simple Counter"</h2>
|
||||
@@ -150,15 +141,24 @@ pub fn Counter() -> impl IntoView {
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch(())>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<span>
|
||||
"Value: "
|
||||
<Suspense>
|
||||
{move || counter.and_then(|count| *count)} "!"
|
||||
</Suspense>
|
||||
</span>
|
||||
<button on:click=move |_| inc.dispatch(())>"+1"</button>
|
||||
</div>
|
||||
{move || {
|
||||
error_msg()
|
||||
.map(|msg| {
|
||||
view! { <p>"Error: " {msg.to_string()}</p> }
|
||||
})
|
||||
}}
|
||||
<Suspense>
|
||||
{move || {
|
||||
counter.get().and_then(|res| match res {
|
||||
Ok(_) => None,
|
||||
Err(e) => Some(e),
|
||||
}).map(|msg| {
|
||||
view! { <p>"Error: " {msg.to_string()}</p> }
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -204,7 +204,7 @@ pub fn FormCounter() -> impl IntoView {
|
||||
<input type="hidden" name="msg" value="form value down"/>
|
||||
<input type="submit" value="-1"/>
|
||||
</ActionForm>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<span>"Value: " <Suspense>{move || value().to_string()} "!"</Suspense></span>
|
||||
<ActionForm action=adjust>
|
||||
<input type="hidden" name="delta" value="1"/>
|
||||
<input type="hidden" name="msg" value="form value up"/>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use leptos::{ev::click, html::AnyElement, *};
|
||||
|
||||
// no extra parameter
|
||||
pub fn highlight(el: HtmlElement<AnyElement>) {
|
||||
let mut highlighted = false;
|
||||
|
||||
@@ -14,6 +15,7 @@ pub fn highlight(el: HtmlElement<AnyElement>) {
|
||||
});
|
||||
}
|
||||
|
||||
// one extra parameter
|
||||
pub fn copy_to_clipboard(el: HtmlElement<AnyElement>, content: &str) {
|
||||
let content = content.to_string();
|
||||
|
||||
@@ -31,6 +33,35 @@ pub fn copy_to_clipboard(el: HtmlElement<AnyElement>, content: &str) {
|
||||
});
|
||||
}
|
||||
|
||||
// custom parameter
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Amount(usize);
|
||||
|
||||
impl From<usize> for Amount {
|
||||
fn from(value: usize) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
// a 'default' value if no value is passed in
|
||||
impl From<()> for Amount {
|
||||
fn from(_: ()) -> Self {
|
||||
Self(1)
|
||||
}
|
||||
}
|
||||
|
||||
// .into() will automatically be called on the parameter
|
||||
pub fn add_dot(el: HtmlElement<AnyElement>, amount: Amount) {
|
||||
_ = el.clone().on(click, move |_| {
|
||||
el.set_inner_text(&format!(
|
||||
"{}{}",
|
||||
el.inner_text(),
|
||||
".".repeat(amount.0)
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SomeComponent() -> impl IntoView {
|
||||
view! {
|
||||
@@ -46,6 +77,11 @@ pub fn App() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<a href="#" use:copy_to_clipboard=data>"Copy \"" {data} "\" to clipboard"</a>
|
||||
// automatically applies the directive to every root element in `SomeComponent`
|
||||
<SomeComponent use:highlight />
|
||||
// no value will default to `().into()`
|
||||
<button use:add_dot>"Add a dot"</button>
|
||||
// `5.into()` automatically called
|
||||
<button use:add_dot=5>"Add 5 dots"</button>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ See the [Examples README](../README.md) for setup and run instructions.
|
||||
|
||||
## Testing
|
||||
|
||||
This project is configured to run start and stop of processes for integration tests wihtout the use of Cargo Leptos or Node.
|
||||
This project is configured to run start and stop of processes for integration tests without the use of Cargo Leptos or Node.
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -61,7 +61,7 @@ reload-port = 3001
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -70,7 +70,7 @@ reload-port = 3001
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -62,16 +62,18 @@ pub fn Stories() -> impl IntoView {
|
||||
}}
|
||||
</span>
|
||||
<span>"page " {page}</span>
|
||||
<span class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
>
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
<Suspense>
|
||||
<span class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
>
|
||||
"more >"
|
||||
</a>
|
||||
</span>
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
>
|
||||
"more >"
|
||||
</a>
|
||||
</span>
|
||||
</Suspense>
|
||||
</div>
|
||||
<main class="news-list">
|
||||
<div>
|
||||
|
||||
@@ -71,7 +71,7 @@ reload-port = 3001
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -81,7 +81,7 @@ reload-port = 3001
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -78,7 +78,7 @@ reload-port = 3001
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
|
||||
@@ -5,13 +5,13 @@ extend = [
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
toolchain = "nightly"
|
||||
toolchain = "nightly-2024-01-29"
|
||||
command = "cargo"
|
||||
args = ["build-all-features", "--target", "wasm32-unknown-unknown"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
toolchain = "nightly"
|
||||
toolchain = "nightly-2024-01-29"
|
||||
command = "cargo"
|
||||
args = ["check-all-features", "--target", "wasm32-unknown-unknown"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -15,7 +15,7 @@ leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite", "rkyv", "multipart" ]}
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite", "rkyv", "multipart"] }
|
||||
log = "0.4"
|
||||
simple_logger = "4.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -31,6 +31,9 @@ web-sys = { version = "0.3.67", features = ["FileList", "File"] }
|
||||
strum = { version = "0.25.0", features = ["strum_macros", "derive"] }
|
||||
notify = { version = "6.1.1", optional = true }
|
||||
pin-project-lite = "0.2.13"
|
||||
dashmap = { version = "5.5.3", optional = true }
|
||||
once_cell = { version = "1.19.0", optional = true }
|
||||
async-broadcast = { version = "0.6.0", optional = true }
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
@@ -43,7 +46,10 @@ ssr = [
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_axum",
|
||||
"dep:notify"
|
||||
"dep:notify",
|
||||
"dep:dashmap",
|
||||
"dep:once_cell",
|
||||
"dep:async-broadcast",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
@@ -71,7 +77,7 @@ end2end-cmd = "cargo make test-ui"
|
||||
end2end-dir = "e2e"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -5,13 +5,15 @@ use leptos_meta::{provide_meta_context, Link, Meta, Stylesheet};
|
||||
use leptos_router::{ActionForm, Route, Router, Routes};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use server_fn::{
|
||||
client::{browser::BrowserClient, Client},
|
||||
codec::{
|
||||
Encoding, FromReq, FromRes, GetUrl, IntoReq, IntoRes, MultipartData,
|
||||
MultipartFormData, Rkyv, SerdeLite, StreamingText, TextStream,
|
||||
},
|
||||
request::{ClientReq, Req},
|
||||
response::{ClientRes, Res},
|
||||
request::{browser::BrowserRequest, ClientReq, Req},
|
||||
response::{browser::BrowserResponse, ClientRes, Res},
|
||||
};
|
||||
use std::future::Future;
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::sync::{
|
||||
atomic::{AtomicU8, Ordering},
|
||||
@@ -55,8 +57,10 @@ pub fn HomePage() -> impl IntoView {
|
||||
<ServerFnArgumentExample/>
|
||||
<RkyvExample/>
|
||||
<FileUpload/>
|
||||
<FileUploadWithProgress/>
|
||||
<FileWatcher/>
|
||||
<CustomEncoding/>
|
||||
<CustomClientExample/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +320,7 @@ pub fn RkyvExample() -> impl IntoView {
|
||||
set_input(value);
|
||||
}
|
||||
>
|
||||
Click to see length
|
||||
Click to capitalize
|
||||
</button>
|
||||
<p>{input}</p>
|
||||
<Transition>
|
||||
@@ -331,8 +335,8 @@ pub fn FileUpload() -> impl IntoView {
|
||||
///
|
||||
/// On the server, this uses the `multer` crate, which provides a streaming API.
|
||||
#[server(
|
||||
input = MultipartFormData,
|
||||
)]
|
||||
input = MultipartFormData,
|
||||
)]
|
||||
pub async fn file_length(
|
||||
data: MultipartData,
|
||||
) -> Result<usize, ServerFnError> {
|
||||
@@ -390,6 +394,168 @@ pub fn FileUpload() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
/// This component uses server functions to upload a file, while streaming updates on the upload
|
||||
/// progress.
|
||||
#[component]
|
||||
pub fn FileUploadWithProgress() -> impl IntoView {
|
||||
/// In theory, you could create a single server function which
|
||||
/// 1) received multipart form data
|
||||
/// 2) returned a stream that contained updates on the progress
|
||||
///
|
||||
/// In reality, browsers do not actually support duplexing requests in this way. In other
|
||||
/// words, every existing browser actually requires that the request stream be complete before
|
||||
/// it begins processing the response stream.
|
||||
///
|
||||
/// Instead, we can create two separate server functions:
|
||||
/// 1) one that receives multipart form data and begins processing the upload
|
||||
/// 2) a second that returns a stream of updates on the progress
|
||||
///
|
||||
/// This requires us to store some global state of all the uploads. In a real app, you probably
|
||||
/// shouldn't do exactly what I'm doing here in the demo. For example, this map just
|
||||
/// distinguishes between files by filename, not by user.
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod progress {
|
||||
use async_broadcast::{broadcast, Receiver, Sender};
|
||||
use dashmap::DashMap;
|
||||
use futures::Stream;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
struct File {
|
||||
total: usize,
|
||||
tx: Sender<usize>,
|
||||
rx: Receiver<usize>,
|
||||
}
|
||||
|
||||
static FILES: Lazy<DashMap<String, File>> = Lazy::new(DashMap::new);
|
||||
|
||||
pub async fn add_chunk(filename: &str, len: usize) {
|
||||
println!("[{filename}]\tadding {len}");
|
||||
let mut entry =
|
||||
FILES.entry(filename.to_string()).or_insert_with(|| {
|
||||
println!("[{filename}]\tinserting channel");
|
||||
let (tx, rx) = broadcast(128);
|
||||
File { total: 0, tx, rx }
|
||||
});
|
||||
entry.total += len;
|
||||
let new_total = entry.total;
|
||||
|
||||
// we're about to do an async broadcast, so we don't want to hold a lock across it
|
||||
let tx = entry.tx.clone();
|
||||
drop(entry);
|
||||
|
||||
// now we send the message and don't have to worry about it
|
||||
tx.broadcast(new_total)
|
||||
.await
|
||||
.expect("couldn't send a message over channel");
|
||||
}
|
||||
|
||||
pub fn for_file(filename: &str) -> impl Stream<Item = usize> {
|
||||
let entry =
|
||||
FILES.entry(filename.to_string()).or_insert_with(|| {
|
||||
println!("[{filename}]\tinserting channel");
|
||||
let (tx, rx) = broadcast(128);
|
||||
File { total: 0, tx, rx }
|
||||
});
|
||||
entry.rx.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[server(
|
||||
input = MultipartFormData,
|
||||
)]
|
||||
pub async fn upload_file(data: MultipartData) -> Result<(), ServerFnError> {
|
||||
let mut data = data.into_inner().unwrap();
|
||||
|
||||
while let Ok(Some(mut field)) = data.next_field().await {
|
||||
let name =
|
||||
field.file_name().expect("no filename on field").to_string();
|
||||
while let Ok(Some(chunk)) = field.chunk().await {
|
||||
let len = chunk.len();
|
||||
println!("[{name}]\t{len}");
|
||||
progress::add_chunk(&name, len).await;
|
||||
// in a real server function, you'd do something like saving the file here
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(output = StreamingText)]
|
||||
pub async fn file_progress(
|
||||
filename: String,
|
||||
) -> Result<TextStream, ServerFnError> {
|
||||
println!("getting progress on {filename}");
|
||||
// get the stream of current length for the file
|
||||
let progress = progress::for_file(&filename);
|
||||
// separate each number with a newline
|
||||
// the HTTP response might pack multiple lines of this into a single chunk
|
||||
// we need some way of dividing them up
|
||||
let progress = progress.map(|bytes| Ok(format!("{bytes}\n")));
|
||||
Ok(TextStream::new(progress))
|
||||
}
|
||||
|
||||
let (filename, set_filename) = create_signal(None);
|
||||
let (max, set_max) = create_signal(None);
|
||||
let (current, set_current) = create_signal(None);
|
||||
let on_submit = move |ev: SubmitEvent| {
|
||||
ev.prevent_default();
|
||||
let target = ev.target().unwrap().unchecked_into::<HtmlFormElement>();
|
||||
let form_data = FormData::new_with_form(&target).unwrap();
|
||||
let file = form_data
|
||||
.get("file_to_upload")
|
||||
.unchecked_into::<web_sys::File>();
|
||||
let filename = file.name();
|
||||
let size = file.size() as usize;
|
||||
set_filename(Some(filename.clone()));
|
||||
set_max(Some(size));
|
||||
set_current(None::<usize>);
|
||||
|
||||
spawn_local(async move {
|
||||
let mut progress = file_progress(filename)
|
||||
.await
|
||||
.expect("couldn't initialize stream")
|
||||
.into_inner();
|
||||
while let Some(Ok(len)) = progress.next().await {
|
||||
// the TextStream from the server function will be a series of `usize` values
|
||||
// however, the response itself may pack those chunks into a smaller number of
|
||||
// chunks, each with more text in it
|
||||
// so we've padded them with newspace, and will split them out here
|
||||
// each value is the latest total, so we'll just take the last one
|
||||
let len = len
|
||||
.split('\n')
|
||||
.filter(|n| !n.is_empty())
|
||||
.last()
|
||||
.expect(
|
||||
"expected at least one non-empty value from \
|
||||
newline-delimited rows",
|
||||
)
|
||||
.parse::<usize>()
|
||||
.expect("invalid length");
|
||||
set_current(Some(len));
|
||||
}
|
||||
});
|
||||
spawn_local(async move {
|
||||
upload_file(form_data.into())
|
||||
.await
|
||||
.expect("couldn't upload file");
|
||||
});
|
||||
};
|
||||
|
||||
view! {
|
||||
<h3>File Upload with Progress</h3>
|
||||
<p>A file upload with progress can be handled with two separate server functions.</p>
|
||||
<aside>See the doc comment on the component for an explanation.</aside>
|
||||
<form on:submit=on_submit>
|
||||
<input type="file" name="file_to_upload"/>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
{move || filename().map(|filename| view! { <p>Uploading {filename}</p> })}
|
||||
{move || max().map(|max| view! {
|
||||
<progress max=max value=move || current().unwrap_or_default()/>
|
||||
})}
|
||||
}
|
||||
}
|
||||
#[component]
|
||||
pub fn FileWatcher() -> impl IntoView {
|
||||
#[server(input = GetUrl, output = StreamingText)]
|
||||
@@ -632,3 +798,55 @@ pub fn CustomEncoding() -> impl IntoView {
|
||||
<p>{result}</p>
|
||||
}
|
||||
}
|
||||
|
||||
/// Middleware lets you modify the request/response on the server.
|
||||
///
|
||||
/// On the client, you might also want to modify the request. For example, you may need to add a
|
||||
/// custom header for authentication on every request. You can do this by creating a "custom
|
||||
/// client."
|
||||
#[component]
|
||||
pub fn CustomClientExample() -> impl IntoView {
|
||||
// Define a type for our client.
|
||||
pub struct CustomClient;
|
||||
|
||||
// Implement the `Client` trait for it.
|
||||
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;
|
||||
type Response = BrowserResponse;
|
||||
|
||||
// Our custom `send()` implementation does all the work.
|
||||
fn send(
|
||||
req: Self::Request,
|
||||
) -> 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();
|
||||
// modify the headers by appending one
|
||||
headers.append("X-Custom-Header", "foobar");
|
||||
// delegate back out to BrowserClient to send the modified request
|
||||
BrowserClient::send(req)
|
||||
}
|
||||
}
|
||||
|
||||
// Specify our custom client with `client = `
|
||||
#[server(client = CustomClient)]
|
||||
pub async fn fn_with_custom_client() -> Result<(), ServerFnError> {
|
||||
use http::header::HeaderMap;
|
||||
use leptos_axum::extract;
|
||||
|
||||
let headers: HeaderMap = extract().await?;
|
||||
let custom_header = headers.get("X-Custom-Header");
|
||||
println!("X-Custom-Header = {custom_header:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
view! {
|
||||
<h3>Custom clients</h3>
|
||||
<p>You can define a custom server function client to do something like adding a header to every request.</p>
|
||||
<p>Check the network request in your browser devtools to see how this client adds a custom header.</p>
|
||||
<button on:click=|_| spawn_local(async { fn_with_custom_client().await.unwrap() })>Click me</button>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4"
|
||||
simple_logger = "4.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7", optional = true, features=["macros"] }
|
||||
axum = { version = "0.7", optional = true, features = ["macros"] }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
tokio = { version = "1", features = ["full"], optional = true }
|
||||
@@ -83,7 +83,7 @@ reload-port = 3001
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -51,7 +51,7 @@ pub mod ssr {
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifying them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
"SELECT token FROM user_permissions WHERE user_id = ?;",
|
||||
)
|
||||
@@ -75,7 +75,7 @@ pub mod ssr {
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifying them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
"SELECT token FROM user_permissions WHERE user_id = ?;",
|
||||
)
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use leptos::*;
|
||||
|
||||
// Slots are created in simillar manner to components, except that they use the #[slot] macro.
|
||||
// Slots are created in similar manner to components, except that they use the #[slot] macro.
|
||||
#[slot]
|
||||
struct Then {
|
||||
children: ChildrenFn,
|
||||
|
||||
@@ -86,7 +86,7 @@ reload-port = 3001
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
|
||||
@@ -47,7 +47,7 @@ pub mod ssr_imports {
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifying them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
"SELECT token FROM user_permissions WHERE user_id = ?;",
|
||||
)
|
||||
@@ -71,7 +71,7 @@ pub mod ssr_imports {
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifying them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
"SELECT token FROM user_permissions WHERE user_id = ?;",
|
||||
)
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -8,7 +8,6 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
@@ -17,12 +16,12 @@ leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
log = "0.4"
|
||||
|
||||
# dependecies for client (enable when csr or hydrate set)
|
||||
# dependencies for client (enable when csr or hydrate set)
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
console_log = { version = "1", optional = true }
|
||||
console_error_panic_hook = { version = "0.1", optional = true }
|
||||
|
||||
# dependecies for server (enable when ssr set)
|
||||
# dependencies for server (enable when ssr set)
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", features = ["macros"], optional = true }
|
||||
futures = { version = "0.3", optional = true }
|
||||
@@ -97,7 +96,7 @@ end2end-cmd = "npx playwright test"
|
||||
end2end-dir = "end2end"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Leptos with Axum + TailwindCSS Tempate
|
||||
# Leptos with Axum + TailwindCSS Template
|
||||
|
||||
This is a template demonstrating how to integrate [TailwindCSS](https://tailwindcss.com/) with the [Leptos](https://github.com/leptos-rs/leptos) web framework, Axum server, and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool.
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -10,9 +10,7 @@ leptos_router = { path = "../../router", features = ["csr", "nightly"] }
|
||||
log = "0.4"
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
|
||||
|
||||
# dependecies for client (enable when csr or hydrate set)
|
||||
# dependencies for client (enable when csr or hydrate set)
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
console_log = { version = "1"}
|
||||
console_error_panic_hook = { version = "0.1"}
|
||||
|
||||
console_log = { version = "1" }
|
||||
console_error_panic_hook = { version = "0.1" }
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -67,7 +67,7 @@ end2end-cmd = "cargo make test-ui"
|
||||
end2end-dir = "e2e"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -69,7 +69,7 @@ end2end-cmd = "cargo make test-ui"
|
||||
end2end-dir = "e2e"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -70,7 +70,7 @@ end2end-cmd = "cargo make test-ui"
|
||||
end2end-dir = "e2e"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -150,14 +150,6 @@ pub fn redirect(path: &str) {
|
||||
to redirect()."
|
||||
);
|
||||
}
|
||||
if let Some(response_options) = use_context::<ResponseOptions>() {
|
||||
response_options.set_status(StatusCode::FOUND);
|
||||
response_options.insert_header(
|
||||
header::LOCATION,
|
||||
header::HeaderValue::from_str(path)
|
||||
.expect("Failed to create HeaderValue"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An Actix [struct@Route](actix_web::Route) that listens for a `POST` request with
|
||||
@@ -1369,7 +1361,7 @@ impl LeptosRoutes for &mut ServiceConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper to make it easier to use Axum extractors in server functions.
|
||||
/// A helper to make it easier to use Actix extractors in server functions.
|
||||
///
|
||||
/// It is generic over some type `T` that implements [`FromRequest`] and can
|
||||
/// therefore be used in an extractor. The compiler can often infer this type.
|
||||
@@ -1390,15 +1382,13 @@ impl LeptosRoutes for &mut ServiceConfig {
|
||||
/// Ok(data)
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn extract<T, CustErr>() -> Result<T, ServerFnError<CustErr>>
|
||||
pub async fn extract<T>() -> Result<T, ServerFnError>
|
||||
where
|
||||
T: actix_web::FromRequest,
|
||||
<T as FromRequest>::Error: Display,
|
||||
{
|
||||
let req = use_context::<HttpRequest>().ok_or_else(|| {
|
||||
ServerFnError::ServerError(
|
||||
"HttpRequest should have been provided via context".to_string(),
|
||||
)
|
||||
ServerFnError::new("HttpRequest should have been provided via context")
|
||||
})?;
|
||||
|
||||
T::extract(&req)
|
||||
|
||||
@@ -14,7 +14,7 @@ axum = { version = "0.7", default-features = false, features = [
|
||||
futures = "0.3"
|
||||
http-body-util = "0.1"
|
||||
leptos = { workspace = true, features = ["ssr"] }
|
||||
server_fn = { workspace = true, features = ["axum"] }
|
||||
server_fn = { workspace = true, features = ["axum-no-default"] }
|
||||
leptos_macro = { workspace = true, features = ["axum"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! To run in this environment, you need to disable the default feature set and enable
|
||||
//! the `wasm` feature on `leptos_axum` in your `Cargo.toml`.
|
||||
//! ```toml
|
||||
//! leptos_axum = { version = "0.6.0-rc1", default-features = false, features = ["wasm"] }
|
||||
//! leptos_axum = { version = "0.6.0", default-features = false, features = ["wasm"] }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Features
|
||||
@@ -54,11 +54,8 @@ use leptos_meta::{generate_head_metadata_separated, MetaContext};
|
||||
use leptos_router::*;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::RwLock;
|
||||
use server_fn::redirect::REDIRECT_HEADER;
|
||||
use std::{
|
||||
error::Error, fmt::Debug, io, pin::Pin, sync::Arc,
|
||||
thread::available_parallelism,
|
||||
};
|
||||
use server_fn::{error::NoCustomError, redirect::REDIRECT_HEADER};
|
||||
use std::{fmt::Debug, io, pin::Pin, sync::Arc, thread::available_parallelism};
|
||||
use tokio_util::task::LocalPoolHandle;
|
||||
use tracing::Instrument;
|
||||
|
||||
@@ -332,7 +329,15 @@ async fn handle_server_fns_inner(
|
||||
_ = tx.send(res);
|
||||
});
|
||||
|
||||
rx.await.unwrap()
|
||||
rx.await.unwrap_or_else(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ServerFnError::<NoCustomError>::ServerError(e.to_string())
|
||||
.ser()
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.into_response()
|
||||
})
|
||||
}
|
||||
|
||||
pub type PinnedHtmlStream =
|
||||
@@ -1589,6 +1594,14 @@ where
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
// S represents the router's finished state allowing us to provide
|
||||
// it to the user's server functions.
|
||||
let state = options.clone();
|
||||
let cx_with_state = move || {
|
||||
provide_context::<S>(state.clone());
|
||||
additional_context();
|
||||
};
|
||||
|
||||
let mut router = self;
|
||||
|
||||
// register router paths
|
||||
@@ -1604,7 +1617,7 @@ where
|
||||
path,
|
||||
LeptosOptions::from_ref(options),
|
||||
app_fn.clone(),
|
||||
additional_context.clone(),
|
||||
cx_with_state.clone(),
|
||||
method,
|
||||
static_mode,
|
||||
)
|
||||
@@ -1624,7 +1637,7 @@ where
|
||||
SsrMode::OutOfOrder => {
|
||||
let s = render_app_to_stream_with_context(
|
||||
LeptosOptions::from_ref(options),
|
||||
additional_context.clone(),
|
||||
cx_with_state.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
@@ -1638,7 +1651,7 @@ where
|
||||
SsrMode::PartiallyBlocked => {
|
||||
let s = render_app_to_stream_with_context_and_replace_blocks(
|
||||
LeptosOptions::from_ref(options),
|
||||
additional_context.clone(),
|
||||
cx_with_state.clone(),
|
||||
app_fn.clone(),
|
||||
true
|
||||
);
|
||||
@@ -1653,7 +1666,7 @@ where
|
||||
SsrMode::InOrder => {
|
||||
let s = render_app_to_stream_in_order_with_context(
|
||||
LeptosOptions::from_ref(options),
|
||||
additional_context.clone(),
|
||||
cx_with_state.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
@@ -1667,7 +1680,7 @@ where
|
||||
SsrMode::Async => {
|
||||
let s = render_app_async_with_context(
|
||||
LeptosOptions::from_ref(options),
|
||||
additional_context.clone(),
|
||||
cx_with_state.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
@@ -1686,9 +1699,9 @@ where
|
||||
|
||||
// register server functions
|
||||
for (path, method) in server_fn::axum::server_fn_paths() {
|
||||
let additional_context = additional_context.clone();
|
||||
let cx_with_state = cx_with_state.clone();
|
||||
let handler = move |req: Request<Body>| async move {
|
||||
handle_server_fns_with_context(additional_context, req).await
|
||||
handle_server_fns_with_context(cx_with_state, req).await
|
||||
};
|
||||
router = router.route(
|
||||
path,
|
||||
@@ -1772,13 +1785,12 @@ fn get_leptos_pool() -> LocalPoolHandle {
|
||||
/// Ok(query)
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn extract<T, CustErr>() -> Result<T, ServerFnError>
|
||||
pub async fn extract<T>() -> Result<T, ServerFnError>
|
||||
where
|
||||
T: Sized + FromRequestParts<()>,
|
||||
T::Rejection: Debug,
|
||||
CustErr: Error + 'static,
|
||||
{
|
||||
extract_with_state::<T, (), CustErr>(&()).await
|
||||
extract_with_state::<T, ()>(&()).await
|
||||
}
|
||||
|
||||
/// A helper to make it easier to use Axum extractors in server functions. This
|
||||
@@ -1800,18 +1812,14 @@ where
|
||||
/// Ok(query)
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn extract_with_state<T, S, CustErr>(
|
||||
state: &S,
|
||||
) -> Result<T, ServerFnError>
|
||||
pub async fn extract_with_state<T, S>(state: &S) -> Result<T, ServerFnError>
|
||||
where
|
||||
T: Sized + FromRequestParts<S>,
|
||||
T::Rejection: Debug,
|
||||
CustErr: Error + 'static,
|
||||
{
|
||||
let mut parts = use_context::<Parts>().ok_or_else(|| {
|
||||
ServerFnError::ServerError::<CustErr>(
|
||||
"should have had Parts provided by the leptos_axum integration"
|
||||
.to_string(),
|
||||
ServerFnError::new(
|
||||
"should have had Parts provided by the leptos_axum integration",
|
||||
)
|
||||
})?;
|
||||
T::from_request_parts(&mut parts, state)
|
||||
|
||||
@@ -206,3 +206,5 @@ fn ssr_option() {
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
// TODO: remove simulated change
|
||||
|
||||
@@ -276,6 +276,14 @@ impl ElementDescriptor for Custom {
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
/// Represents an HTML element.
|
||||
///### Beginner's tip:
|
||||
/// `HtmlElement<El>` implements [`std::ops::Deref`] where `El` implements [`std::ops::Deref`].
|
||||
/// When `El` has a corresponding [`web_sys::HtmlElement`] -> `El` will implement [`std::ops::Deref`] for it.
|
||||
/// For instance [`leptos::HtmlElement<Div>`] implements [`std::ops::Deref`] for [`web_sys::HtmlDivElement`]
|
||||
/// Because of [Deref Coercion](https://doc.rust-lang.org/std/ops/trait.Deref.html#deref-coercion) you can call applicable [`web_sys::HtmlElement`] methods on `HtmlElement<El>` as if it were that type.
|
||||
/// If both `HtmlElement<El>` and one of its derefs have a method with the same name, the dot syntax will call the method on the inherent impl (i.e. `HtmlElement<El>` or it's [`std::ops::Deref`] Target).
|
||||
/// You may need to manually deref to access other methods, for example, `(*el).style()` to get the `CssStyleDeclaration` instead of calling [`leptos::HtmlElement::style`].
|
||||
|
||||
#[must_use = "You are creating an HtmlElement<_> but not using it. An unused view can \
|
||||
cause your view to be rendered as () unexpectedly, and it can \
|
||||
also cause issues with client-side hydration."]
|
||||
|
||||
@@ -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", "test", "--doc"]
|
||||
args = ["+nightly-2024-01-29", "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", "doc"]
|
||||
args = ["+nightly-2024-01-29", "doc"]
|
||||
cwd = "example"
|
||||
install_crate = false
|
||||
|
||||
@@ -915,6 +915,16 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// Whatever encoding is provided to `input` should implement `IntoReq` and `FromReq`. Whatever encoding is provided
|
||||
/// to `output` should implement `IntoRes` and `FromRes`.
|
||||
///
|
||||
/// ## Default Values for Parameters
|
||||
///
|
||||
/// Individual function parameters can be annotated with `#[server(default)]`, which will pass
|
||||
/// through `#[serde(default)]`. This is useful for the empty values of arguments with some
|
||||
/// encodings. The URL encoding, for example, omits a field entirely if it is an empty `Vec<_>`,
|
||||
/// but this causes a deserialization error: the correct solution is to add `#[server(default)]`.
|
||||
/// ```rust,ignore
|
||||
/// pub async fn with_default_value(#[server(default)] values: Vec<u32>) /* etc. */
|
||||
/// ```
|
||||
///
|
||||
/// ## Important Notes
|
||||
/// - **Server functions must be `async`.** Even if the work being done inside the function body
|
||||
/// can run synchronously on the server, from the client’s perspective it involves an asynchronous
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{attribute_value, Mode};
|
||||
use convert_case::{Case::Snake, Casing};
|
||||
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
|
||||
use quote::{format_ident, quote, quote_spanned};
|
||||
use quote::{quote, quote_spanned};
|
||||
use rstml::node::{KeyedAttribute, Node, NodeElement, NodeName};
|
||||
use syn::{spanned::Spanned, Expr, Expr::Tuple, ExprLit, ExprPath, Lit};
|
||||
|
||||
@@ -534,12 +534,12 @@ pub(crate) fn directive_call_from_attribute_node(
|
||||
attr: &KeyedAttribute,
|
||||
directive_name: &str,
|
||||
) -> TokenStream {
|
||||
let handler = format_ident!("{directive_name}", span = attr.key.span());
|
||||
let handler = syn::Ident::new(directive_name, attr.key.span());
|
||||
|
||||
let param = if let Some(value) = attr.value() {
|
||||
quote! { #value.into() }
|
||||
quote! { ::std::convert::Into::into(#value) }
|
||||
} else {
|
||||
quote! { () }
|
||||
quote! { ().into() }
|
||||
};
|
||||
|
||||
quote! { .directive(#handler, #param) }
|
||||
|
||||
@@ -385,7 +385,10 @@ where
|
||||
// client
|
||||
create_render_effect({
|
||||
let r = Rc::clone(&r);
|
||||
move |_| r.load(false, id)
|
||||
move |_| {
|
||||
source.track();
|
||||
r.load(false, id)
|
||||
}
|
||||
});
|
||||
|
||||
Resource {
|
||||
|
||||
@@ -281,7 +281,7 @@ impl Runtime {
|
||||
let source_map = self.node_sources.borrow();
|
||||
for effect in subs.borrow().iter() {
|
||||
if let Some(effect_sources) = source_map.get(*effect) {
|
||||
effect_sources.borrow_mut().remove(&node);
|
||||
effect_sources.borrow_mut().swap_remove(&node);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,7 +308,18 @@ impl Runtime {
|
||||
let subs = self.node_subscribers.borrow();
|
||||
for source in sources.borrow().iter() {
|
||||
if let Some(source) = subs.get(*source) {
|
||||
source.borrow_mut().remove(&node_id);
|
||||
// Using `.shift_remove()` here guarantees that dependencies
|
||||
// of a signal are always triggered in the same order.
|
||||
// This is important for cases in which, for example, the first effect
|
||||
// conditionally checks that the signal value is `Some(_)`, and the
|
||||
// second one unwraps its value; if they maintain this order, then the check
|
||||
// will always run first, and will cancel the unwrap if it is None. But if the
|
||||
// order can be inverted (by using .swap_remove() here), the unwrap will
|
||||
// run first on a subsequent run.
|
||||
//
|
||||
// Maintaining execution order is the intention of using an IndexSet here anyway,
|
||||
// but using .swap_remove() would undermine that goal.
|
||||
source.borrow_mut().shift_remove(&node_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.6.0-rc1"
|
||||
version = "0.6.5"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -158,7 +158,7 @@ impl MetaTagsContext {
|
||||
move || {
|
||||
let head = document().head().unwrap_throw();
|
||||
_ = head.remove_child(&el);
|
||||
els.borrow_mut().remove(&id);
|
||||
els.borrow_mut().swap_remove(&id);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.6.0-rc1"
|
||||
version = "0.6.5"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -421,7 +421,7 @@ fn current_window_origin() -> String {
|
||||
#[component]
|
||||
pub fn ActionForm<ServFn>(
|
||||
/// The action from which to build the form. This should include a URL, which can be generated
|
||||
/// by default using [`create_server_action`](l:eptos_server::create_server_action) or added
|
||||
/// by default using [`create_server_action`](leptos_server::create_server_action) or added
|
||||
/// manually using [`using_server_fn`](leptos_server::Action::using_server_fn).
|
||||
action: Action<
|
||||
ServFn,
|
||||
@@ -593,21 +593,38 @@ where
|
||||
action_form
|
||||
}
|
||||
|
||||
fn form_from_event(ev: &SubmitEvent) -> Option<HtmlFormElement> {
|
||||
let submitter = ev.unchecked_ref::<SubmitEvent>().submitter();
|
||||
match &submitter {
|
||||
fn form_data_from_event(
|
||||
ev: &SubmitEvent,
|
||||
) -> Result<FormData, FromFormDataError> {
|
||||
let submitter = ev.submitter();
|
||||
let mut submitter_name_value = None;
|
||||
let opt_form = match &submitter {
|
||||
Some(el) => {
|
||||
if let Some(form) = el.dyn_ref::<HtmlFormElement>() {
|
||||
Some(form.clone())
|
||||
} else if el.is_instance_of::<HtmlInputElement>()
|
||||
|| el.is_instance_of::<HtmlButtonElement>()
|
||||
{
|
||||
} else if let Some(input) = el.dyn_ref::<HtmlInputElement>() {
|
||||
submitter_name_value = Some((input.name(), input.value()));
|
||||
Some(ev.target().unwrap().unchecked_into())
|
||||
} else if let Some(button) = el.dyn_ref::<HtmlButtonElement>() {
|
||||
submitter_name_value = Some((button.name(), button.value()));
|
||||
Some(ev.target().unwrap().unchecked_into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
None => ev.target().map(|form| form.unchecked_into()),
|
||||
};
|
||||
match opt_form.as_ref().map(FormData::new_with_form) {
|
||||
None => Err(FromFormDataError::MissingForm(ev.clone().into())),
|
||||
Some(Err(e)) => Err(FromFormDataError::FormData(e)),
|
||||
Some(Ok(form_data)) => {
|
||||
if let Some((name, value)) = submitter_name_value {
|
||||
form_data
|
||||
.append_with_str(&name, &value)
|
||||
.map_err(FromFormDataError::FormData)?;
|
||||
}
|
||||
Ok(form_data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -755,10 +772,8 @@ where
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
fn from_event(ev: &Event) -> Result<Self, FromFormDataError> {
|
||||
let form = form_from_event(ev.unchecked_ref())
|
||||
.ok_or_else(|| FromFormDataError::MissingForm(ev.clone()))?;
|
||||
let form_data = FormData::new_with_form(&form)
|
||||
.map_err(FromFormDataError::FormData)?;
|
||||
let submit_ev = ev.unchecked_ref();
|
||||
let form_data = form_data_from_event(submit_ev)?;
|
||||
Self::from_form_data(&form_data)
|
||||
.map_err(FromFormDataError::Deserialization)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ impl ParamsMap {
|
||||
/// Inserts a value into the map.
|
||||
#[inline(always)]
|
||||
pub fn insert(&mut self, key: String, value: String) -> Option<String> {
|
||||
use crate::history::url::unescape;
|
||||
let value = unescape(&value);
|
||||
self.0.insert(key, value)
|
||||
}
|
||||
|
||||
@@ -76,12 +78,14 @@ impl Default for ParamsMap {
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_router::params_map;
|
||||
/// # #[cfg(feature = "ssr")] {
|
||||
/// let map = params_map! {
|
||||
/// "crate" => "leptos",
|
||||
/// 42 => true, // where key & val: core::fmt::Display
|
||||
/// };
|
||||
/// assert_eq!(map.get("crate"), Some(&"leptos".to_string()));
|
||||
/// assert_eq!(map.get("42"), Some(&true.to_string()))
|
||||
/// # }
|
||||
/// ```
|
||||
// Original implementation included the below credits.
|
||||
//
|
||||
|
||||
@@ -15,6 +15,14 @@ pub struct Url {
|
||||
pub hash: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn unescape(s: &str) -> String {
|
||||
percent_encoding::percent_decode_str(s)
|
||||
.decode_utf8()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn unescape(s: &str) -> String {
|
||||
js_sys::decode_uri(s).unwrap().into()
|
||||
|
||||
@@ -42,6 +42,21 @@ cfg_if! {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_build_params_collection_and_decode() {
|
||||
let matcher = Matcher::new("/foo/:id");
|
||||
let matched = matcher.test("/foo/%E2%89%A1abc%20123");
|
||||
assert_eq!(
|
||||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/%E2%89%A1abc%20123".into(),
|
||||
params: params_map!(
|
||||
"id" => "≡abc 123"
|
||||
)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_match_past_end_when_ending_in_asterisk() {
|
||||
let matcher = Matcher::new("/foo/bar/*");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
edition = "2021"
|
||||
imports_granularity = "Crate"
|
||||
max_width = 80
|
||||
format_strings = true
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
name = "server_fn"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "RPC for any web framework."
|
||||
readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
server_fn_macro_default = "0.6.0-rc1"
|
||||
server_fn_macro_default = { workspace = true }
|
||||
# used for hashing paths in #[server] macro
|
||||
const_format = "0.2"
|
||||
xxhash-rust = { version = "0.8", features = ["const_xxh64"] }
|
||||
@@ -18,7 +18,7 @@ serde = { version = "1", features = ["derive"] }
|
||||
send_wrapper = { version = "0.6", features = ["futures"], optional = true }
|
||||
|
||||
# registration system
|
||||
inventory = {version="0.3",optional=true}
|
||||
inventory = { version = "0.3", optional = true }
|
||||
dashmap = "5"
|
||||
once_cell = "1"
|
||||
|
||||
@@ -27,7 +27,7 @@ once_cell = "1"
|
||||
actix-web = { version = "4", optional = true }
|
||||
|
||||
# axum
|
||||
axum = { version = "0.7", optional = true, features = ["multipart"] }
|
||||
axum = { version = "0.7", optional = true, default-features = false, features = ["multipart"] }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-layer = { version = "0.3", optional = true }
|
||||
|
||||
@@ -72,17 +72,21 @@ reqwest = { version = "0.11", default-features = false, optional = true, feature
|
||||
url = "2"
|
||||
|
||||
[features]
|
||||
default = [ "json", "cbor"]
|
||||
default = ["json", "cbor"]
|
||||
form-redirects = []
|
||||
actix = ["ssr", "dep:actix-web", "dep:send_wrapper"]
|
||||
axum = [
|
||||
"ssr",
|
||||
axum-no-default = [
|
||||
"ssr",
|
||||
"dep:axum",
|
||||
"dep:hyper",
|
||||
"dep:http-body-util",
|
||||
"dep:tower",
|
||||
"dep:tower-layer",
|
||||
]
|
||||
axum = [
|
||||
"axum/default",
|
||||
"axum-no-default",
|
||||
]
|
||||
browser = [
|
||||
"dep:gloo-net",
|
||||
"dep:js-sys",
|
||||
@@ -109,4 +113,21 @@ all-features = true
|
||||
# disables some feature combos for testing in CI
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["rustls", "default-tls", "form-redirects"]
|
||||
skip_feature_sets = [["actix", "axum"], ["browser", "actix"], ["browser", "axum"], ["browser", "reqwest"]]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"actix",
|
||||
"axum",
|
||||
],
|
||||
[
|
||||
"browser",
|
||||
"actix",
|
||||
],
|
||||
[
|
||||
"browser",
|
||||
"axum",
|
||||
],
|
||||
[
|
||||
"browser",
|
||||
"reqwest",
|
||||
],
|
||||
]
|
||||
|
||||
@@ -175,7 +175,6 @@ impl<E> ViaError<E> for WrapError<E> {
|
||||
/// This means that other error types can easily be converted into it using the
|
||||
/// `?` operator.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ServerFnError<E = NoCustomError> {
|
||||
/// A user-defined custom error type, which defaults to [`NoCustomError`].
|
||||
WrappedServerError(E),
|
||||
|
||||
@@ -117,7 +117,7 @@ pub mod response;
|
||||
#[cfg(feature = "actix")]
|
||||
#[doc(hidden)]
|
||||
pub use ::actix_web as actix_export;
|
||||
#[cfg(feature = "axum")]
|
||||
#[cfg(feature = "axum-no-default")]
|
||||
#[doc(hidden)]
|
||||
pub use ::axum as axum_export;
|
||||
use client::Client;
|
||||
@@ -456,7 +456,7 @@ impl<Req: 'static, Res: 'static> inventory::Collect
|
||||
}
|
||||
|
||||
/// Axum integration.
|
||||
#[cfg(feature = "axum")]
|
||||
#[cfg(feature = "axum-no-default")]
|
||||
pub mod axum {
|
||||
use crate::{
|
||||
middleware::{BoxedService, Service},
|
||||
|
||||
@@ -26,7 +26,7 @@ pub trait Service<Request, Response> {
|
||||
) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
#[cfg(feature = "axum-no-default")]
|
||||
mod axum {
|
||||
use super::{BoxedService, Service};
|
||||
use crate::{response::Res, ServerFnError};
|
||||
|
||||
@@ -5,6 +5,7 @@ use futures::{Stream, StreamExt};
|
||||
pub use gloo_net::http::Request;
|
||||
use js_sys::{Reflect, Uint8Array};
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use wasm_bindgen::JsValue;
|
||||
use wasm_streams::ReadableStream;
|
||||
use web_sys::{FormData, Headers, RequestInit, UrlSearchParams};
|
||||
@@ -19,6 +20,32 @@ impl From<Request> for BrowserRequest {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BrowserRequest> for Request {
|
||||
fn from(value: BrowserRequest) -> Self {
|
||||
value.0.take()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BrowserRequest> for web_sys::Request {
|
||||
fn from(value: BrowserRequest) -> Self {
|
||||
value.0.take().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for BrowserRequest {
|
||||
type Target = Request;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.0.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for BrowserRequest {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.0.deref_mut()
|
||||
}
|
||||
}
|
||||
|
||||
/// The `FormData` type available in the browser.
|
||||
#[derive(Debug)]
|
||||
pub struct BrowserFormData(pub(crate) SendWrapper<FormData>);
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::{borrow::Cow, future::Future};
|
||||
#[cfg(feature = "actix")]
|
||||
pub mod actix;
|
||||
/// Request types for Axum.
|
||||
#[cfg(feature = "axum")]
|
||||
#[cfg(feature = "axum-no-default")]
|
||||
pub mod axum;
|
||||
/// Request types for the browser.
|
||||
#[cfg(feature = "browser")]
|
||||
|
||||
62
server_fn/src/request/spin.rs
Normal file
62
server_fn/src/request/spin.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use crate::{error::ServerFnError, request::Req};
|
||||
use axum::body::{Body, Bytes};
|
||||
use futures::{Stream, StreamExt};
|
||||
use http::{
|
||||
header::{ACCEPT, CONTENT_TYPE, REFERER},
|
||||
Request,
|
||||
};
|
||||
use http_body_util::BodyExt;
|
||||
use std::borrow::Cow;
|
||||
|
||||
impl<CustErr> Req<CustErr> for IncomingRequest
|
||||
where
|
||||
CustErr: 'static,
|
||||
{
|
||||
fn as_query(&self) -> Option<&str> {
|
||||
self.uri().query()
|
||||
}
|
||||
|
||||
fn to_content_type(&self) -> Option<Cow<'_, str>> {
|
||||
self.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.map(|h| String::from_utf8_lossy(h.as_bytes()))
|
||||
}
|
||||
|
||||
fn accepts(&self) -> Option<Cow<'_, str>> {
|
||||
self.headers()
|
||||
.get(ACCEPT)
|
||||
.map(|h| String::from_utf8_lossy(h.as_bytes()))
|
||||
}
|
||||
|
||||
fn referer(&self) -> Option<Cow<'_, str>> {
|
||||
self.headers()
|
||||
.get(REFERER)
|
||||
.map(|h| String::from_utf8_lossy(h.as_bytes()))
|
||||
}
|
||||
|
||||
async fn try_into_bytes(self) -> Result<Bytes, ServerFnError<CustErr>> {
|
||||
let (_parts, body) = self.into_parts();
|
||||
|
||||
body.collect()
|
||||
.await
|
||||
.map(|c| c.to_bytes())
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
}
|
||||
|
||||
async fn try_into_string(self) -> Result<String, ServerFnError<CustErr>> {
|
||||
let bytes = self.try_into_bytes().await?;
|
||||
String::from_utf8(bytes.to_vec())
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
}
|
||||
|
||||
fn try_into_stream(
|
||||
self,
|
||||
) -> Result<
|
||||
impl Stream<Item = Result<Bytes, ServerFnError>> + Send + 'static,
|
||||
ServerFnError<CustErr>,
|
||||
> {
|
||||
Ok(self.into_body().into_data_stream().map(|chunk| {
|
||||
chunk.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ pub mod actix;
|
||||
#[cfg(feature = "browser")]
|
||||
pub mod browser;
|
||||
/// Response types for Axum.
|
||||
#[cfg(feature = "axum")]
|
||||
#[cfg(feature = "axum-no-default")]
|
||||
pub mod http;
|
||||
/// Response types for [`reqwest`].
|
||||
#[cfg(feature = "reqwest")]
|
||||
|
||||
@@ -55,6 +55,54 @@ pub fn server_macro_impl(
|
||||
}
|
||||
});
|
||||
|
||||
let fields = body
|
||||
.inputs
|
||||
.iter_mut()
|
||||
.map(|f| {
|
||||
let typed_arg = match f {
|
||||
FnArg::Receiver(_) => {
|
||||
return Err(syn::Error::new(
|
||||
f.span(),
|
||||
"cannot use receiver types in server function macro",
|
||||
))
|
||||
}
|
||||
FnArg::Typed(t) => t,
|
||||
};
|
||||
|
||||
// strip `mut`, which is allowed in fn args but not in struct fields
|
||||
if let Pat::Ident(ident) = &mut *typed_arg.pat {
|
||||
ident.mutability = None;
|
||||
}
|
||||
|
||||
// allow #[server(default)] on fields
|
||||
let mut default = false;
|
||||
let mut other_attrs = Vec::new();
|
||||
for attr in typed_arg.attrs.iter() {
|
||||
if !attr.path().is_ident("server") {
|
||||
other_attrs.push(attr.clone());
|
||||
continue;
|
||||
}
|
||||
attr.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("default") && meta.input.is_empty() {
|
||||
default = true;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(meta.error(
|
||||
"Unrecognized #[server] attribute, expected \
|
||||
#[server(default)]",
|
||||
))
|
||||
}
|
||||
})?;
|
||||
}
|
||||
typed_arg.attrs = other_attrs;
|
||||
if default {
|
||||
Ok(quote! { #[serde(default)] pub #typed_arg })
|
||||
} else {
|
||||
Ok(quote! { pub #typed_arg })
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let dummy = body.to_dummy_output();
|
||||
let dummy_name = body.to_dummy_ident();
|
||||
let args = syn::parse::<ServerFnArgs>(args.into())?;
|
||||
@@ -130,60 +178,11 @@ pub fn server_macro_impl(
|
||||
};
|
||||
|
||||
// build struct for type
|
||||
let mut body = body;
|
||||
let fn_name = &body.ident;
|
||||
let fn_name_as_str = body.ident.to_string();
|
||||
let vis = body.vis;
|
||||
let attrs = body.attrs;
|
||||
|
||||
let fields = body
|
||||
.inputs
|
||||
.iter_mut()
|
||||
.map(|f| {
|
||||
let typed_arg = match f {
|
||||
FnArg::Receiver(_) => {
|
||||
return Err(syn::Error::new(
|
||||
f.span(),
|
||||
"cannot use receiver types in server function macro",
|
||||
))
|
||||
}
|
||||
FnArg::Typed(t) => t,
|
||||
};
|
||||
|
||||
// strip `mut`, which is allowed in fn args but not in struct fields
|
||||
if let Pat::Ident(ident) = &mut *typed_arg.pat {
|
||||
ident.mutability = None;
|
||||
}
|
||||
|
||||
// allow #[server(default)] on fields — TODO is this documented?
|
||||
let mut default = false;
|
||||
let mut other_attrs = Vec::new();
|
||||
for attr in typed_arg.attrs.iter() {
|
||||
if !attr.path().is_ident("server") {
|
||||
other_attrs.push(attr.clone());
|
||||
continue;
|
||||
}
|
||||
attr.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("default") && meta.input.is_empty() {
|
||||
default = true;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(meta.error(
|
||||
"Unrecognized #[server] attribute, expected \
|
||||
#[server(default)]",
|
||||
))
|
||||
}
|
||||
})?;
|
||||
}
|
||||
typed_arg.attrs = other_attrs;
|
||||
if default {
|
||||
Ok(quote! { #[serde(default)] pub #typed_arg })
|
||||
} else {
|
||||
Ok(quote! { pub #typed_arg })
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let fn_args = body
|
||||
.inputs
|
||||
.iter()
|
||||
@@ -637,18 +636,20 @@ fn err_type(return_ty: &Type) -> Result<Option<&GenericArgument>> {
|
||||
else if let GenericArgument::Type(Type::Path(pat)) =
|
||||
&args.args[1]
|
||||
{
|
||||
if pat.path.segments[0].ident == "ServerFnError" {
|
||||
let args = &pat.path.segments[0].arguments;
|
||||
match args {
|
||||
// Result<T, ServerFnError>
|
||||
PathArguments::None => return Ok(None),
|
||||
// Result<T, ServerFnError<E>>
|
||||
PathArguments::AngleBracketed(args) => {
|
||||
if args.args.len() == 1 {
|
||||
return Ok(Some(&args.args[0]));
|
||||
if let Some(segment) = pat.path.segments.last() {
|
||||
if segment.ident == "ServerFnError" {
|
||||
let args = &pat.path.segments[0].arguments;
|
||||
match args {
|
||||
// Result<T, ServerFnError>
|
||||
PathArguments::None => return Ok(None),
|
||||
// Result<T, ServerFnError<E>>
|
||||
PathArguments::AngleBracketed(args) => {
|
||||
if args.args.len() == 1 {
|
||||
return Ok(Some(&args.args[0]));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user