mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 10:11:56 -05:00
Compare commits
2 Commits
v0.6.1
...
gbj-patch-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fce3e775a2 | ||
|
|
866575d49f |
26
Cargo.toml
26
Cargo.toml
@@ -25,22 +25,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.6.1"
|
||||
version = "0.6.0-rc1"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.6.1" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.6.1" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.6.1" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.6.1" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.6.1" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.6.1" }
|
||||
server_fn = { path = "./server_fn", version = "0.6.1" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.6.1" }
|
||||
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" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.6" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.6.1" }
|
||||
leptos_router = { path = "./router", version = "0.6.1" }
|
||||
leptos_meta = { path = "./meta", version = "0.6.1" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.6.1" }
|
||||
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" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
42
README.md
42
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! {
|
||||
@@ -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.
|
||||
|
||||
@@ -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", default-features = false, features = ["wasm"] }
|
||||
//! leptos_axum = { version = "0.6.0-rc1", default-features = false, features = ["wasm"] }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Features
|
||||
|
||||
@@ -915,16 +915,6 @@ 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,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.6.1"
|
||||
version = "0.6.0-rc1"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.6.1"
|
||||
version = "0.6.0-rc1"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
name = "server_fn"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
authors = ["Greg Johnston"]
|
||||
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.1"
|
||||
server_fn_macro_default = "0.6.0-rc1"
|
||||
# used for hashing paths in #[server] macro
|
||||
const_format = "0.2"
|
||||
xxhash-rust = { version = "0.8", features = ["const_xxh64"] }
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
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()))
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -55,54 +55,6 @@ 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())?;
|
||||
@@ -178,11 +130,60 @@ 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()
|
||||
@@ -636,20 +637,18 @@ fn err_type(return_ty: &Type) -> Result<Option<&GenericArgument>> {
|
||||
else if let GenericArgument::Type(Type::Path(pat)) =
|
||||
&args.args[1]
|
||||
{
|
||||
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]));
|
||||
}
|
||||
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]));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user