sycamore_web/node/
ssr_render.rs1use super::*;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum SsrMode {
6 Sync,
10 Blocking,
14 Streaming,
19}
20
21#[must_use]
23pub fn render_to_string(view: impl FnOnce() -> View) -> String {
24 is_not_ssr! {
25 let _ = view;
26 panic!("`render_to_string` only available in SSR mode");
27 }
28 is_ssr! {
29 use std::cell::LazyCell;
30
31 thread_local! {
32 static SSR_ROOT: LazyCell<RootHandle> = LazyCell::new(|| create_root(|| {}));
35 }
36 SSR_ROOT.with(|root| {
37 root.dispose();
38 root.run_in(|| {
39 render_to_string_in_scope(view)
40 })
41 })
42 }
43}
44
45#[must_use]
49pub fn render_to_string_in_scope(view: impl FnOnce() -> View) -> String {
50 is_not_ssr! {
51 let _ = view;
52 panic!("`render_to_string` only available in SSR mode");
53 }
54 is_ssr! {
55 let mut buf = String::new();
56
57 let handle = create_child_scope(|| {
58 provide_context(HydrationRegistry::new());
59 provide_context(SsrMode::Sync);
60
61 let prev = IS_HYDRATING.replace(true);
62 let view = view();
63 IS_HYDRATING.set(prev);
64 ssr_node::render_recursive_view(&view, &mut buf);
65 });
66 handle.dispose();
67 buf
68 }
69}
70
71#[must_use]
92#[cfg(feature = "suspense")]
93pub async fn render_to_string_await_suspense(view: impl FnOnce() -> View) -> String {
94 is_not_ssr! {
95 let _ = view;
96 panic!("`render_to_string` only available in SSR mode");
97 }
98 is_ssr! {
99 use std::num::NonZeroU32;
100 use std::cell::LazyCell;
101 use std::fmt::Write;
102 use std::collections::HashMap;
103
104 use futures::StreamExt;
105
106 const BUFFER_SIZE: usize = 5;
107
108 thread_local! {
109 static SSR_ROOT: LazyCell<RootHandle> = LazyCell::new(|| create_root(|| {}));
112 }
113 IS_HYDRATING.set(true);
114 sycamore_futures::provide_executor_scope(async {
115 let mut buf = String::new();
116
117 let (sender, mut receiver) = futures::channel::mpsc::channel(BUFFER_SIZE);
118 SSR_ROOT.with(|root| {
119 root.dispose();
120 root.run_in(|| {
121 provide_context(HydrationRegistry::new());
123 provide_context(SsrMode::Blocking);
124 let suspense_state = SuspenseState { sender };
125
126 provide_context(suspense_state);
127
128 let view = view();
129 ssr_node::render_recursive_view(&view, &mut buf);
130 });
131 });
132
133 let split = buf.split("<!--sycamore-suspense-").collect::<Vec<_>>();
135 let n = split.len() - 1;
137
138 let mut fragment_map = HashMap::new();
140 if n == 0 {
141 receiver.close();
142 }
143 let mut i = 0;
144 while let Some(fragment) = receiver.next().await {
145 fragment_map.insert(fragment.key, fragment.view);
146 i += 1;
147 if i == n {
148 receiver.close();
150 }
151 }
152 IS_HYDRATING.set(false);
153
154 if let [first, rest @ ..] = split.as_slice() {
156 rest.iter().fold(first.to_string(), |mut acc, s| {
157 let (num, rest) = s.split_once("-->").expect("end of suspense marker not found");
159 let key: u32 = num.parse().expect("could not parse suspense key");
160 let key = NonZeroU32::try_from(key).expect("suspense key cannot be 0");
161 let fragment = fragment_map.get(&key).expect("fragment not found");
162 ssr_node::render_recursive_view(fragment, &mut acc);
163
164 write!(&mut acc, "{rest}").unwrap();
165 acc
166 })
167 } else {
168 unreachable!("split should always have at least one element")
169 }
170 }).await
171 }
172}
173
174#[cfg(feature = "suspense")]
231pub fn render_to_string_stream(
232 view: impl FnOnce() -> View,
233) -> impl futures::Stream<Item = String> + Send {
234 is_not_ssr! {
235 let _ = view;
236 panic!("`render_to_string` only available in SSR mode");
237 #[allow(unreachable_code)] futures::stream::empty()
239 }
240 is_ssr! {
241 use std::cell::LazyCell;
242
243 use futures::StreamExt;
244
245 const BUFFER_SIZE: usize = 5;
246
247 thread_local! {
248 static SSR_ROOT: LazyCell<RootHandle> = LazyCell::new(|| create_root(|| {}));
251 }
252 IS_HYDRATING.set(true);
253 let mut buf = String::new();
254 let (sender, mut receiver) = futures::channel::mpsc::channel(BUFFER_SIZE);
255 SSR_ROOT.with(|root| {
256 root.dispose();
257 root.run_in(|| {
258 provide_context(HydrationRegistry::new());
260 provide_context(SsrMode::Streaming);
261 let suspense_state = SuspenseState { sender };
262
263 provide_context(suspense_state);
264
265 let view = view();
266 ssr_node::render_recursive_view(&view, &mut buf);
267 });
268 });
269
270 let mut n = buf.matches("<!--sycamore-suspense-").count();
272
273 static SUSPENSE_REPLACE_SCRIPT: &str = r#"<script>function __sycamore_suspense(e){let s=document.querySelector(`suspense-start[data-key="${e}"]`),n=document.querySelector(`suspense-end[data-key="${e}"]`),r=document.getElementById(`sycamore-suspense-${e}`);for(s.parentNode.insertBefore(r.content,s);s.nextSibling!=n;)s.parentNode.removeChild(s.nextSibling);}</script>"#;
285 async_stream::stream! {
286 let mut initial = String::new();
287 initial.push_str("<!doctype html>");
288 initial.push_str(&buf);
289 initial.push_str(SUSPENSE_REPLACE_SCRIPT);
290 yield initial;
291
292 if n == 0 {
293 receiver.close();
294 }
295 let mut i = 0;
296 while let Some(fragment) = receiver.next().await {
297 let buf_fragment = render_suspense_fragment(fragment);
298 let n_add = buf_fragment.matches("<!--sycamore-suspense-").count();
300 n += n_add;
301
302 yield buf_fragment;
303
304 i += 1;
305 if i == n {
306 receiver.close();
308 }
309 }
310 }
311 }
312}
313
314#[cfg_ssr]
315#[cfg(feature = "suspense")]
316fn render_suspense_fragment(SuspenseFragment { key, view }: SuspenseFragment) -> String {
317 use std::fmt::Write;
318
319 let mut buf = String::new();
320 write!(&mut buf, "<template id=\"sycamore-suspense-{key}\">",).unwrap();
321 ssr_node::render_recursive_view(&view, &mut buf);
322 write!(
323 &mut buf,
324 "</template><script>__sycamore_suspense({key})</script>"
325 )
326 .unwrap();
327
328 buf
329}
330
331#[cfg(test)]
332#[cfg(feature = "suspense")]
333#[cfg_ssr]
334mod tests {
335 use expect_test::expect;
336 use futures::channel::oneshot;
337
338 use super::*;
339
340 #[component(inline_props)]
341 async fn AsyncComponent(receiver: oneshot::Receiver<()>) -> View {
342 receiver.await.unwrap();
343 view! {
344 "Hello, async!"
345 }
346 }
347
348 #[component(inline_props)]
349 fn App(receiver: oneshot::Receiver<()>) -> View {
350 view! {
351 Suspense(fallback=|| "fallback".into()) {
352 AsyncComponent(receiver=receiver)
353 }
354 }
355 }
356
357 #[test]
358 fn render_to_string_renders_fallback() {
359 let (sender, receiver) = oneshot::channel();
360 let res = render_to_string(move || view! { App(receiver=receiver) });
361 assert_eq!(
362 res,
363 "<!--/--><!--/-->fallback<!--/--><!--/--><!--/--><!--/-->"
364 );
365 assert!(sender.send(()).is_err(), "receiver should be dropped");
366 }
367
368 #[tokio::test]
369 async fn render_to_string_await_suspense_works() {
370 let (sender, receiver) = oneshot::channel();
371 let ssr = render_to_string_await_suspense(move || view! { App(receiver=receiver) });
372 futures::pin_mut!(ssr);
373 assert!(futures::poll!(&mut ssr).is_pending());
374
375 sender.send(()).unwrap();
376 let res = ssr.await;
377
378 let expect = expect![[
379 r#"<suspense-start data-key="1" data-hk="0.0"></suspense-start><!--/--><!--/--><!--/-->Hello, async!<!--/--><!--/--><!--/--><suspense-end data-key="1"></suspense-end>"#
380 ]];
381 expect.assert_eq(&res);
382 }
383}