2
0
Fork 0
mirror of https://github.com/leptos-rs/leptos synced 2025-02-02 23:13:25 +00:00

feat: #[lazy] macros to support lazy loading and code splitting ()

This commit is contained in:
Greg Johnston 2025-01-17 15:15:12 -05:00 committed by GitHub
parent 1c3e013a63
commit 22b2d8ec84
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 135 additions and 2 deletions
Cargo.lock
leptos_macro/src
router_macro

2
Cargo.lock generated
View file

@ -1999,10 +1999,12 @@ dependencies = [
name = "leptos_router_macro"
version = "0.7.4"
dependencies = [
"leptos_macro",
"leptos_router",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]]

32
leptos_macro/src/lazy.rs Normal file
View file

@ -0,0 +1,32 @@
use convert_case::{Case, Casing};
use proc_macro::TokenStream;
use proc_macro2::Ident;
use proc_macro_error2::abort;
use quote::quote;
use syn::{spanned::Spanned, ItemFn};
pub fn lazy_impl(
_args: proc_macro::TokenStream,
s: TokenStream,
) -> TokenStream {
let fun = syn::parse::<ItemFn>(s).unwrap_or_else(|e| {
abort!(e.span(), "`lazy` can only be used on a function")
});
if fun.sig.asyncness.is_none() {
abort!(
fun.sig.asyncness.span(),
"`lazy` can only be used on an async function"
)
}
let converted_name = Ident::new(
&fun.sig.ident.to_string().to_case(Case::Snake),
fun.sig.ident.span(),
);
quote! {
#[cfg_attr(feature = "split", wasm_split::wasm_split(#converted_name))]
#fun
}
.into()
}

View file

@ -23,6 +23,7 @@ mod params;
mod view;
use crate::component::unmodified_fn_name_from_fn_name;
mod component;
mod lazy;
mod memo;
mod slice;
mod slot;
@ -1002,3 +1003,17 @@ pub fn slice(input: TokenStream) -> TokenStream {
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 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
/// asynchronously to maintain the same API.
///
/// All parameters and output types should be concrete types, with no generics.
#[proc_macro_attribute]
#[proc_macro_error]
pub fn lazy(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
lazy::lazy_impl(args, s)
}

View file

@ -16,9 +16,11 @@ proc-macro = true
proc-macro-error2 = { version = "2.0", default-features = false }
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
[dev-dependencies]
leptos_router = { path = "../router" }
leptos_macro = { path = "../leptos_macro" }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View file

@ -1,16 +1,21 @@
//! A macro to make path definitions easier with [`leptos_router`].
//!
//! [`leptos_router`]: https://docs.rs/leptos_router/latest/leptos_router/components/fn.Route.html
#![deny(missing_docs)]
use proc_macro::{TokenStream, TokenTree};
use proc_macro2::Span;
use proc_macro_error2::abort;
use proc_macro_error2::{abort, proc_macro_error};
use quote::{quote, ToTokens};
use syn::{
spanned::Spanned, Block, Ident, ImplItem, ItemImpl, Path, Type, TypePath,
};
const RFC3986_UNRESERVED: [char; 4] = ['-', '.', '_', '~'];
const RFC3986_PCHAR_OTHER: [char; 1] = ['@'];
/// Constructs a path for use in a [`leptos_router::Route`] definition.
/// Constructs a path for use in a [`Route`] definition.
///
/// Note that this is an optional convenience. Manually defining route segments
/// is equivalent.
@ -33,6 +38,7 @@ const RFC3986_PCHAR_OTHER: [char; 1] = ['@'];
///
/// assert_eq!(path, output);
/// ```
/// [`Route`]: https://docs.rs/leptos_router/latest/leptos_router/components/fn.Route.html
#[proc_macro_error2::proc_macro_error]
#[proc_macro]
pub fn path(tokens: TokenStream) -> TokenStream {
@ -187,3 +193,79 @@ impl ToTokens for Segments {
}
}
}
/// When added to an [`impl LazyRoute`] implementation block, this will automatically
/// 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.
///
/// [`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]
#[proc_macro_error]
pub fn lazy_route(
args: proc_macro::TokenStream,
s: TokenStream,
) -> TokenStream {
lazy_route_impl(args, s)
}
fn lazy_route_impl(
_args: proc_macro::TokenStream,
s: TokenStream,
) -> TokenStream {
let mut im = syn::parse::<ItemImpl>(s).unwrap_or_else(|e| {
abort!(e.span(), "`lazy_route` can only be used on an `impl` block")
});
if im.trait_.is_none() {
abort!(
im.span(),
"`lazy_route` can only be used on an `impl LazyRoute for ...` \
block"
)
}
let self_ty = im.self_ty.clone();
let ty_name_to_snake = match &*self_ty {
Type::Path(TypePath {
path: Path { segments, .. },
..
}) => segments.last().unwrap().ident.to_string(),
_ => abort!(self_ty.span(), "only path types are supported"),
};
let lazy_view_ident = Ident::new(&ty_name_to_snake, im.self_ty.span());
let item = im.items.iter_mut().find_map(|item| match item {
ImplItem::Fn(inner) => {
if inner.sig.ident.to_string().as_str() == "view" {
Some(inner)
} else {
None
}
}
_ => None,
});
match item {
None => abort!(im.span(), "must contain a fn called `view`"),
Some(fun) => {
let body = fun.block.clone();
let new_block = quote! {{
#[cfg_attr(feature = "split", wasm_split::wasm_split(#lazy_view_ident))]
async fn view(this: #self_ty) -> ::leptos::prelude::AnyView {
#body
}
view(self).await
}};
let block =
syn::parse::<Block>(new_block.into()).unwrap_or_else(|e| {
abort!(
e.span(),
"`lazy_route` can only be used on an `impl` block"
)
});
fun.block = block;
}
}
quote! { #im }.into()
}