Compare commits

...

8 Commits

Author SHA1 Message Date
Greg Johnston
4efaac6a94 boolean logic was backwards 2025-07-23 21:58:38 -04:00
Greg Johnston
972e9c1d1c clippy 2025-07-23 15:49:59 -04:00
Greg Johnston
3a6c2fc547 fix: remove lazy macro on from server fns on server 2025-07-23 15:38:16 -04:00
Greg Johnston
e679b72ebb feat: allow lazy server functions 2025-07-21 13:19:52 -04:00
Greg Johnston
f5d3fbb091 0.8.5 2025-07-21 09:04:16 -04:00
Greg Johnston
fbe7cdc482 docs: update documentation for #[lazy] and #[lazy_route] 2025-07-21 08:53:38 -04:00
Greg Johnston
14884bc8ac Merge pull request #3988 from leptos-rs/wasm-splitting-support
feat: wasm-splitting library support for future cargo-leptos integration
2025-07-21 07:17:29 -04:00
martin frances
5f2d511553 chore: bump oco_ref version number (#4168) 2025-07-20 18:44:09 -04:00
19 changed files with 202 additions and 93 deletions

50
Cargo.lock generated
View File

@@ -1788,7 +1788,7 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
[[package]]
name = "leptos"
version = "0.8.4"
version = "0.8.5"
dependencies = [
"any_spawner",
"base64",
@@ -1844,7 +1844,7 @@ dependencies = [
[[package]]
name = "leptos_actix"
version = "0.8.4"
version = "0.8.5"
dependencies = [
"actix-files",
"actix-http",
@@ -1869,7 +1869,7 @@ dependencies = [
[[package]]
name = "leptos_axum"
version = "0.8.4"
version = "0.8.5"
dependencies = [
"any_spawner",
"axum",
@@ -1892,7 +1892,7 @@ dependencies = [
[[package]]
name = "leptos_config"
version = "0.8.4"
version = "0.8.5"
dependencies = [
"config",
"regex",
@@ -1906,7 +1906,7 @@ dependencies = [
[[package]]
name = "leptos_dom"
version = "0.8.4"
version = "0.8.5"
dependencies = [
"js-sys",
"leptos",
@@ -1923,7 +1923,7 @@ dependencies = [
[[package]]
name = "leptos_hot_reload"
version = "0.8.4"
version = "0.8.5"
dependencies = [
"anyhow",
"camino",
@@ -1939,7 +1939,7 @@ dependencies = [
[[package]]
name = "leptos_integration_utils"
version = "0.8.4"
version = "0.8.5"
dependencies = [
"futures",
"hydration_context",
@@ -1952,7 +1952,7 @@ dependencies = [
[[package]]
name = "leptos_macro"
version = "0.8.4"
version = "0.8.5"
dependencies = [
"attribute-derive",
"cfg-if",
@@ -1972,7 +1972,7 @@ dependencies = [
"rustc_version",
"serde",
"server_fn",
"server_fn_macro 0.8.4",
"server_fn_macro 0.8.5",
"syn 2.0.104",
"tracing",
"trybuild",
@@ -1982,7 +1982,7 @@ dependencies = [
[[package]]
name = "leptos_meta"
version = "0.8.4"
version = "0.8.5"
dependencies = [
"futures",
"indexmap",
@@ -1996,7 +1996,7 @@ dependencies = [
[[package]]
name = "leptos_router"
version = "0.8.4"
version = "0.8.5"
dependencies = [
"any_spawner",
"either_of",
@@ -2020,8 +2020,9 @@ dependencies = [
[[package]]
name = "leptos_router_macro"
version = "0.8.4"
version = "0.8.5"
dependencies = [
"leptos",
"leptos_macro",
"leptos_router",
"proc-macro-error2",
@@ -2032,7 +2033,7 @@ dependencies = [
[[package]]
name = "leptos_server"
version = "0.8.4"
version = "0.8.5"
dependencies = [
"any_spawner",
"base64",
@@ -2312,7 +2313,7 @@ dependencies = [
[[package]]
name = "oco_ref"
version = "0.2.0"
version = "0.2.1"
dependencies = [
"serde",
"serde_json",
@@ -2716,9 +2717,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.1"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
@@ -2745,7 +2746,7 @@ dependencies = [
[[package]]
name = "reactive_graph"
version = "0.2.4"
version = "0.2.5"
dependencies = [
"any_spawner",
"async-lock",
@@ -2768,7 +2769,7 @@ dependencies = [
[[package]]
name = "reactive_stores"
version = "0.2.4"
version = "0.2.5"
dependencies = [
"any_spawner",
"dashmap",
@@ -2787,7 +2788,7 @@ dependencies = [
[[package]]
name = "reactive_stores_macro"
version = "0.2.4"
version = "0.2.5"
dependencies = [
"convert_case 0.8.0",
"proc-macro-error2",
@@ -3281,7 +3282,7 @@ dependencies = [
[[package]]
name = "server_fn"
version = "0.8.4"
version = "0.8.5"
dependencies = [
"actix-web",
"actix-ws",
@@ -3344,7 +3345,7 @@ dependencies = [
[[package]]
name = "server_fn_macro"
version = "0.8.4"
version = "0.8.5"
dependencies = [
"const_format",
"convert_case 0.8.0",
@@ -3357,9 +3358,9 @@ dependencies = [
[[package]]
name = "server_fn_macro_default"
version = "0.8.4"
version = "0.8.5"
dependencies = [
"server_fn_macro 0.8.4",
"server_fn_macro 0.8.5",
"syn 2.0.104",
]
@@ -3561,7 +3562,7 @@ dependencies = [
[[package]]
name = "tachys"
version = "0.2.5"
version = "0.2.6"
dependencies = [
"any_spawner",
"async-trait",
@@ -4341,6 +4342,7 @@ name = "wasm_split_helpers"
version = "0.1.0"
dependencies = [
"async-once-cell",
"or_poisoned",
"wasm_split_macros",
]

View File

@@ -40,7 +40,7 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.8.4"
version = "0.8.5"
edition = "2021"
rust-version = "1.88"
@@ -51,26 +51,26 @@ any_spawner = { path = "./any_spawner/", version = "0.3.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
either_of = { path = "./either_of/", version = "0.1.6" }
hydration_context = { path = "./hydration_context", version = "0.3.0" }
leptos = { path = "./leptos", version = "0.8.4" }
leptos_config = { path = "./leptos_config", version = "0.8.4" }
leptos_dom = { path = "./leptos_dom", version = "0.8.4" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.4" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.4" }
leptos_macro = { path = "./leptos_macro", version = "0.8.4" }
leptos_router = { path = "./router", version = "0.8.4" }
leptos_router_macro = { path = "./router_macro", version = "0.8.4" }
leptos_server = { path = "./leptos_server", version = "0.8.4" }
leptos_meta = { path = "./meta", version = "0.8.4" }
leptos = { path = "./leptos", version = "0.8.5" }
leptos_config = { path = "./leptos_config", version = "0.8.5" }
leptos_dom = { path = "./leptos_dom", version = "0.8.5" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.5" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.5" }
leptos_macro = { path = "./leptos_macro", version = "0.8.5" }
leptos_router = { path = "./router", version = "0.8.5" }
leptos_router_macro = { path = "./router_macro", version = "0.8.5" }
leptos_server = { path = "./leptos_server", version = "0.8.5" }
leptos_meta = { path = "./meta", version = "0.8.5" }
next_tuple = { path = "./next_tuple", version = "0.1.0" }
oco_ref = { path = "./oco", version = "0.2.0" }
oco_ref = { path = "./oco", version = "0.2.1" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.2.4" }
reactive_stores = { path = "./reactive_stores", version = "0.2.4" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.4" }
server_fn = { path = "./server_fn", version = "0.8.4" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.4" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.4" }
tachys = { path = "./tachys", version = "0.2.5" }
reactive_graph = { path = "./reactive_graph", version = "0.2.5" }
reactive_stores = { path = "./reactive_stores", version = "0.2.5" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.5" }
server_fn = { path = "./server_fn", version = "0.8.5" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.5" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.5" }
tachys = { path = "./tachys", version = "0.2.6" }
wasm_split_helpers = { path = "./wasm_split", version = "0.1.0" }
wasm_split_macros = { path = "./wasm_split_macros", version = "0.1.0" }

View File

@@ -305,7 +305,10 @@ impl LazyRoute for ViewD {
}
}
// Server functions can be made lazy by combining the two macros,
// with `#[server]` coming first, then `#[lazy]`
#[server]
#[lazy]
async fn d_data() -> Result<Vec<i32>, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(vec![1, 1, 2, 3, 5, 8, 13])

View File

@@ -348,6 +348,7 @@ pub use web_sys;
#[doc(hidden)]
pub mod __reexports {
pub use send_wrapper;
pub use wasm_bindgen_futures;
}

View File

@@ -48,7 +48,10 @@ pub fn lazy_impl(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let is_wasm = cfg!(feature = "csr") || cfg!(feature = "hydrate");
if is_wasm {
quote! {
#[::leptos::wasm_split_helpers::wasm_split(#unique_name)]
#[::leptos::wasm_split_helpers::wasm_split(
#unique_name,
::leptos::__reexports::send_wrapper
)]
#fun
}
} else {

View File

@@ -1030,14 +1030,41 @@ pub fn memo(input: TokenStream) -> TokenStream {
memo::memo_impl(input)
}
/// The `#[lazy]` macro marks an `async` function as a function that can be lazy-loaded from a
/// separate (WebAssembly) binary.
/// The `#[lazy]` macro indicates that a function can be lazy-loaded from a separate WebAssembly (WASM) binary.
///
/// The first time the function is called, calling the function will first load that other binary,
/// then call the function. On subsequent call it will be called immediately, but still return
/// then call the function. On subsequent calls it will be called immediately, but still return
/// asynchronously to maintain the same API.
///
/// All parameters and output types should be concrete types, with no generics.
/// `#[lazy]` can be used to annotate synchronous or `async` functions. In both cases, the final function will be
/// `async` and must be called as such.
///
/// All parameters and output types should be concrete types, with no generics or `impl Trait` types.
///
/// This should be used in tandem with a suitable build process, such as `cargo leptos --split`.
///
/// ```rust
/// # use leptos_macro::lazy;
///
/// #[lazy]
/// fn lazy_synchronous_function() -> String {
/// "Hello, lazy world!".to_string()
/// }
///
/// #[lazy]
/// async fn lazy_async_function() -> String {
/// /* do something that requires async work */
/// "Hello, lazy async world!".to_string()
/// }
///
/// async fn use_lazy_functions() {
/// // synchronous function has been converted to async
/// let value1 = lazy_synchronous_function().await;
///
/// // async function is still async
/// let value1 = lazy_async_function().await;
/// }
/// ```
#[proc_macro_attribute]
#[proc_macro_error]
pub fn lazy(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.8.4"
version = { workspace = true }
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"

View File

@@ -1,6 +1,6 @@
[package]
name = "oco_ref"
version = "0.2.0"
version = "0.2.1"
authors = ["Danik Vitek", "Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -10,7 +10,7 @@ edition.workspace = true
[dependencies]
serde = { workspace = true, default-features = true }
thiserror = { workspace = true , default-features = true }
thiserror = { workspace = true, default-features = true }
[dev-dependencies]
serde_json = { workspace = true , default-features = true }
serde_json = { workspace = true, default-features = true }

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.2.4"
version = "0.2.5"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores"
version = "0.2.4"
version = "0.2.5"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores_macro"
version = "0.2.4"
version = "0.2.5"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.8.4"
version = { workspace = true }
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router_macro"
version = "0.8.4"
version = { workspace = true }
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"
@@ -19,6 +19,7 @@ quote = { workspace = true, default-features = true }
syn = { features = ["full"], workspace = true, default-features = true }
[dev-dependencies]
leptos = { path = "../leptos" }
leptos_router = { path = "../router" }
leptos_macro = { path = "../leptos_macro" }

View File

@@ -198,6 +198,37 @@ impl ToTokens for Segments {
/// add a [`lazy`] annotation to the `view` method, which will cause the code for the view
/// to lazy-load concurrently with the `data` being loaded for the route.
///
/// ```rust
/// use leptos::prelude::*;
/// use leptos_router::{lazy_route, LazyRoute};
///
/// // the route definition
/// #[derive(Debug)]
/// struct BlogListingRoute {
/// titles: Resource<Vec<String>>
/// }
///
/// #[lazy_route]
/// impl LazyRoute for BlogListingRoute {
/// fn data() -> Self {
/// Self {
/// titles: Resource::new(|| (), |_| async {
/// vec![/* todo: load blog posts */]
/// })
/// }
/// }
///
/// // this function will be lazy-loaded, concurrently with data()
/// fn view(this: Self) -> AnyView {
/// let BlogListingRoute { titles } = this;
///
/// // ... now you can use the `posts` resource with Suspense, etc.,
/// // and return AnyView by calling .into_any() on a view
/// # ().into_any()
/// }
/// }
/// ```
///
/// [`impl LazyRoute`]: https://docs.rs/leptos_router/latest/leptos_router/trait.LazyRoute.html
/// [`lazy`]: https://docs.rs/leptos_macro/latest/leptos_macro/macro.lazy.html
#[proc_macro_attribute]

View File

@@ -1525,7 +1525,10 @@ impl Parse for ServerFnBody {
true
}
} else {
true
// in ssr mode, remove the "lazy" macro
// the lazy macro doesn't do anything on the server anyway, but it can cause confusion for rust-analyzer
// when the lazy macro is applied to both the function and the dummy
!(cfg!(feature = "ssr") && matches!(attr.meta.path().segments.last(), Some(PathSegment { ident, .. }) if ident == "lazy") )
}
});

View File

@@ -1,6 +1,6 @@
[package]
name = "tachys"
version = "0.2.5"
version = "0.2.6"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -14,3 +14,4 @@ async-once-cell = { default-features = true, workspace = true, features = [
"std",
] }
wasm_split_macros.workspace = true
or_poisoned.workspace = true

View File

@@ -1,9 +1,8 @@
use std::{
cell::Cell,
ffi::c_void,
future::Future,
pin::Pin,
rc::Rc,
sync::{Arc, Mutex},
task::{Context, Poll, Waker},
};
@@ -12,18 +11,19 @@ pub type LoadFn = unsafe extern "C" fn(LoadCallbackFn, *const c_void) -> ();
type Lazy = async_once_cell::Lazy<Option<()>, SplitLoaderFuture>;
use or_poisoned::OrPoisoned;
pub use wasm_split_macros::wasm_split;
pub struct LazySplitLoader {
lazy: Pin<Rc<Lazy>>,
lazy: Pin<Arc<Lazy>>,
}
impl LazySplitLoader {
pub fn new(load: LoadFn) -> Self {
Self {
lazy: Rc::pin(Lazy::new(SplitLoaderFuture::new(SplitLoader::new(
load,
)))),
lazy: Arc::pin(Lazy::new(SplitLoaderFuture::new(
SplitLoader::new(load),
))),
}
}
}
@@ -42,36 +42,33 @@ enum SplitLoaderState {
}
struct SplitLoader {
state: Cell<SplitLoaderState>,
waker: Cell<Option<Waker>>,
state: Mutex<SplitLoaderState>,
waker: Mutex<Option<Waker>>,
}
impl SplitLoader {
fn new(load: LoadFn) -> Rc<Self> {
Rc::new(SplitLoader {
state: Cell::new(SplitLoaderState::Deferred(load)),
waker: Cell::new(None),
fn new(load: LoadFn) -> Arc<Self> {
Arc::new(SplitLoader {
state: Mutex::new(SplitLoaderState::Deferred(load)),
waker: Mutex::new(None),
})
}
fn complete(&self, value: bool) {
self.state.set(SplitLoaderState::Completed(if value {
Some(())
} else {
None
}));
if let Some(waker) = self.waker.take() {
*self.state.lock().or_poisoned() =
SplitLoaderState::Completed(if value { Some(()) } else { None });
if let Some(waker) = self.waker.lock().or_poisoned().take() {
waker.wake();
}
}
}
struct SplitLoaderFuture {
loader: Rc<SplitLoader>,
loader: Arc<SplitLoader>,
}
impl SplitLoaderFuture {
fn new(loader: Rc<SplitLoader>) -> Self {
fn new(loader: Arc<SplitLoader>) -> Self {
SplitLoaderFuture { loader }
}
}
@@ -80,21 +77,24 @@ impl Future for SplitLoaderFuture {
type Output = Option<()>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<()>> {
match self.loader.state.get() {
let mut loader = self.loader.state.lock().or_poisoned();
match *loader {
SplitLoaderState::Deferred(load) => {
self.loader.state.set(SplitLoaderState::Pending);
self.loader.waker.set(Some(cx.waker().clone()));
*loader = SplitLoaderState::Pending;
*self.loader.waker.lock().or_poisoned() =
Some(cx.waker().clone());
unsafe {
load(
load_callback,
Rc::<SplitLoader>::into_raw(self.loader.clone())
Arc::<SplitLoader>::into_raw(self.loader.clone())
as *const c_void,
)
};
Poll::Pending
}
SplitLoaderState::Pending => {
self.loader.waker.set(Some(cx.waker().clone()));
*self.loader.waker.lock().or_poisoned() =
Some(cx.waker().clone());
Poll::Pending
}
SplitLoaderState::Completed(value) => Poll::Ready(value),
@@ -103,5 +103,5 @@ impl Future for SplitLoaderFuture {
}
unsafe extern "C" fn load_callback(loader: *const c_void, success: bool) {
unsafe { Rc::from_raw(loader as *const SplitLoader) }.complete(success);
unsafe { Arc::from_raw(loader as *const SplitLoader) }.complete(success);
}

View File

@@ -1,11 +1,40 @@
use digest::Digest;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Ident, ItemFn, ReturnType, Signature};
use syn::{
parse,
parse::{Parse, ParseStream},
parse_macro_input,
token::Comma,
Ident, ItemFn, Path, Result, ReturnType, Signature,
};
struct WasmSplitArgs {
module_ident: Ident,
_comma: Option<Comma>,
send_wrapper_path: Option<Path>,
}
impl Parse for WasmSplitArgs {
fn parse(input: ParseStream) -> Result<Self> {
let module_ident = input.parse()?;
let _comma = input.parse().ok();
let send_wrapper_path = input.parse().ok();
Ok(Self {
module_ident,
_comma,
send_wrapper_path,
})
}
}
#[proc_macro_attribute]
pub fn wasm_split(args: TokenStream, input: TokenStream) -> TokenStream {
let module_ident = parse_macro_input!(args as Ident);
let WasmSplitArgs {
module_ident,
send_wrapper_path,
..
} = parse_macro_input!(args);
let item_fn = parse_macro_input!(input as ItemFn);
let name = &item_fn.sig.ident;
@@ -45,9 +74,9 @@ pub fn wasm_split(args: TokenStream, input: TokenStream) -> TokenStream {
ReturnType::Default => quote! { () },
ReturnType::Type(_, ty) => quote! { #ty },
};
let async_output = syn::parse::<ReturnType>(
let async_output = parse::<ReturnType>(
quote! {
-> std::pin::Pin<Box<dyn std::future::Future<Output = #ty>>>
-> std::pin::Pin<Box<dyn std::future::Future<Output = #ty> + Send + Sync>>
}
.into(),
)
@@ -83,10 +112,18 @@ pub fn wasm_split(args: TokenStream, input: TokenStream) -> TokenStream {
let stmts = &item_fn.block.stmts;
let body = if was_async {
quote! {
Box::pin(async move {
#(#stmts)*
})
if let Some(send_wrapper_path) = send_wrapper_path {
quote! {
Box::pin(#send_wrapper_path::SendWrapper::new(async move {
#(#stmts)*
}))
}
} else {
quote! {
Box::pin(async move {
#(#stmts)*
})
}
}
} else {
quote! { #(#stmts)* }