Skip to main content

resuma/
lib.rs

1//! # Resuma
2//!
3//! **SSR + resumability for Rust** — components run on the server only; the browser
4//! resumes serialized signals and lazy handler chunks instead of re-hydrating the tree.
5//!
6//! ## Quick start
7//!
8//! ```no_run
9//! use resuma::prelude::*;
10//!
11//! #[component]
12//! fn Counter() {
13//!     let n = signal(0);
14//!     view! {
15//!         <button onClick={n.update(|v| *v += 1)}>{n}</button>
16//!     }
17//! }
18//!
19//! #[tokio::main]
20//! async fn main() -> std::io::Result<()> {
21//!     ResumaApp::new()
22//!         .component("/", Counter)
23//!         .serve(ServeOptions::default())
24//!         .await
25//! }
26//! ```
27//!
28//! Install the CLI: `cargo install resuma`. Narrative guides live at
29//! [resuma-docs.fly.dev](https://resuma-docs.fly.dev/docs).
30//!
31//! ## Resumability model (v0.4)
32//!
33//! * Every [`#[component]`](component) is a **resumable boundary** — handlers register
34//!   under `/_resuma/handler/{Component}.js` and prefetch when the boundary enters the viewport.
35//! * [`computed!`](computed), [`effect!`](effect), and [`debounce!`](debounce) translate Rust
36//!   closures to client-replayable JS via rs2js (in `resuma-macros`).
37//! * Plain [`use_computed`] / [`use_effect`] run on SSR only;
38//!   use the macros when the browser must replay derived state or side effects.
39//! * [`#[island]`](island) is **optional** — for heavy lazy bundles, `load = "visible"`, or dev HMR.
40//!
41//! ## Crate layout
42//!
43//! | Module | Role |
44//! |--------|------|
45//! | [`core`] | Signals, `View`, [`RenderContext`], [`ResumePayload`] |
46//! | [`ssr`] | HTML rendering + embedded resumability payload |
47//! | [`mod@server`] | axum HTTP, `ResumaApp`, `/_resuma/*` assets |
48//! | [`flow`] | `FlowApp`, file-based pages, `#[load]`, `#[submit]` |
49//! | [`router`] | Page discovery scanner |
50//! | [`cli`] | `resuma new` / `dev` / `build` (feature `cli`) |
51//!
52//! Users depend on **`resuma`** only; [`resuma-macros`](https://docs.rs/resuma-macros) is a separate
53//! proc-macro crate required by the build.
54//!
55//! ## Re-exports
56//!
57//! Most apps start with [`prelude`] (`use resuma::prelude::*`). Macros (`view!`, `#[component]`,
58//! `#[server]`, `#[data]`, Flow attributes) and common types are re-exported at the crate root
59//! for convenience.
60
61pub mod client;
62pub mod core;
63pub mod flow;
64pub mod router;
65pub mod server;
66pub mod ssr;
67
68#[cfg(feature = "cli")]
69pub mod cli;
70
71pub use resuma_macros::{
72    component, computed, data, debounce, effect, island, js, layout, load, middleware, server,
73    submit, view,
74};
75
76pub use crate::client::{
77    client_component, client_script_url, ClientComponent, CLIENT_SCRIPT_PREFIX,
78};
79
80pub use crate::core::{
81    combine_js, error_boundary, nav_link, no_serialize, portal, provide_context, provide_theme,
82    push_slots, resolve_slot, show, signal, stream_chunk, stream_slot, theme_css_vars,
83    use_computed, use_computed_with_js, use_context, use_debounce, use_effect, use_signal,
84    use_store, use_task, use_theme, use_visible_task, visible_task_js, with_default_slot,
85    with_view_transition, Child, Component, Computed, ContextId, Effect, FlowRequest, IntoView,
86    NoSerialize, ReadSignal, RenderContext, RenderMode, Result, ResumaError, ResumePayload, Signal,
87    SlotGuard, SlottedChild, Store, Theme, View, WriteSignal,
88};
89
90pub use crate::server::{
91    configure_security, register_server_action, set_action_middleware, ResumaApp, SecurityConfig,
92    ServeOptions, CSRF_FIELD, CSRF_HEADER,
93};
94
95pub use crate::ssr::{render_to_stream, render_to_string, render_view, PageOptions};
96
97pub use crate::flow::{
98    apply_layouts, current_request, discover_pages, encode_submit_result, error_page,
99    extract_redirect, flash_message, form, load_boundary, not_found_page, redirect,
100    redirect_with_flash, register_layout, register_loader, register_loader_cache,
101    register_middleware, register_stream_chunk, register_stream_loader, register_submit,
102    try_use_load, try_use_load_value, use_load, with_request, DiscoveredPage, FlowApp, FlowError,
103    FlowExtensions, FlowPageRegistry, FlowPwaConfig, FlowServeOptions, LoadValue, LoaderError,
104    Redirect, SubmitError, SubmitValue,
105};
106
107/// CLI entry point (`cargo install resuma`).
108#[cfg(feature = "cli")]
109pub fn run() -> anyhow::Result<()> {
110    crate::cli::run()
111}
112
113pub mod prelude {
114    //! Convenient re-exports for application code.
115    //!
116    //! ```rust,ignore
117    //! use resuma::prelude::*;
118    //! ```
119    //!
120    //! Includes:
121    //!
122    //! * **Macros** — [`view!`](crate::view), [`#[component]`](crate::component),
123    //!   [`#[server]`](macro@crate::server), [`#[data]`](macro@crate::data), [`computed!`](crate::computed),
124    //!   [`effect!`](crate::effect), [`debounce!`](crate::debounce), Flow (`#[load]`, `#[submit]`, …)
125    //! * **Components** — [`View`], [`Signal`], [`Component`]
126    //! * **Apps** — [`ResumaApp`], [`FlowApp`],
127    //!   [`ServeOptions`], [`FlowServeOptions`]
128    //! * **SSR** — [`render_to_string`], [`render_view`]
129    //! * **Flow runtime** — [`FlowRequest`], [`current_request`],
130    //!   [`use_load`], [`form`](crate::form())
131    //! * **Client components** — [`ClientComponent`], [`client_component`]
132    //!
133    //! For low-level types ([`RenderContext`](crate::RenderContext), [`ResumePayload`](crate::ResumePayload)),
134    //! import from [`crate::core`].
135    pub use super::{
136        client_component, client_script_url, combine_js, component, computed, configure_security,
137        current_request, data, debounce, effect, error_boundary, error_page, extract_redirect,
138        flash_message, form, island, js, layout, load, load_boundary, middleware, nav_link,
139        not_found_page, portal, provide_context, provide_theme, push_slots, redirect,
140        redirect_with_flash, render_to_string, render_view, resolve_slot, server,
141        set_action_middleware, show, signal, stream_slot, submit, theme_css_vars, try_use_load,
142        try_use_load_value, use_computed, use_computed_with_js, use_context, use_debounce,
143        use_effect, use_load, use_signal, use_store, use_task, use_theme, use_visible_task, view,
144        with_view_transition, Child, ClientComponent, Component, Computed, Effect, FlowApp,
145        FlowError, FlowPageRegistry, FlowRequest, FlowServeOptions, IntoView, LoadValue,
146        LoaderError, PageOptions, ReadSignal, Redirect, Result, ResumaApp, ResumaError,
147        SecurityConfig, ServeOptions, Signal, SlottedChild, Store, SubmitError, Theme, View,
148        WriteSignal, CLIENT_SCRIPT_PREFIX, CSRF_FIELD, CSRF_HEADER,
149    };
150}
151
152#[doc(hidden)]
153pub mod __private {
154    //! Re-exports used by the macro-generated code.
155    pub use crate::core::effect::{attach_client_effect, use_computed_with_js};
156    pub use crate::core::task::register_debounce_effect;
157    pub use crate::core::{combine_js, nav_link, show};
158    pub use crate::core::{
159        context::{current_context, with_handler_chunk, RenderContext, RenderMode},
160        handler::{HandlerCapture, HandlerRef},
161        signal::SignalId,
162        slot::{push_slots, resolve_slot, with_default_slot, SlottedChild},
163        view::{AttrValue, Element, Fragment, Island as IslandView},
164        Child, Component, IntoView, ReadSignal, Result, ResumaError, Signal, View, WriteSignal,
165    };
166    pub use crate::flow::form as flow_form;
167    pub use crate::server::register_server_action;
168    pub use ctor;
169    pub use serde;
170    pub use serde_json;
171
172    #[derive(Debug, Clone)]
173    pub enum HandlerSource {
174        Inline(String),
175        Chunk {
176            chunk: String,
177            symbol: String,
178            source: String,
179        },
180    }
181
182    #[derive(Debug, Clone)]
183    pub enum ResumeCapture {
184        Signal { name: String, id: SignalId },
185        Action(String),
186    }
187
188    pub use crate::core::view::Element as ElementType;
189
190    pub fn register_handler(
191        event: &str,
192        _chunk: &str,
193        symbol: &str,
194        js_source: &str,
195        captures: Vec<ResumeCapture>,
196        actions: Vec<String>,
197    ) -> AttrValue {
198        let chunk = current_context()
199            .map(|c| c.current_handler_chunk())
200            .unwrap_or_else(|| "__page__".to_string());
201
202        if let Some(ctx) = current_context() {
203            ctx.register_handler(&chunk, symbol, js_source);
204            for a in &actions {
205                ctx.register_action(a);
206            }
207        }
208
209        let signal_captures: Vec<HandlerCapture> = captures
210            .into_iter()
211            .filter_map(|c| match c {
212                ResumeCapture::Signal { name, id } => Some(HandlerCapture { name, id }),
213                _ => None,
214            })
215            .collect();
216
217        let inline = if chunk == "__page__"
218            && js_source.len() <= crate::core::context::INLINE_HANDLER_MAX_BYTES
219        {
220            Some(js_source.to_string())
221        } else {
222            None
223        };
224
225        AttrValue::Handler(HandlerRef {
226            event: event.to_string(),
227            chunk,
228            symbol: symbol.to_string(),
229            captures: signal_captures,
230            inline,
231        })
232    }
233
234    pub trait ElementBuilderExt {
235        fn attr_runtime(self, kv: (String, AttrValue)) -> Self;
236    }
237
238    impl ElementBuilderExt for crate::core::view::ElementBuilder {
239        fn attr_runtime(self, (name, value): (String, AttrValue)) -> Self {
240            self.attr(name, value)
241        }
242    }
243
244    pub fn render_component<C: Component>(props: C::Props) -> View {
245        C::render(props)
246    }
247
248    pub fn resolve_attr_value<T: Into<AttrValueAuto>>(value: T) -> AttrValue {
249        value.into().into_attr_value()
250    }
251
252    pub struct AttrValueAuto(AttrValue);
253
254    impl AttrValueAuto {
255        fn into_attr_value(self) -> AttrValue {
256            self.0
257        }
258    }
259
260    impl From<&str> for AttrValueAuto {
261        fn from(s: &str) -> Self {
262            Self(AttrValue::Static(s.to_string()))
263        }
264    }
265    impl From<String> for AttrValueAuto {
266        fn from(s: String) -> Self {
267            Self(AttrValue::Static(s))
268        }
269    }
270    impl From<bool> for AttrValueAuto {
271        fn from(b: bool) -> Self {
272            Self(AttrValue::Static(b.to_string()))
273        }
274    }
275    impl From<i32> for AttrValueAuto {
276        fn from(n: i32) -> Self {
277            Self(AttrValue::Static(n.to_string()))
278        }
279    }
280    impl From<i64> for AttrValueAuto {
281        fn from(n: i64) -> Self {
282            Self(AttrValue::Static(n.to_string()))
283        }
284    }
285    impl From<u32> for AttrValueAuto {
286        fn from(n: u32) -> Self {
287            Self(AttrValue::Static(n.to_string()))
288        }
289    }
290    impl From<u64> for AttrValueAuto {
291        fn from(n: u64) -> Self {
292            Self(AttrValue::Static(n.to_string()))
293        }
294    }
295    impl From<f64> for AttrValueAuto {
296        fn from(n: f64) -> Self {
297            Self(AttrValue::Static(n.to_string()))
298        }
299    }
300
301    impl<T: Clone + serde::Serialize + 'static> From<&Signal<T>> for AttrValueAuto {
302        fn from(s: &Signal<T>) -> Self {
303            Self(AttrValue::Dynamic {
304                signal: s.id(),
305                format: None,
306            })
307        }
308    }
309    impl<T: Clone + serde::Serialize + 'static> From<Signal<T>> for AttrValueAuto {
310        fn from(s: Signal<T>) -> Self {
311            Self(AttrValue::Dynamic {
312                signal: s.id(),
313                format: None,
314            })
315        }
316    }
317
318    pub fn wrap_in_island(name: &str, instance: u32, view: View, load: &str) -> View {
319        if let Some(ctx) = current_context() {
320            ctx.register_island(name);
321        }
322        let load = match load {
323            "visible" | "Visible" => view_mod::IslandLoad::Visible,
324            _ => view_mod::IslandLoad::Eager,
325        };
326        let inner = crate::core::context::with_handler_chunk(name, || view);
327        View::Island(IslandView {
328            chunk_id: name.to_string(),
329            instance_id: format!("{}-{}", name, instance),
330            signal_ids: Vec::new(),
331            view: Box::new(inner),
332            props: serde_json::Value::Null,
333            load,
334        })
335    }
336
337    pub use crate::core::view as view_mod;
338    pub use crate::core::view::ElementBuilder;
339
340    pub fn fragment(children: Vec<Child>) -> View {
341        View::fragment(children)
342    }
343
344    pub fn element(tag: &str) -> ElementBuilder {
345        View::element(tag)
346    }
347}