diff --git a/docs/0.4.x/en-US/tutorials/second-app.md b/docs/0.4.x/en-US/tutorials/second-app.md index 6c68b05729..04a291c6cc 100644 --- a/docs/0.4.x/en-US/tutorials/second-app.md +++ b/docs/0.4.x/en-US/tutorials/second-app.md @@ -60,7 +60,7 @@ The first thing then is `IndexPageState`, which is our first usage of Perseus' s Importantly, we've annotated that with `#[perseus::make_rx(IndexPageStateRx)]`, which will create a version of this `struct` that uses Sycamore's `Signal`s: a reactive version. If you're unfamiliar with Sycamore's reactivity system, you should read [this](https://sycamore-rs.netlify.app/docs/basics/reactivity) quickly before continuing. -Next, we create a function called `index_page`, which we annotate with `#[perseus::template_rx]`. That macro is used for declaring templates, and you can think of it like black box that makes things work. +Next, we create a function called `index_page`, which we annotate with `#[perseus::template]`. That macro is used for declaring templates, and you can think of it like black box that makes things work.
Details?? @@ -94,7 +94,7 @@ Notably, we could actually change this value at runtime if we wanted by calling The next function we define is `get_template()`, which is fairly straightforward. It just declares a [`Template`](=struct.Template@perseus) with the necessary properties. Specifically, we define the function that actually renders the template as `index_page`, and the other two we'll get to now. -The first of those is the `head()` function, which is annotated as `#[perseus::head]` (which has similar responsibilities to `#[perseus::template_rx]`). In HTML, the language for creating views on the web, there are two main components to every page: the `` and the ``, the latter of which defines certain metadata, like the title, and any stylesheets you need, for example. If `index_page()` creates the body, then `head()` creates the head in this example. Notably, because the head is rendered only ahead of time, it can't be reactive. For that reason, it's passed the unreactive version of the state, and, rather than being generic over `Html`, it uses [`SsrNode`](=struct.SsrNode@perseus), which is specialized for the engine-side. +The first of those is the `head()` function, which is annotated as `#[perseus::head]` (which has similar responsibilities to `#[perseus::template]`). In HTML, the language for creating views on the web, there are two main components to every page: the `` and the ``, the latter of which defines certain metadata, like the title, and any stylesheets you need, for example. If `index_page()` creates the body, then `head()` creates the head in this example. Notably, because the head is rendered only ahead of time, it can't be reactive. For that reason, it's passed the unreactive version of the state, and, rather than being generic over `Html`, it uses [`SsrNode`](=struct.SsrNode@perseus), which is specialized for the engine-side. Because this function will only ever run on the engine-side, `#[perseus::head]` implies a target-gate to the engine (i.e. `#[cfg(not(target_arch = "wasm32"))]` is implicit). This means you can use engine-side dependencies here without any extra gating. @@ -123,7 +123,7 @@ With that done, we can build the second template of this app, which is much simp This is basically a simpler version of the index template, with no state, and this template only defines a simple view and some metadata in the head. -Importantly, this illustrates that templates that don't take state don't have to have a second argument for their nonexistent state, the `#[perseus::template_rx]` macro is smart enough to handle that (and even a third argument for global state). +Importantly, this illustrates that templates that don't take state don't have to have a second argument for their nonexistent state, the `#[perseus::template]` macro is smart enough to handle that (and even a third argument for global state). ## Error pages diff --git a/docs/next/en-US/tutorials/second-app.md b/docs/next/en-US/tutorials/second-app.md index 6c68b05729..04a291c6cc 100644 --- a/docs/next/en-US/tutorials/second-app.md +++ b/docs/next/en-US/tutorials/second-app.md @@ -60,7 +60,7 @@ The first thing then is `IndexPageState`, which is our first usage of Perseus' s Importantly, we've annotated that with `#[perseus::make_rx(IndexPageStateRx)]`, which will create a version of this `struct` that uses Sycamore's `Signal`s: a reactive version. If you're unfamiliar with Sycamore's reactivity system, you should read [this](https://sycamore-rs.netlify.app/docs/basics/reactivity) quickly before continuing. -Next, we create a function called `index_page`, which we annotate with `#[perseus::template_rx]`. That macro is used for declaring templates, and you can think of it like black box that makes things work. +Next, we create a function called `index_page`, which we annotate with `#[perseus::template]`. That macro is used for declaring templates, and you can think of it like black box that makes things work.
Details?? @@ -94,7 +94,7 @@ Notably, we could actually change this value at runtime if we wanted by calling The next function we define is `get_template()`, which is fairly straightforward. It just declares a [`Template`](=struct.Template@perseus) with the necessary properties. Specifically, we define the function that actually renders the template as `index_page`, and the other two we'll get to now. -The first of those is the `head()` function, which is annotated as `#[perseus::head]` (which has similar responsibilities to `#[perseus::template_rx]`). In HTML, the language for creating views on the web, there are two main components to every page: the `` and the ``, the latter of which defines certain metadata, like the title, and any stylesheets you need, for example. If `index_page()` creates the body, then `head()` creates the head in this example. Notably, because the head is rendered only ahead of time, it can't be reactive. For that reason, it's passed the unreactive version of the state, and, rather than being generic over `Html`, it uses [`SsrNode`](=struct.SsrNode@perseus), which is specialized for the engine-side. +The first of those is the `head()` function, which is annotated as `#[perseus::head]` (which has similar responsibilities to `#[perseus::template]`). In HTML, the language for creating views on the web, there are two main components to every page: the `` and the ``, the latter of which defines certain metadata, like the title, and any stylesheets you need, for example. If `index_page()` creates the body, then `head()` creates the head in this example. Notably, because the head is rendered only ahead of time, it can't be reactive. For that reason, it's passed the unreactive version of the state, and, rather than being generic over `Html`, it uses [`SsrNode`](=struct.SsrNode@perseus), which is specialized for the engine-side. Because this function will only ever run on the engine-side, `#[perseus::head]` implies a target-gate to the engine (i.e. `#[cfg(not(target_arch = "wasm32"))]` is implicit). This means you can use engine-side dependencies here without any extra gating. @@ -123,7 +123,7 @@ With that done, we can build the second template of this app, which is much simp This is basically a simpler version of the index template, with no state, and this template only defines a simple view and some metadata in the head. -Importantly, this illustrates that templates that don't take state don't have to have a second argument for their nonexistent state, the `#[perseus::template_rx]` macro is smart enough to handle that (and even a third argument for global state). +Importantly, this illustrates that templates that don't take state don't have to have a second argument for their nonexistent state, the `#[perseus::template]` macro is smart enough to handle that (and even a third argument for global state). ## Error pages diff --git a/examples/core/basic/src/templates/about.rs b/examples/core/basic/src/templates/about.rs index f7b3dd6492..7614743c8c 100644 --- a/examples/core/basic/src/templates/about.rs +++ b/examples/core/basic/src/templates/about.rs @@ -1,7 +1,7 @@ use perseus::Template; use sycamore::prelude::{view, Html, Scope, SsrNode, View}; -#[perseus::template_rx] +#[perseus::template] pub fn about_page(cx: Scope) -> View { view! { cx, p { "About." } diff --git a/examples/core/basic/src/templates/index.rs b/examples/core/basic/src/templates/index.rs index 10082b7f02..160d251e51 100644 --- a/examples/core/basic/src/templates/index.rs +++ b/examples/core/basic/src/templates/index.rs @@ -6,7 +6,7 @@ pub struct IndexPageState { pub greeting: String, } -#[perseus::template_rx] +#[perseus::template] pub fn index_page<'a, G: Html>(cx: Scope<'a>, state: IndexPageStateRx<'a>) -> View { view! { cx, p { (state.greeting.get()) } diff --git a/examples/core/custom_server/src/templates/about.rs b/examples/core/custom_server/src/templates/about.rs index 06184e39a6..a85f96122c 100644 --- a/examples/core/custom_server/src/templates/about.rs +++ b/examples/core/custom_server/src/templates/about.rs @@ -1,7 +1,7 @@ use perseus::Template; use sycamore::prelude::{view, Html, Scope, View}; -#[perseus::template_rx] +#[perseus::template] pub fn about_page(cx: Scope) -> View { view! { cx, p { "About." } diff --git a/examples/core/custom_server/src/templates/index.rs b/examples/core/custom_server/src/templates/index.rs index ccc5bdbfba..7530a77604 100644 --- a/examples/core/custom_server/src/templates/index.rs +++ b/examples/core/custom_server/src/templates/index.rs @@ -1,7 +1,7 @@ use perseus::{Html, Template}; use sycamore::prelude::{view, Scope, View}; -#[perseus::template_rx] +#[perseus::template] pub fn index_page<'a, G: Html>(cx: Scope<'a>) -> View { view! { cx, p { "Hello World!" } diff --git a/examples/core/freezing_and_thawing/src/templates/about.rs b/examples/core/freezing_and_thawing/src/templates/about.rs index 02b0eb8a2e..041094f59a 100644 --- a/examples/core/freezing_and_thawing/src/templates/about.rs +++ b/examples/core/freezing_and_thawing/src/templates/about.rs @@ -4,7 +4,7 @@ use sycamore::prelude::*; use crate::global_state::AppStateRx; -#[perseus::template_rx] +#[perseus::template] pub fn about_page<'a, G: Html>(cx: Scope<'a>) -> View { // This is not part of our data model, we do NOT want the frozen app // synchronized as part of our page's state, it should be separate diff --git a/examples/core/freezing_and_thawing/src/templates/index.rs b/examples/core/freezing_and_thawing/src/templates/index.rs index f9e1fa37ac..f3d362ce79 100644 --- a/examples/core/freezing_and_thawing/src/templates/index.rs +++ b/examples/core/freezing_and_thawing/src/templates/index.rs @@ -9,7 +9,7 @@ pub struct IndexProps { username: String, } -#[perseus::template_rx] +#[perseus::template] pub fn index_page<'a, G: Html>(cx: Scope<'a>, state: IndexPropsRx<'a>) -> View { // This is not part of our data model, we do NOT want the frozen app // synchronized as part of our page's state, it should be separate diff --git a/examples/core/global_state/src/templates/about.rs b/examples/core/global_state/src/templates/about.rs index cf4ba1515a..0fcb710333 100644 --- a/examples/core/global_state/src/templates/about.rs +++ b/examples/core/global_state/src/templates/about.rs @@ -3,7 +3,7 @@ use sycamore::prelude::*; use crate::global_state::AppStateRx; -#[perseus::template_rx] +#[perseus::template] pub fn about_page<'a, G: Html>(cx: Scope<'a>) -> View { let global_state = RenderCtx::from_ctx(cx).get_global_state::(cx); diff --git a/examples/core/global_state/src/templates/index.rs b/examples/core/global_state/src/templates/index.rs index 3e52e7fe58..fa124e53f7 100644 --- a/examples/core/global_state/src/templates/index.rs +++ b/examples/core/global_state/src/templates/index.rs @@ -5,7 +5,7 @@ use crate::global_state::AppStateRx; // Note that this template takes no state of its own in this example, but it // certainly could -#[perseus::template_rx] +#[perseus::template] pub fn index_page<'a, G: Html>(cx: Scope<'a>) -> View { // We access the global state through the render context, extracted from // Sycamore's context system diff --git a/examples/core/i18n/src/templates/about.rs b/examples/core/i18n/src/templates/about.rs index 6138778e2e..2ad1a2b55c 100644 --- a/examples/core/i18n/src/templates/about.rs +++ b/examples/core/i18n/src/templates/about.rs @@ -1,7 +1,7 @@ use perseus::{t, Template}; use sycamore::prelude::{view, Html, Scope, View}; -#[perseus::template_rx] +#[perseus::template] pub fn about_page(cx: Scope) -> View { view! { cx, p { (t!("about", cx)) } diff --git a/examples/core/i18n/src/templates/index.rs b/examples/core/i18n/src/templates/index.rs index f7dfe8a630..adc270bd7d 100644 --- a/examples/core/i18n/src/templates/index.rs +++ b/examples/core/i18n/src/templates/index.rs @@ -1,7 +1,7 @@ use perseus::{link, t, Template}; use sycamore::prelude::{view, Html, Scope, View}; -#[perseus::template_rx] +#[perseus::template] pub fn index_page(cx: Scope) -> View { let username = "User"; diff --git a/examples/core/i18n/src/templates/post.rs b/examples/core/i18n/src/templates/post.rs index a95520bc32..ab594feb2a 100644 --- a/examples/core/i18n/src/templates/post.rs +++ b/examples/core/i18n/src/templates/post.rs @@ -7,7 +7,7 @@ pub struct PostPageState { content: String, } -#[perseus::template_rx] +#[perseus::template] pub fn post_page<'a, G: Html>(cx: Scope<'a>, props: PostPageStateRx<'a>) -> View { let title = props.title; let content = props.content; diff --git a/examples/core/idb_freezing/src/templates/about.rs b/examples/core/idb_freezing/src/templates/about.rs index d0950a6fbd..7d4706a8d8 100644 --- a/examples/core/idb_freezing/src/templates/about.rs +++ b/examples/core/idb_freezing/src/templates/about.rs @@ -3,7 +3,7 @@ use sycamore::prelude::*; use crate::global_state::AppStateRx; -#[perseus::template_rx] +#[perseus::template] pub fn about_page<'a, G: Html>(cx: Scope<'a>) -> View { // This is not part of our data model let freeze_status = create_signal(cx, String::new()); diff --git a/examples/core/idb_freezing/src/templates/index.rs b/examples/core/idb_freezing/src/templates/index.rs index ee522befd2..30ba843684 100644 --- a/examples/core/idb_freezing/src/templates/index.rs +++ b/examples/core/idb_freezing/src/templates/index.rs @@ -8,7 +8,7 @@ pub struct IndexProps { username: String, } -#[perseus::template_rx] +#[perseus::template] pub fn index_page<'a, G: Html>(cx: Scope<'a>, state: IndexPropsRx<'a>) -> View { // This is not part of our data model let freeze_status = create_signal(cx, String::new()); diff --git a/examples/core/index_view/src/templates/about.rs b/examples/core/index_view/src/templates/about.rs index 8fb692f195..f8519274a4 100644 --- a/examples/core/index_view/src/templates/about.rs +++ b/examples/core/index_view/src/templates/about.rs @@ -1,7 +1,7 @@ use perseus::Template; use sycamore::prelude::{view, Html, Scope, View}; -#[perseus::template_rx] +#[perseus::template] pub fn about_page(cx: Scope) -> View { view! { cx, p { "About." } diff --git a/examples/core/index_view/src/templates/index.rs b/examples/core/index_view/src/templates/index.rs index 368b3e61df..49a1c5e94d 100644 --- a/examples/core/index_view/src/templates/index.rs +++ b/examples/core/index_view/src/templates/index.rs @@ -1,7 +1,7 @@ use perseus::{Html, Template}; use sycamore::prelude::{view, Scope, View}; -#[perseus::template_rx] +#[perseus::template] pub fn index_page(cx: Scope) -> View { view! { cx, p { "Hello World!" } diff --git a/examples/core/js_interop/src/templates/index.rs b/examples/core/js_interop/src/templates/index.rs index 02ba086982..0aa6f9c8bb 100644 --- a/examples/core/js_interop/src/templates/index.rs +++ b/examples/core/js_interop/src/templates/index.rs @@ -3,7 +3,7 @@ use sycamore::prelude::{view, Scope, View}; #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::wasm_bindgen; -#[perseus::template_rx] +#[perseus::template] pub fn index_page<'a, G: Html>(cx: Scope<'a>) -> View { view! { cx, // We'll use JS to change this message manually diff --git a/examples/core/plugins/src/templates/about.rs b/examples/core/plugins/src/templates/about.rs index 730e061038..1f19d25de0 100644 --- a/examples/core/plugins/src/templates/about.rs +++ b/examples/core/plugins/src/templates/about.rs @@ -2,7 +2,7 @@ use perseus::Template; use sycamore::prelude::{view, Html, Scope, SsrNode, View}; // This page will actually be replaced entirely by a plugin! -#[perseus::template_rx] +#[perseus::template] pub fn about_page(cx: Scope) -> View { view! { cx, p { "About." } diff --git a/examples/core/plugins/src/templates/index.rs b/examples/core/plugins/src/templates/index.rs index 58f821ca70..9803200603 100644 --- a/examples/core/plugins/src/templates/index.rs +++ b/examples/core/plugins/src/templates/index.rs @@ -1,7 +1,7 @@ use perseus::{Html, Template}; use sycamore::prelude::{view, Scope, SsrNode, View}; -#[perseus::template_rx] +#[perseus::template] pub fn index_page(cx: Scope) -> View { view! { cx, p { "Hello World!" } diff --git a/examples/core/preload/src/templates/about.rs b/examples/core/preload/src/templates/about.rs index 7360b31a4b..bbe83eb102 100644 --- a/examples/core/preload/src/templates/about.rs +++ b/examples/core/preload/src/templates/about.rs @@ -2,7 +2,7 @@ use perseus::{Html, Template}; use sycamore::prelude::{view, Scope}; use sycamore::view::View; -#[perseus::template_rx] +#[perseus::template] pub fn about_page(cx: Scope) -> View { view! { cx, p { "Check out your browser's network DevTools, no new requests were needed to get to this page!" } diff --git a/examples/core/preload/src/templates/index.rs b/examples/core/preload/src/templates/index.rs index 25cd56ba71..a71584f01f 100644 --- a/examples/core/preload/src/templates/index.rs +++ b/examples/core/preload/src/templates/index.rs @@ -1,7 +1,7 @@ use perseus::Template; use sycamore::prelude::{view, Html, Scope, SsrNode, View}; -#[perseus::template_rx] +#[perseus::template] pub fn index_page(cx: Scope) -> View { // We can't preload pages on the engine-side #[cfg(target_arch = "wasm32")] diff --git a/examples/core/router_state/src/templates/about.rs b/examples/core/router_state/src/templates/about.rs index cf556e3303..a2d44a833a 100644 --- a/examples/core/router_state/src/templates/about.rs +++ b/examples/core/router_state/src/templates/about.rs @@ -1,7 +1,7 @@ use perseus::{Html, Template}; use sycamore::prelude::{view, Scope, SsrNode, View}; -#[perseus::template_rx] +#[perseus::template] pub fn about_page(cx: Scope) -> View { view! { cx, p { "Hello World!" } diff --git a/examples/core/router_state/src/templates/index.rs b/examples/core/router_state/src/templates/index.rs index 68977a2d7a..ad696c4c81 100644 --- a/examples/core/router_state/src/templates/index.rs +++ b/examples/core/router_state/src/templates/index.rs @@ -2,7 +2,7 @@ use perseus::prelude::*; use perseus::router::RouterLoadState; use sycamore::prelude::*; -#[perseus::template_rx] +#[perseus::template] pub fn router_state_page(cx: Scope) -> View { let load_state = RenderCtx::from_ctx(cx).router.get_load_state(cx); // This uses Sycamore's `create_memo` to create a state that will update diff --git a/examples/core/rx_state/src/templates/about.rs b/examples/core/rx_state/src/templates/about.rs index 8e7a2e732b..2599eedb2c 100644 --- a/examples/core/rx_state/src/templates/about.rs +++ b/examples/core/rx_state/src/templates/about.rs @@ -2,7 +2,7 @@ use perseus::{Html, Template}; use sycamore::prelude::{view, Scope}; use sycamore::view::View; -#[perseus::template_rx] +#[perseus::template] pub fn about_page(cx: Scope) -> View { view! { cx, p { "Try going back to the index page, and the state should still be the same!" } diff --git a/examples/core/rx_state/src/templates/index.rs b/examples/core/rx_state/src/templates/index.rs index 64ca850129..c8b2b018e3 100644 --- a/examples/core/rx_state/src/templates/index.rs +++ b/examples/core/rx_state/src/templates/index.rs @@ -9,7 +9,7 @@ pub struct IndexPageState { // This macro will make our state reactive *and* store it in the page state // store, which means it'll be the same even if we go to the about page and come // back (as long as we're in the same session) -#[perseus::template_rx] +#[perseus::template] pub fn index_page<'a, G: Html>(cx: Scope<'a>, state: IndexPageStateRx<'a>) -> View { view! { cx, p { (format!("Greetings, {}!", state.username.get())) } diff --git a/examples/core/set_headers/src/templates/index.rs b/examples/core/set_headers/src/templates/index.rs index 9e2ad71c2a..4a7085b7fc 100644 --- a/examples/core/set_headers/src/templates/index.rs +++ b/examples/core/set_headers/src/templates/index.rs @@ -6,7 +6,7 @@ struct PageState { greeting: String, } -#[perseus::template_rx] +#[perseus::template] pub fn index_page<'a, G: Html>(cx: Scope<'a>, state: PageStateRx<'a>) -> View { view! { cx, p { (state.greeting.get()) } diff --git a/examples/core/state_generation/src/templates/amalgamation.rs b/examples/core/state_generation/src/templates/amalgamation.rs index 1a9636623d..41bc31f843 100644 --- a/examples/core/state_generation/src/templates/amalgamation.rs +++ b/examples/core/state_generation/src/templates/amalgamation.rs @@ -8,7 +8,7 @@ pub struct PageState { pub message: String, } -#[perseus::template_rx] +#[perseus::template] pub fn amalgamation_page<'a, G: Html>(cx: Scope<'a>, state: PageStateRx<'a>) -> View { view! { cx, p { (format!("The message is: '{}'", state.message.get())) } diff --git a/examples/core/state_generation/src/templates/build_paths.rs b/examples/core/state_generation/src/templates/build_paths.rs index 7e38d04549..ef7574f0f4 100644 --- a/examples/core/state_generation/src/templates/build_paths.rs +++ b/examples/core/state_generation/src/templates/build_paths.rs @@ -7,7 +7,7 @@ pub struct PageState { content: String, } -#[perseus::template_rx] +#[perseus::template] pub fn build_paths_page<'a, G: Html>(cx: Scope<'a>, state: PageStateRx<'a>) -> View { let title = state.title; let content = state.content; diff --git a/examples/core/state_generation/src/templates/build_state.rs b/examples/core/state_generation/src/templates/build_state.rs index 477a104dc4..998825fe4d 100644 --- a/examples/core/state_generation/src/templates/build_state.rs +++ b/examples/core/state_generation/src/templates/build_state.rs @@ -6,7 +6,7 @@ pub struct PageState { pub greeting: String, } -#[perseus::template_rx] +#[perseus::template] pub fn build_state_page<'a, G: Html>(cx: Scope<'a>, state: PageStateRx<'a>) -> View { view! { cx, p { (state.greeting.get()) } diff --git a/examples/core/state_generation/src/templates/incremental_generation.rs b/examples/core/state_generation/src/templates/incremental_generation.rs index 19426c68ae..e84309b9ff 100644 --- a/examples/core/state_generation/src/templates/incremental_generation.rs +++ b/examples/core/state_generation/src/templates/incremental_generation.rs @@ -10,7 +10,7 @@ pub struct PageState { content: String, } -#[perseus::template_rx] +#[perseus::template] pub fn incremental_generation_page<'a, G: Html>(cx: Scope<'a>, state: PageStateRx<'a>) -> View { let title = state.title; let content = state.content; diff --git a/examples/core/state_generation/src/templates/request_state.rs b/examples/core/state_generation/src/templates/request_state.rs index 3ff483d084..2aeddf18aa 100644 --- a/examples/core/state_generation/src/templates/request_state.rs +++ b/examples/core/state_generation/src/templates/request_state.rs @@ -8,7 +8,7 @@ pub struct PageState { ip: String, } -#[perseus::template_rx] +#[perseus::template] pub fn request_state_page<'a, G: Html>(cx: Scope<'a>, state: PageStateRx<'a>) -> View { view! { cx, p { diff --git a/examples/core/state_generation/src/templates/revalidation.rs b/examples/core/state_generation/src/templates/revalidation.rs index c9e313af4c..4e5c93eb44 100644 --- a/examples/core/state_generation/src/templates/revalidation.rs +++ b/examples/core/state_generation/src/templates/revalidation.rs @@ -6,7 +6,7 @@ pub struct PageState { pub time: String, } -#[perseus::template_rx] +#[perseus::template] pub fn revalidation_page<'a, G: Html>(cx: Scope<'a>, state: PageStateRx<'a>) -> View { view! { cx, p { (format!("The time when this page was last rendered was '{}'.", state.time.get())) } diff --git a/examples/core/state_generation/src/templates/revalidation_and_incremental_generation.rs b/examples/core/state_generation/src/templates/revalidation_and_incremental_generation.rs index 07b8bdd599..3a51e427f9 100644 --- a/examples/core/state_generation/src/templates/revalidation_and_incremental_generation.rs +++ b/examples/core/state_generation/src/templates/revalidation_and_incremental_generation.rs @@ -10,7 +10,7 @@ pub struct PageState { pub time: String, } -#[perseus::template_rx] +#[perseus::template] pub fn revalidation_and_incremental_generation_page<'a, G: Html>( cx: Scope<'a>, state: PageStateRx<'a>, diff --git a/examples/core/static_content/src/templates/index.rs b/examples/core/static_content/src/templates/index.rs index a8772f75be..76a517d54e 100644 --- a/examples/core/static_content/src/templates/index.rs +++ b/examples/core/static_content/src/templates/index.rs @@ -1,7 +1,7 @@ use perseus::{Html, Template}; use sycamore::prelude::{view, Scope, SsrNode, View}; -#[perseus::template_rx] +#[perseus::template] pub fn index_page(cx: Scope) -> View { view! { cx, p { "Hello World!" } diff --git a/examples/core/unreactive/src/templates/about.rs b/examples/core/unreactive/src/templates/about.rs index c3c5de84ad..3eba8f7d65 100644 --- a/examples/core/unreactive/src/templates/about.rs +++ b/examples/core/unreactive/src/templates/about.rs @@ -1,12 +1,7 @@ -use perseus::{Html, SsrNode, Template}; -use sycamore::prelude::{view, Scope, View}; +use perseus::prelude::*; +use sycamore::prelude::*; -// With the old template macro, we have to add the Sycamore `#[component(...)]` -// annotation manually and we get unreactive state passed in Additionally, -// global state is not supported at all So there's no way of persisting state -// between templates -#[perseus::template] -#[sycamore::component] +#[perseus::template(unreactive)] pub fn about_page(cx: Scope) -> View { view! { cx, p { "About." } diff --git a/examples/core/unreactive/src/templates/index.rs b/examples/core/unreactive/src/templates/index.rs index 6671e14139..04265fbb0a 100644 --- a/examples/core/unreactive/src/templates/index.rs +++ b/examples/core/unreactive/src/templates/index.rs @@ -1,20 +1,23 @@ -use perseus::{Html, RenderFnResultWithCause, SsrNode, Template}; +use perseus::prelude::*; use serde::{Deserialize, Serialize}; -use sycamore::prelude::{view, Scope, View}; +use sycamore::prelude::*; // Without `#[make_rx(...)]`, we have to manually derive `Serialize` and // `Deserialize` -#[derive(Serialize, Deserialize)] +// We derive `UnreactiveState` too, which actually creates a pseudo-reactive +// wrapper for this unreactive type, allowing it to work with Perseus; +// rather strict state platform (this is just a marker trait though) +#[derive(Serialize, Deserialize, Clone, UnreactiveState)] pub struct IndexPageState { pub greeting: String, } -// With the old template macro, we have to add the Sycamore `#[component(...)]` -// annotation manually and we get unreactive state passed in Additionally, -// global state is not supported at all So there's no way of persisting state -// between templates -#[perseus::template] -#[sycamore::component] +// By adding `unreactive` in brackets, we tell Perseus to expect something with +// `UnreactiveState` derived. +// Otherwise, you can do everything in this macro that you can do with a +// reactive template! Caching, preloading, reactive global state, etc. are all +// supported. +#[perseus::template(unreactive)] pub fn index_page(cx: Scope, state: IndexPageState) -> View { view! { cx, p { (state.greeting) } diff --git a/examples/demos/auth/src/templates/about.rs b/examples/demos/auth/src/templates/about.rs index e5a73d69d5..84e5768e00 100644 --- a/examples/demos/auth/src/templates/about.rs +++ b/examples/demos/auth/src/templates/about.rs @@ -1,7 +1,7 @@ use perseus::Template; use sycamore::prelude::{view, Html, Scope, View}; -#[perseus::template_rx] +#[perseus::template] pub fn about_page(cx: Scope) -> View { view! { cx, p { "About." } diff --git a/examples/demos/auth/src/templates/index.rs b/examples/demos/auth/src/templates/index.rs index 75585f7c45..9a6019aeab 100644 --- a/examples/demos/auth/src/templates/index.rs +++ b/examples/demos/auth/src/templates/index.rs @@ -2,7 +2,7 @@ use crate::global_state::*; use perseus::prelude::*; use sycamore::prelude::*; -#[perseus::template_rx] +#[perseus::template] fn index_view<'a, G: Html>(cx: Scope<'a>) -> View { let AppStateRx { auth } = RenderCtx::from_ctx(cx).get_global_state::(cx); diff --git a/examples/demos/fetching/src/templates/index.rs b/examples/demos/fetching/src/templates/index.rs index 8c947d70fa..0390e3887c 100644 --- a/examples/demos/fetching/src/templates/index.rs +++ b/examples/demos/fetching/src/templates/index.rs @@ -7,7 +7,7 @@ pub struct IndexPageState { browser_ip: Option, } -#[perseus::template_rx] +#[perseus::template] pub fn index_page<'a, G: Html>( cx: Scope<'a>, IndexPageStateRx { diff --git a/examples/demos/full_page_layout/src/templates/index.rs b/examples/demos/full_page_layout/src/templates/index.rs index 2d2eb8047e..c91b1028e6 100644 --- a/examples/demos/full_page_layout/src/templates/index.rs +++ b/examples/demos/full_page_layout/src/templates/index.rs @@ -2,7 +2,7 @@ use crate::components::layout::Layout; use perseus::Template; use sycamore::prelude::{view, Html, Scope, SsrNode, View}; -#[perseus::template_rx] +#[perseus::template] pub fn index_page(cx: Scope) -> View { view! { cx, Layout(title = "Index") { diff --git a/examples/demos/full_page_layout/src/templates/long.rs b/examples/demos/full_page_layout/src/templates/long.rs index 8dec056c52..90b3861d4a 100644 --- a/examples/demos/full_page_layout/src/templates/long.rs +++ b/examples/demos/full_page_layout/src/templates/long.rs @@ -2,7 +2,7 @@ use crate::components::layout::Layout; use perseus::Template; use sycamore::prelude::{view, Html, Scope, SsrNode, View}; -#[perseus::template_rx] +#[perseus::template] pub fn long_page(cx: Scope) -> View { view! { cx, Layout(title = "Long") { diff --git a/examples/website/app_in_a_file/src/main.rs b/examples/website/app_in_a_file/src/main.rs index 01aff883a3..4b74f75a5b 100644 --- a/examples/website/app_in_a_file/src/main.rs +++ b/examples/website/app_in_a_file/src/main.rs @@ -16,7 +16,7 @@ pub fn main() -> PerseusApp { } // EXCERPT_START -#[perseus::template_rx] +#[perseus::template] fn index_page<'a, G: Html>(cx: Scope<'a>, props: IndexPropsRx<'a>) -> View { view! { cx, h1 { (format!( @@ -50,7 +50,7 @@ async fn get_index_build_state( } // EXCERPT_END -#[perseus::template_rx] +#[perseus::template] fn about_page(cx: Scope) -> View { view! { cx, p { "This is an example webapp created with Perseus!" } diff --git a/examples/website/i18n/src/main.rs b/examples/website/i18n/src/main.rs index 9791d1ea2b..afd2f33100 100644 --- a/examples/website/i18n/src/main.rs +++ b/examples/website/i18n/src/main.rs @@ -18,7 +18,7 @@ pub fn main() -> PerseusApp { // `/es-ES`, or `/fr-FR` based on the user's locale settings in their browser, // all automatically. If nothing matches, the default locale (`en-US`) will be // used. -#[perseus::template_rx] +#[perseus::template] fn index_page(cx: Scope) -> View { view! { cx, h1 { (t!("greeting", cx)) } diff --git a/examples/website/state_generation/src/main.rs b/examples/website/state_generation/src/main.rs index 5b6aca783a..7716588b1d 100644 --- a/examples/website/state_generation/src/main.rs +++ b/examples/website/state_generation/src/main.rs @@ -19,7 +19,7 @@ pub fn main() -> PerseusApp { } // EXCERPT_START -#[perseus::template_rx] +#[perseus::template] fn post_page<'a, G: Html>(cx: Scope<'a>, props: PostRx<'a>) -> View { view! { cx, h1 { (props.title.get()) } diff --git a/packages/perseus-cli/src/init.rs b/packages/perseus-cli/src/init.rs index eca688ac27..57027116d9 100644 --- a/packages/perseus-cli/src/init.rs +++ b/packages/perseus-cli/src/init.rs @@ -155,7 +155,7 @@ static DFLT_INIT_MOD_RS: &str = r#"pub mod index;"#; static DFLT_INIT_INDEX_RS: &str = r#"use perseus::Template; use sycamore::prelude::{view, Html, Scope, SsrNode, View}; -#[perseus::template_rx] +#[perseus::template] pub fn index_page(cx: Scope) -> View { view! { cx, // Don't worry, there are much better ways of styling in Perseus! diff --git a/packages/perseus-macro/src/lib.rs b/packages/perseus-macro/src/lib.rs index cfaa27e9cf..a41c2f872a 100644 --- a/packages/perseus-macro/src/lib.rs +++ b/packages/perseus-macro/src/lib.rs @@ -16,14 +16,13 @@ mod head; mod rx_state; mod state_fns; mod template; -mod template_rx; mod test; use darling::FromMeta; use proc_macro::TokenStream; use quote::quote; use state_fns::StateFnType; -use syn::{ItemStruct, Path}; +use syn::{DeriveInput, ItemStruct, Path}; /// Annotates functions used for generating state at build time to support /// automatic serialization/deserialization of app state and client/server @@ -102,16 +101,14 @@ pub fn should_revalidate(_args: TokenStream, input: TokenStream) -> TokenStream state_fns::state_fn_impl(parsed, StateFnType::ShouldRevalidate).into() } -/// Labels a Sycamore component as a Perseus template, turning it into something -/// that can be easily inserted into the `.template()` function, avoiding the -/// need for you to manually serialize/deserialize things. This should be -/// provided the name of the Sycamore component (same as given to Sycamore's -/// `#[component()]`, but without the ``). +// TODO(0.5.x) Remove this entirely +#[doc(hidden)] #[proc_macro_attribute] -pub fn template(_args: TokenStream, input: TokenStream) -> TokenStream { - let parsed = syn::parse_macro_input!(input as template::TemplateFn); - - template::template_impl(parsed).into() +pub fn template_rx(_args: TokenStream, _input: TokenStream) -> TokenStream { + quote! { + compile_error!("the `template_rx` macro has been replaced by the `template` macro") + } + .into() } /// The new version of `#[template]` designed for reactive state. This can @@ -129,10 +126,34 @@ pub fn template(_args: TokenStream, input: TokenStream) -> TokenStream { /// The second argument your template function can take is a global state /// generated with the `GlobalStateCreator`. You should also provide the /// reactive type here, and Perseus will do all the rest in the background. +/// +/// Labels a function as a Perseus template, automatically managing its state +/// and integrating it into your app. Functions annotated with this macro +/// take at least one argument for Sycamore's reactive scope, and then a +/// possible other argument for some state they generate with a rendering +/// strategy (e.g. *build state*, generated when you build your app, see the +/// book for more). That state is expected to be reactive (see [`make_rx`]), +/// although, if you use `#[template(unreactive)]`, you can use any state that +/// has been annotated with [`UnreactiveState`] to make it clear to Perseus not +/// to expect something reactive. +/// +/// Although you can make a Perseus app without using this macro, this isn't +/// recommended, since Perseus passes around state in your app as `String`s and +/// `dyn Any`s, meaning there is a large amount of overhead to actually using +/// the state you expect. This macro will automatically handle all that overhead +/// for you, making the process of building your app *significantly* smoother! +/// +/// *Note: in previous versions of Perseus, there was a `template_rx` macro, +/// which has become this. The old unreactive `template` macro has become +/// `#[template(unreactive)]`. For thos used to using Sycamore `#[component]` +/// annotation on their pages, this is no longer required. Note also that global +/// state is now accessed through the `.get_global_state()` method on Perseus' +/// `RenderCtx`.* #[proc_macro_attribute] -pub fn template_rx(_args: TokenStream, input: TokenStream) -> TokenStream { - let parsed = syn::parse_macro_input!(input as template_rx::TemplateFn); - template_rx::template_impl(parsed).into() +pub fn template(args: TokenStream, input: TokenStream) -> TokenStream { + let parsed = syn::parse_macro_input!(input as template::TemplateFn); + let is_reactive = args.to_string() != "unreactive"; + template::template_impl(parsed, is_reactive).into() } /// Labels a function as a Perseus head function, which is very similar to a @@ -353,3 +374,15 @@ pub fn browser(_args: TokenStream, input: TokenStream) -> TokenStream { } .into() } + +#[proc_macro_derive(UnreactiveState)] +pub fn unreactive_state(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as DeriveInput); + let name = input.ident; + + // This is a marker trait, so we barely have to do anything here + quote! { + impl ::perseus::state::UnreactiveState for #name {} + } + .into() +} diff --git a/packages/perseus-macro/src/template.rs b/packages/perseus-macro/src/template.rs index fcd3ae1ac3..bd01cfdb1f 100644 --- a/packages/perseus-macro/src/template.rs +++ b/packages/perseus-macro/src/template.rs @@ -1,16 +1,22 @@ +use std::str::FromStr; + +use darling::ToTokens; use proc_macro2::{Span, TokenStream}; use quote::quote; +use regex::Regex; use syn::parse::{Parse, ParseStream}; use syn::{ - Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, Result, ReturnType, Type, Visibility, + Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, PatType, Result, ReturnType, Type, + TypeTuple, Visibility, }; /// A function that can be wrapped in the Perseus test sub-harness. pub struct TemplateFn { /// The body of the function. pub block: Box, - /// The arguments to the function. One is mandatory for the reactive scope, - /// and then there can be an optional state type. + /// The arguments for custom properties and a global state, both of which + /// are optional. (But global state needs custom properties, which can be a + /// dummy `struct`.) pub args: Vec, /// The visibility of the function. pub vis: Visibility, @@ -68,7 +74,6 @@ impl Parse for TemplateFn { } ReturnType::Type(_, ty) => ty, }; - let mut args = Vec::new(); for arg in sig.inputs.iter() { // We don't care what the type is, as long as it's not `self` @@ -79,7 +84,7 @@ impl Parse for TemplateFn { } // We can have anywhere between 1 and 3 arguments (scope, ?state, ?global state) if args.len() > 3 || args.is_empty() { - return Err(syn::Error::new_spanned(&sig.inputs, "template functions accept between one and two arguments (reactive scope; then one optional for custom properties)")); + return Err(syn::Error::new_spanned(&sig.inputs, "template functions accept either one or two arguments (reactive scope; then one optional for custom properties)")); } Ok(Self { @@ -100,10 +105,33 @@ impl Parse for TemplateFn { } } -pub fn template_impl(input: TemplateFn) -> TokenStream { +/// Converts the user-given name of a final reactive `struct` with lifetimes +/// into the same type, just without those lifetimes, so we can use it outside +/// the scope in which those lifetimes have been defined. +/// +/// See the callers of this function to see exactly why it's necessary. +fn remove_lifetimes(ty: &Type) -> Type { + // Don't run any transformation if this is the unit type + match ty { + Type::Tuple(TypeTuple { elems, .. }) if elems.is_empty() => ty.clone(), + _ => { + let ty_str = ty.to_token_stream().to_string(); + // Remove any lifetimes from the type (anything in angular brackets beginning + // with `'`) This regex just removes any lifetimes next to generics + // or on their own, allowing for the whitespace Syn seems to insert + let ty_str = Regex::new(r#"(('.*?) |<\s*('[^, ]*?)\s*>)"#) + .unwrap() + .replace_all(&ty_str, ""); + Type::Verbatim(TokenStream::from_str(&ty_str).unwrap()) + } + } +} + +pub fn template_impl(input: TemplateFn, is_reactive: bool) -> TokenStream { let TemplateFn { block, - args, + // We know that these are all typed (none are `self`) + args: fn_args, generics, vis, attrs, @@ -113,45 +141,132 @@ pub fn template_impl(input: TemplateFn) -> TokenStream { let component_name = Ident::new(&(name.to_string() + "_component"), Span::call_site()); - // We create a wrapper function that can be easily provided to `.template()` - // that does deserialization automatically if needed This is dependent on - // what arguments the template takes - if args.len() == 2 { - // There's an argument that will be provided as a `String`, so the wrapper will - // deserialize it (also the reactive state) - let cx_arg = &args[0]; - let arg = &args[1]; - + if fn_args.len() == 2 && is_reactive { + // Get the argument for the reactive scope + let cx_arg = &fn_args[0]; + // There's an argument for page properties that needs to have state extracted, + // so the wrapper will deserialize it We'll also make it reactive and + // add it to the page state store + let arg = &fn_args[1]; + let rx_props_ty = match arg { + FnArg::Typed(PatType { ty, .. }) => remove_lifetimes(ty), + FnArg::Receiver(_) => unreachable!(), + }; + let name_string = name.to_string(); quote! { #vis fn #name(cx: ::sycamore::prelude::Scope, props: ::perseus::template::PageProps) -> ::sycamore::prelude::View { + use ::perseus::state::{MakeRx, MakeRxRef, RxRef}; + // The user's function, with Sycamore component annotations and the like preserved // We know this won't be async because Sycamore doesn't allow that #(#attrs)* + #[::sycamore::component] fn #component_name #generics(#cx_arg, #arg) -> #return_type { #block } - // If there are props, they will always be provided, the compiler just doesn't know that - let props = ::serde_json::from_str(&props.state.unwrap()).unwrap(); + let props = { + // Check if properties of the reactive type are already in the page state store + // If they are, we'll use them (so state persists for templates across the whole app) + let render_ctx = ::perseus::RenderCtx::from_ctx(cx); + // The render context will automatically handle prioritizing frozen or active state for us for this page as long as we have a reactive state type, which we do! + // We need there to be no lifetimes in `rx_props_ty` here, since the lifetimes the user decalred are defined inside the above function, which we + // aren't inside! + match render_ctx.get_active_or_frozen_page_state::<<#rx_props_ty as RxRef>::RxNonRef>(&props.path) { + // If we navigated back to this page, and it's still in the PSS, the given state will be a dummy, but we don't need to worry because it's never checked if this evaluates + ::std::option::Option::Some(existing_state) => existing_state, + // Again, frozen state has been dealt with already, so we'll fall back to generated state + ::std::option::Option::None => { + // Again, the render context can do the heavy lifting for us (this returns what we need, and can do type checking) + // We also assume that any state we have is valid because it comes from the server + // The user really should have a generation function, but if they don't then they'd get a panic, so give them a nice error message + render_ctx.register_page_state_str::<<#rx_props_ty as RxRef>::RxNonRef>(&props.path, &props.state.unwrap_or_else(|| panic!("template `{}` takes a state, but no state generation functions were provided (please add at least one to use state)", #name_string))).unwrap() + } + } + }; - #component_name(cx, props) + #component_name(cx, props.to_ref_struct(cx)) } } - } else { - // There is one argument for the reactive scope - let cx_arg = &args[0]; + } else if fn_args.len() == 2 && is_reactive == false { + // This template takes state that isn't reactive (but it must implement + // `UnreactiveState`) Get the argument for the reactive scope + let cx_arg = &fn_args[0]; + // There's an argument for page properties that needs to have state extracted, + // so the wrapper will deserialize it + // We'll also make it reactive and add it to the page state store + let arg = &fn_args[1]; + let props_ty = match arg { + // This type isn't reactive, so we shouldn't need to remove lifetimes (this also acts as + // a way of ensuring that users don't mix unreactive with reactive + // accidentally, since this part should lead to compile-time errors) + FnArg::Typed(PatType { ty, .. }) => ty, + FnArg::Receiver(_) => unreachable!(), + }; + let name_string = name.to_string(); + quote! { + #vis fn #name(cx: ::sycamore::prelude::Scope, props: ::perseus::template::PageProps) -> ::sycamore::prelude::View { + use ::perseus::state::{MakeRx, MakeRxRef, RxRef, MakeUnrx}; + + // The user's function, with Sycamore component annotations and the like preserved + // We know this won't be async because Sycamore doesn't allow that + #(#attrs)* + #[::sycamore::component] + fn #component_name #generics(#cx_arg, #arg) -> #return_type { + #block + } + + let props = { + // Check if properties of the reactive type are already in the page state store + // If they are, we'll use them (so state persists for templates across the whole app) + let render_ctx = ::perseus::RenderCtx::from_ctx(cx); + // The render context will automatically handle prioritizing frozen or active state for us for this page as long as we have a reactive state type, which we do! + // We need there to be no lifetimes in `rx_props_ty` here, since the lifetimes the user decalred are defined inside the above function, which we + // aren't inside! + // We're taking normal, unwrapped types, so we use the fact that anything implementing + // `UnreactiveState` can be turned into `UnreactiveStateWrapper` reactively to manage this + match render_ctx.get_active_or_frozen_page_state::<<#props_ty as MakeRx>::Rx>(&props.path) { + // If we navigated back to this page, and it's still in the PSS, the given state will be a dummy, but we don't need to worry because it's never checked if this evaluates + ::std::option::Option::Some(existing_state) => existing_state, + // Again, frozen state has been dealt with already, so we'll fall back to generated state + ::std::option::Option::None => { + // Again, the render context can do the heavy lifting for us (this returns what we need, and can do type checking) + // We also assume that any state we have is valid because it comes from the server + // The user really should have a generation function, but if they don't then they'd get a panic, so give them a nice error message + render_ctx.register_page_state_str::<<#props_ty as MakeRx>::Rx>(&props.path, &props.state.unwrap_or_else(|| panic!("template `{}` takes a state, but no state generation functions were provided (please add at least one to use state)", #name_string))).unwrap() + } + } + }; + // The `.make_unrx()` function will just convert back to the user's type + #component_name(cx, props.make_unrx()) + } + } + } else if fn_args.len() == 1 { + // Get the argument for the reactive scope + let cx_arg = &fn_args[0]; + // There are no arguments except for the scope quote! { #vis fn #name(cx: ::sycamore::prelude::Scope, props: ::perseus::template::PageProps) -> ::sycamore::prelude::View { + use ::perseus::state::{MakeRx, MakeRxRef}; + // The user's function, with Sycamore component annotations and the like preserved // We know this won't be async because Sycamore doesn't allow that #(#attrs)* + #[::sycamore::component] fn #component_name #generics(#cx_arg) -> #return_type { #block } + // Declare that this page will never take any state to enable full caching + let render_ctx = ::perseus::RenderCtx::from_ctx(cx); + render_ctx.register_page_no_state(&props.path); + #component_name(cx) } } + } else { + // We filtered out this possibility in the function parsing + unreachable!() } } diff --git a/packages/perseus-macro/src/template_rx.rs b/packages/perseus-macro/src/template_rx.rs deleted file mode 100644 index 04a9677ce4..0000000000 --- a/packages/perseus-macro/src/template_rx.rs +++ /dev/null @@ -1,218 +0,0 @@ -use std::str::FromStr; - -use darling::ToTokens; -use proc_macro2::{Span, TokenStream}; -use quote::quote; -use regex::Regex; -use syn::parse::{Parse, ParseStream}; -use syn::{ - Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, PatType, Result, ReturnType, Type, - TypeTuple, Visibility, -}; - -/// A function that can be wrapped in the Perseus test sub-harness. -pub struct TemplateFn { - /// The body of the function. - pub block: Box, - /// The arguments for custom properties and a global state, both of which - /// are optional. (But global state needs custom properties, which can be a - /// dummy `struct`.) - pub args: Vec, - /// The visibility of the function. - pub vis: Visibility, - /// Any attributes the function uses. - pub attrs: Vec, - /// The actual name of the function. - pub name: Ident, - /// The return type of the function. - pub return_type: Box, - /// Any generics the function takes (should be one for the Sycamore - /// `GenericNode`). - pub generics: Generics, -} -impl Parse for TemplateFn { - fn parse(input: ParseStream) -> Result { - let parsed: Item = input.parse()?; - - match parsed { - Item::Fn(func) => { - let ItemFn { - attrs, - vis, - sig, - block, - } = func; - // Validate each part of this function to make sure it fulfills the requirements - // Mustn't be async - if sig.asyncness.is_some() { - return Err(syn::Error::new_spanned( - sig.asyncness, - "templates cannot be asynchronous", - )); - } - // Can't be const - if sig.constness.is_some() { - return Err(syn::Error::new_spanned( - sig.constness, - "const functions can't be used as templates", - )); - } - // Can't be external - if sig.abi.is_some() { - return Err(syn::Error::new_spanned( - sig.abi, - "external functions can't be used as templates", - )); - } - // Must have an explicit return type - let return_type = match sig.output { - ReturnType::Default => { - return Err(syn::Error::new_spanned( - sig, - "template functions can't return default/inferred type", - )) - } - ReturnType::Type(_, ty) => ty, - }; - let mut args = Vec::new(); - for arg in sig.inputs.iter() { - // We don't care what the type is, as long as it's not `self` - if let FnArg::Receiver(arg) = arg { - return Err(syn::Error::new_spanned(arg, "templates can't take `self`")); - } - args.push(arg.clone()) - } - // We can have anywhere between 1 and 3 arguments (scope, ?state, ?global state) - if args.len() > 3 || args.is_empty() { - return Err(syn::Error::new_spanned(&sig.inputs, "template functions accept either one or two arguments (reactive scope; then one optional for custom properties)")); - } - - Ok(Self { - block, - args, - vis, - attrs, - name: sig.ident, - return_type, - generics: sig.generics, - }) - } - item => Err(syn::Error::new_spanned( - item, - "only functions can be used as templates", - )), - } - } -} - -/// Converts the user-given name of a final reactive `struct` with lifetimes -/// into the same type, just without those lifetimes, so we can use it outside -/// the scope in which those lifetimes have been defined. -/// -/// See the callers of this function to see exactly why it's necessary. -fn remove_lifetimes(ty: &Type) -> Type { - // Don't run any transformation if this is the unit type - match ty { - Type::Tuple(TypeTuple { elems, .. }) if elems.is_empty() => ty.clone(), - _ => { - let ty_str = ty.to_token_stream().to_string(); - // Remove any lifetimes from the type (anything in angular brackets beginning - // with `'`) This regex just removes any lifetimes next to generics - // or on their own, allowing for the whitespace Syn seems to insert - let ty_str = Regex::new(r#"(('.*?) |<\s*('[^, ]*?)\s*>)"#) - .unwrap() - .replace_all(&ty_str, ""); - Type::Verbatim(TokenStream::from_str(&ty_str).unwrap()) - } - } -} - -pub fn template_impl(input: TemplateFn) -> TokenStream { - let TemplateFn { - block, - // We know that these are all typed (none are `self`) - args: fn_args, - generics, - vis, - attrs, - name, - return_type, - } = input; - - let component_name = Ident::new(&(name.to_string() + "_component"), Span::call_site()); - - if fn_args.len() == 2 { - // Get the argument for the reactive scope - let cx_arg = &fn_args[0]; - // There's an argument for page properties that needs to have state extracted, - // so the wrapper will deserialize it We'll also make it reactive and - // add it to the page state store - let arg = &fn_args[1]; - let rx_props_ty = match arg { - FnArg::Typed(PatType { ty, .. }) => remove_lifetimes(ty), - FnArg::Receiver(_) => unreachable!(), - }; - let name_string = name.to_string(); - quote! { - #vis fn #name(cx: ::sycamore::prelude::Scope, props: ::perseus::template::PageProps) -> ::sycamore::prelude::View { - use ::perseus::state::{MakeRx, MakeRxRef, RxRef}; - - // The user's function, with Sycamore component annotations and the like preserved - // We know this won't be async because Sycamore doesn't allow that - #(#attrs)* - #[::sycamore::component] - fn #component_name #generics(#cx_arg, #arg) -> #return_type { - #block - } - - let props = { - // Check if properties of the reactive type are already in the page state store - // If they are, we'll use them (so state persists for templates across the whole app) - let render_ctx = ::perseus::RenderCtx::from_ctx(cx); - // The render context will automatically handle prioritizing frozen or active state for us for this page as long as we have a reactive state type, which we do! - // We need there to be no lifetimes in `rx_props_ty` here, since the lifetimes the user decalred are defined inside the above function, which we - // aren't inside! - match render_ctx.get_active_or_frozen_page_state::<<#rx_props_ty as RxRef>::RxNonRef>(&props.path) { - // If we navigated back to this page, and it's still in the PSS, the given state will be a dummy, but we don't need to worry because it's never checked if this evaluates - ::std::option::Option::Some(existing_state) => existing_state, - // Again, frozen state has been dealt with already, so we'll fall back to generated state - ::std::option::Option::None => { - // Again, the render context can do the heavy lifting for us (this returns what we need, and can do type checking) - // We also assume that any state we have is valid because it comes from the server - // The user really should have a generation function, but if they don't then they'd get a panic, so give them a nice error message - render_ctx.register_page_state_str::<<#rx_props_ty as RxRef>::RxNonRef>(&props.path, &props.state.unwrap_or_else(|| panic!("template `{}` takes a state, but no state generation functions were provided (please add at least one to use state)", #name_string))).unwrap() - } - } - }; - - #component_name(cx, props.to_ref_struct(cx)) - } - } - } else if fn_args.len() == 1 { - // Get the argument for the reactive scope - let cx_arg = &fn_args[0]; - // There are no arguments except for the scope - quote! { - #vis fn #name(cx: ::sycamore::prelude::Scope, props: ::perseus::template::PageProps) -> ::sycamore::prelude::View { - use ::perseus::state::{MakeRx, MakeRxRef}; - - // The user's function, with Sycamore component annotations and the like preserved - // We know this won't be async because Sycamore doesn't allow that - #(#attrs)* - #[::sycamore::component] - fn #component_name #generics(#cx_arg) -> #return_type { - #block - } - - // Declare that this page will never take any state to enable full caching - let render_ctx = ::perseus::RenderCtx::from_ctx(cx); - render_ctx.register_page_no_state(&props.path); - - #component_name(cx) - } - } - } else { - // We filtered out this possibility in the function parsing - unreachable!() - } -} diff --git a/packages/perseus/src/lib.rs b/packages/perseus/src/lib.rs index bb7a426fc4..d1574a9aa8 100644 --- a/packages/perseus/src/lib.rs +++ b/packages/perseus/src/lib.rs @@ -81,7 +81,7 @@ pub type Request = HttpRequest<()>; pub use perseus_macro::{ amalgamate_states, browser, browser_main, build_paths, build_state, engine, engine_main, global_build_state, head, main, main_export, make_rx, request_state, set_headers, - should_revalidate, template, template_rx, test, + should_revalidate, template, template_rx, test, UnreactiveState, }; pub use sycamore::prelude::{DomNode, Html, HydrateNode, SsrNode}; pub use sycamore_router::{navigate, navigate_replace}; @@ -124,7 +124,7 @@ pub mod prelude { pub use crate::{ amalgamate_states, browser, browser_main, build_paths, build_state, engine, engine_main, global_build_state, head, main, main_export, make_rx, request_state, set_headers, - should_revalidate, template, template_rx, test, + should_revalidate, template, template_rx, test, UnreactiveState, }; #[cfg(feature = "i18n")] pub use crate::{link, t}; diff --git a/packages/perseus/src/state/mod.rs b/packages/perseus/src/state/mod.rs index b15ffcc458..aa1ebe7ad6 100644 --- a/packages/perseus/src/state/mod.rs +++ b/packages/perseus/src/state/mod.rs @@ -6,7 +6,7 @@ mod rx_state; pub use freeze::{FrozenApp, PageThawPrefs, ThawPrefs}; pub use global_state::{GlobalState, GlobalStateCreator, GlobalStateType}; pub use page_state_store::{PageStateStore, PssContains, PssEntry, PssState}; -pub use rx_state::{AnyFreeze, Freeze, MakeRx, MakeRxRef, MakeUnrx, RxRef}; +pub use rx_state::{AnyFreeze, Freeze, MakeRx, MakeRxRef, MakeUnrx, RxRef, UnreactiveState}; #[cfg(all(feature = "idb-freezing", target_arch = "wasm32"))] mod freeze_idb; diff --git a/packages/perseus/src/state/rx_state.rs b/packages/perseus/src/state/rx_state.rs index 7c0c8ba950..2434471557 100644 --- a/packages/perseus/src/state/rx_state.rs +++ b/packages/perseus/src/state/rx_state.rs @@ -71,3 +71,52 @@ impl AnyFreeze for T { self } } + +/// A marker trait for types that you want to be able to use with the Perseus +/// state platform, without using `#[make_rx]`. If you want to use unreactive +/// state, implement this, and you'll automatically be able to use your +/// unreactive type without problems! +pub trait UnreactiveState {} + +/// A wrapper for storing unreactive state in Perseus, and allowing it to +/// interface with the (fundamentally reactive) state platform. Generally, you +/// would just use reactive state, however, sometimes, you may wish to use +/// unreactive state without sacrificing features like automatic page state +/// caching: this `struct` allows that. +/// +/// This is handled automatically by the `#[template]` macro, and you should +/// never need to use this manually unless you don't use the macros. +/// +/// This wrapper will automatically implement all the necessary `trait`s to +/// interface with Perseus' reactive state platform, along with `Serialize` and +/// `Deserialize` (provided the underlying type also implements the latter two). +#[derive(Clone)] +pub struct UnreactiveStateWrapper< + T: Serialize + for<'de> Deserialize<'de> + UnreactiveState + Clone, +>(pub T); +// Automatically implement `MakeRx` for any marked unreactive type, using +// `UnreactiveStateWrapper` as the reactive type +impl Deserialize<'de> + UnreactiveState + Clone> MakeRx for T { + type Rx = UnreactiveStateWrapper; + fn make_rx(self) -> Self::Rx { + UnreactiveStateWrapper(self) + } +} +// And let it be converted back +impl Deserialize<'de> + UnreactiveState + Clone> MakeUnrx + for UnreactiveStateWrapper +{ + type Unrx = T; + fn make_unrx(self) -> Self::Unrx { + self.0 + } +} +// And, since the underlying type can be serialized, implement `Freeze` +impl Deserialize<'de> + UnreactiveState + Clone> Freeze + for UnreactiveStateWrapper +{ + fn freeze(&self) -> String { + // Just serialize the underlying type + serde_json::to_string(&self.0).unwrap() + } +} diff --git a/packages/perseus/src/template/core.rs b/packages/perseus/src/template/core.rs index 399aa51dbb..f01da30dc3 100644 --- a/packages/perseus/src/template/core.rs +++ b/packages/perseus/src/template/core.rs @@ -556,7 +556,7 @@ impl Template { // saving!) The macros handle the creation of empty functions to make user's // lives easier /// Sets the template rendering function to use. This function might take in - /// some state (use the `#[perseus::template_rx]` macro for serialization + /// some state (use the `#[perseus::template]` macro for serialization /// convenience) and/or some global state, and then it must return a /// Sycamore [`View`]. pub fn template(