perseus_macro/lib.rs
1#![doc = include_str!("../README.proj.md")]
2/*!
3## Features
4
5- `live-reload` -- enables reloading the browser automatically when you make changes to your app
6- `hsr` -- enables *hot state reloading*, which reloads the state of your app right before you made code changes in development, allowing you to pick up where you left off
7
8## Packages
9
10This is the API documentation for the `perseus-macro` package, which manages Perseus' procedural macros. Note that Perseus mostly uses [the book](https://framesurge.sh/perseus/en-US) for
11documentation, and this should mostly be used as a secondary reference source. You can also find full usage examples [here](https://github.com/framesurge/perseus/tree/main/examples).
12*/
13
14mod auto_scope;
15mod entrypoint;
16mod rx_state;
17mod test;
18
19use darling::{FromDeriveInput, FromMeta};
20use proc_macro::TokenStream;
21use quote::quote;
22use syn::{parse_macro_input, DeriveInput, ItemFn, Path, Signature};
23
24use crate::rx_state::ReactiveStateDeriveInput;
25
26/// A helper macro for templates that use reactive state. Once, this was needed
27/// on all Perseus templates, however, today, templates that take no state, or
28/// templates that take unreactive state, can be provided as normal functions
29/// to the methods `.view()` and `.view_with_unreactive_state()`
30/// respectively, on Perseus' `Template` type.
31///
32/// In fact, even if you're using fully reactive state, this macro isn't even
33/// mandated anymore! It just exists to turn function signatures like this
34///
35/// ```text
36/// fn my_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, state: &'a MyStateRx) -> View<G>
37/// ```
38///
39/// into this
40///
41/// ```text
42/// #[auto_scope]
43/// fn my_page<G: Html>(cx: Scope, state: &MyStateRx) -> View<G>
44/// ```
45///
46/// In other words, all this does is rewrites some lifetimes for you so Perseus
47/// is a little more convenient to use! It's worth remembering, however, when
48/// you use this macro, that the `Scope` is actually a `BoundedScope<'app,
49/// 'page>`, meaning it is a *child scope* of the whole app. Your state is a
50/// reference with the lifetime `'page`, which links to an owned type that the
51/// app controls. All this lifetime complexity is needed to make sure Rust
52/// understands that all your pages are part of your app, and that, when one of
53/// your users goes to a new page, the previous page will be dropped, along with
54/// all its artifacts (e.g. any `create_effect` calls). It also makes it really
55/// convenient to use your state, because we can prove to Sycamore that it will
56/// live long enough to be interpolated anywhere in your page's `view!`.
57///
58/// If you dislike macros, or if you want to make the lifetimes of a page very
59/// clear, it's recommended that you don't use this macro, and manually write
60/// the longer function signatures instead. However, if you like the convenience
61/// of it, this macro is here to help!
62///
63/// *Note: this can also be used for capsules that take reactive state, it's not
64/// just limited to templates.*
65#[proc_macro_attribute]
66pub fn auto_scope(_args: TokenStream, input: TokenStream) -> TokenStream {
67 let parsed = syn::parse_macro_input!(input as auto_scope::TemplateFn);
68 auto_scope::template_impl(parsed).into()
69}
70
71/// Marks the given function as a Perseus test. Functions marked with this
72/// attribute must have the following signature: `async fn foo(client: &mut
73/// fantoccini::Client) -> Result<>`.
74#[proc_macro_attribute]
75pub fn test(args: TokenStream, input: TokenStream) -> TokenStream {
76 let parsed = syn::parse_macro_input!(input as test::TestFn);
77 let attr_args = syn::parse_macro_input!(args as syn::AttributeArgs);
78 // Parse macro arguments with `darling`
79 let args = match test::TestArgs::from_list(&attr_args) {
80 Ok(v) => v,
81 Err(e) => {
82 return TokenStream::from(e.write_errors());
83 }
84 };
85
86 test::test_impl(parsed, args).into()
87}
88
89/// Marks the given function as the universal entrypoint into your app. This is
90/// designed for simple use-cases, and the annotated function should return
91/// a `PerseusApp`. This will expand into separate `main()` functions for both
92/// the browser and engine sides.
93///
94/// This should take an argument for the function that will produce your server.
95/// In most apps using this macro (which is designed for simple use-cases), this
96/// will just be something like `perseus_axum::dflt_server` (with `perseus-warp`
97/// as a dependency with the `dflt-server` feature enabled).
98///
99/// Note that the `dflt-engine` and `client-helpers` features must be enabled on
100/// `perseus` for this to work. (These are enabled by default.)
101///
102/// Note further that you'll need to have `wasm-bindgen` as a dependency to use
103/// this.
104#[proc_macro_attribute]
105pub fn main(args: TokenStream, input: TokenStream) -> TokenStream {
106 let parsed = syn::parse_macro_input!(input as entrypoint::MainFn);
107 let args = syn::parse_macro_input!(args as Path);
108
109 entrypoint::main_impl(parsed, args).into()
110}
111
112/// This is identical to `#[main]`, except it doesn't require a server
113/// integration, because it sets your app up for exporting only. This is useful
114/// for apps not using server-requiring features (like incremental static
115/// generation and revalidation) that want to avoid bringing in another
116/// dependency on the server-side.
117#[proc_macro_attribute]
118pub fn main_export(_args: TokenStream, input: TokenStream) -> TokenStream {
119 let parsed = syn::parse_macro_input!(input as entrypoint::MainFn);
120
121 entrypoint::main_export_impl(parsed).into()
122}
123
124/// Marks the given function as the browser entrypoint into your app. This is
125/// designed for more complex apps that need to manually distinguish between the
126/// engine and browser entrypoints.
127///
128/// If you just want to run some simple customizations, you should probably use
129/// `perseus::run_client` to use the default client logic after you've made your
130/// modifications. `perseus::ClientReturn` should be your return type no matter
131/// what.
132///
133/// Note that any generics on the annotated function will not be preserved. You
134/// should put the `PerseusApp` generator in a separate function.
135///
136/// Note further that you'll need to have `wasm-bindgen` as a dependency to use
137/// this.
138#[proc_macro_attribute]
139pub fn browser_main(_args: TokenStream, input: TokenStream) -> TokenStream {
140 let parsed = syn::parse_macro_input!(input as entrypoint::MainFn);
141
142 entrypoint::browser_main_impl(parsed).into()
143}
144
145/// Marks the given function as the engine entrypoint into your app. This is
146/// designed for more complex apps that need to manually distinguish between the
147/// engine and browser entrypoints.
148///
149/// If you just want to run some simple customizations, you should probably use
150/// `perseus::run_dflt_engine` with `perseus::builder::get_op` to use the
151/// default client logic after you've made your modifications. You'll also want
152/// to return an exit code from this function (use `std::process:exit(..)`).
153///
154/// Note that the `dflt-engine` and `client-helpers` features must be enabled on
155/// `perseus` for this to work. (These are enabled by default.)
156///
157/// Note further that you'll need to have `tokio` as a dependency to use this.
158///
159/// Finally, note that any generics on the annotated function will not be
160/// preserved. You should put the `PerseusApp` generator in a separate function.
161#[proc_macro_attribute]
162pub fn engine_main(_args: TokenStream, input: TokenStream) -> TokenStream {
163 let parsed = syn::parse_macro_input!(input as entrypoint::EngineMainFn);
164
165 entrypoint::engine_main_impl(parsed).into()
166}
167
168/// Processes the given `struct` to create a reactive version by wrapping each
169/// field in a `Signal`. This will generate a new `struct` with the given name
170/// and implement a `.make_rx()` method on the original that allows turning an
171/// instance of the unreactive `struct` into an instance of the reactive one.
172///
173/// If one of your fields is itself a `struct`, by default it will just be
174/// wrapped in a `Signal`, but you can also enable nested fine-grained
175/// reactivity by adding the `#[rx(nested)]` helper macro to the field.
176/// Fields that have nested reactivity should also use this derive macro.
177#[proc_macro_derive(ReactiveState, attributes(rx))]
178pub fn reactive_state(input: TokenStream) -> TokenStream {
179 let input = match ReactiveStateDeriveInput::from_derive_input(&syn::parse_macro_input!(
180 input as DeriveInput
181 )) {
182 Ok(input) => input,
183 Err(err) => return err.write_errors().into(),
184 };
185
186 rx_state::make_rx_impl(input).into()
187}
188
189/// A convenience macro that makes sure the given function is only defined on
190/// the engine-side, creating an empty function on the browser-side. Perseus
191/// implicitly expects most of your state generation functions to be defined in
192/// this way (though you certainly don't have to use this macro).
193///
194/// Note that this will convert `async` functions to non-`async` functions on
195/// the browser-side (your function will be left alone on the engine-side).
196#[proc_macro_attribute]
197pub fn engine_only_fn(_args: TokenStream, input: TokenStream) -> TokenStream {
198 let input_2: proc_macro2::TokenStream = input.clone().into();
199 let ItemFn {
200 vis,
201 sig: Signature { ident, .. },
202 ..
203 } = parse_macro_input!(input as ItemFn);
204
205 quote! {
206 #[cfg(client)]
207 #vis fn #ident () {}
208 // On the engine-side, the function is unmodified
209 #[cfg(engine)]
210 #input_2
211 }
212 .into()
213}
214
215/// A convenience macro that makes sure the given function is only defined on
216/// the browser-side, creating an empty function on the engine-side. Perseus
217/// implicitly expects your browser-side state modification functions to be
218/// defined in this way (though you certainly don't have to use this macro).
219///
220/// Note that this will convert `async` functions to non-`async` functions on
221/// the engine-side (your function will be left alone on the browser-side).
222#[proc_macro_attribute]
223pub fn browser_only_fn(_args: TokenStream, input: TokenStream) -> TokenStream {
224 let input_2: proc_macro2::TokenStream = input.clone().into();
225 let ItemFn {
226 vis,
227 sig: Signature { ident, .. },
228 ..
229 } = parse_macro_input!(input as ItemFn);
230
231 quote! {
232 #[cfg(engine)]
233 #vis fn #ident () {}
234 // One the browser-side, the function is unmodified
235 #[cfg(client)]
236 #input_2
237 }
238 .into()
239}
240
241#[proc_macro_derive(UnreactiveState)]
242pub fn unreactive_state(input: TokenStream) -> TokenStream {
243 let input = syn::parse_macro_input!(input as DeriveInput);
244 let name = input.ident;
245
246 // This is a marker trait, so we barely have to do anything here
247 quote! {
248 impl ::perseus::state::UnreactiveState for #name {}
249 }
250 .into()
251}