use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SsrMode {
Sync,
Blocking,
Streaming,
}
#[must_use]
pub fn render_to_string(view: impl FnOnce() -> View) -> String {
is_not_ssr! {
let _ = view;
panic!("`render_to_string` only available in SSR mode");
}
is_ssr! {
use std::cell::LazyCell;
thread_local! {
static SSR_ROOT: LazyCell<RootHandle> = LazyCell::new(|| create_root(|| {}));
}
SSR_ROOT.with(|root| {
root.dispose();
root.run_in(|| {
render_to_string_in_scope(view)
})
})
}
}
#[must_use]
pub fn render_to_string_in_scope(view: impl FnOnce() -> View) -> String {
is_not_ssr! {
let _ = view;
panic!("`render_to_string_in_scope` only available in SSR mode");
}
is_ssr! {
let mut buf = String::new();
let handle = create_child_scope(|| {
provide_context(HydrationRegistry::new());
provide_context(SsrMode::Sync);
let prev = IS_HYDRATING.replace(true);
let view = view();
IS_HYDRATING.set(prev);
ssr_node::render_recursive_view(&view, &mut buf);
});
handle.dispose();
buf
}
}
#[must_use]
#[cfg(feature = "suspense")]
pub async fn render_to_string_await_suspense(f: impl FnOnce() -> View) -> String {
is_not_ssr! {
let _ = f;
panic!("`render_to_string` only available in SSR mode");
}
is_ssr! {
use std::cell::LazyCell;
use futures::channel::oneshot;
use sycamore_futures::{provide_executor_scope, use_is_loading_global};
thread_local! {
static SSR_ROOT: LazyCell<RootHandle> = LazyCell::new(|| create_root(|| {}));
}
let mut handle: Option<NodeHandle> = None;
let (tx, rx) = oneshot::channel();
let mut tx = Some(tx);
let mut view = View::default();
let is_hydrating = IS_HYDRATING.replace(true);
provide_executor_scope(async {
SSR_ROOT.with(|root| {
root.dispose();
root.run_in(|| {
handle = Some(create_child_scope(|| {
provide_context(HydrationRegistry::new());
provide_context(SsrMode::Blocking);
view = f();
}));
create_effect(move || {
if !use_is_loading_global() {
if let Some(tx) = tx.take() {
tx.send(()).ok().unwrap();
}
}
});
});
});
rx.await.unwrap();
handle.unwrap().dispose();
IS_HYDRATING.set(is_hydrating);
}).await;
let mut buf = String::new();
ssr_node::render_recursive_view(&view, &mut buf);
buf
}
}
#[cfg(feature = "suspense")]
pub fn render_to_string_stream(
view: impl FnOnce() -> View,
) -> impl futures::Stream<Item = String> + Send {
is_not_ssr! {
let _ = view;
panic!("`render_to_string` only available in SSR mode");
#[allow(unreachable_code)] futures::stream::empty()
}
is_ssr! {
use std::cell::{LazyCell, RefCell};
use std::rc::Rc;
use futures::{SinkExt, StreamExt};
use futures::stream::FuturesUnordered;
thread_local! {
static SSR_ROOT: LazyCell<RootHandle> = LazyCell::new(|| create_root(|| {}));
}
IS_HYDRATING.set(true);
let mut buf = String::new();
let futures = Rc::new(RefCell::new(FuturesUnordered::new()));
let (mut tx, mut rx) = futures::channel::mpsc::unbounded();
SSR_ROOT.with(|root| {
root.dispose();
root.run_in(|| {
provide_context(HydrationRegistry::new());
provide_context(SsrMode::Streaming);
let suspense_state = SuspenseStream { futures: futures.clone() };
provide_context(suspense_state);
let view = view();
ssr_node::render_recursive_view(&view, &mut buf);
let mut pending_futures = futures.take();
sycamore_futures::spawn_local_scoped(async move {
while let Some(fragment) = pending_futures.next().await {
tx.send(fragment).await.unwrap();
pending_futures.extend(futures.take());
}
});
});
});
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>"#;
async_stream::stream! {
let mut initial = String::new();
initial.push_str("<!doctype html>");
initial.push_str(&buf);
initial.push_str(SUSPENSE_REPLACE_SCRIPT);
yield initial;
while let Some(fragment) = rx.next().await {
yield render_suspense_fragment(fragment);
}
}
}
}
#[cfg_ssr]
#[cfg(feature = "suspense")]
fn render_suspense_fragment(SuspenseFragment { key, view }: SuspenseFragment) -> String {
use std::fmt::Write;
let mut buf = String::new();
write!(&mut buf, "<template id=\"sycamore-suspense-{key}\">",).unwrap();
ssr_node::render_recursive_view(&view, &mut buf);
write!(
&mut buf,
"</template><script>__sycamore_suspense({key})</script>"
)
.unwrap();
buf
}
#[cfg(test)]
#[cfg(feature = "suspense")]
#[cfg_ssr]
mod tests {
use expect_test::expect;
use futures::channel::oneshot;
use super::*;
#[component(inline_props)]
async fn AsyncComponent(receiver: oneshot::Receiver<()>) -> View {
receiver.await.unwrap();
view! {
"Hello, async!"
}
}
#[component(inline_props)]
fn App(receiver: oneshot::Receiver<()>) -> View {
view! {
Suspense(fallback=|| "fallback".into()) {
AsyncComponent(receiver=receiver)
}
}
}
#[test]
fn render_to_string_renders_fallback() {
let (sender, receiver) = oneshot::channel();
let res = render_to_string(move || view! { App(receiver=receiver) });
assert_eq!(
res,
"<!--/--><!--/-->fallback<!--/--><!--/--><!--/--><!--/-->"
);
assert!(sender.send(()).is_err(), "receiver should be dropped");
}
#[tokio::test]
async fn render_to_string_await_suspense_works() {
let (sender, receiver) = oneshot::channel();
let ssr = render_to_string_await_suspense(move || view! { App(receiver=receiver) });
futures::pin_mut!(ssr);
assert!(futures::poll!(&mut ssr).is_pending());
sender.send(()).unwrap();
let res = ssr.await;
let expect = expect![[
r#"<suspense-start data-key="1" data-hk="0.0"></suspense-start><no-ssr data-hk="0.1"></no-ssr><!--/--><!--/-->Hello, async!<!--/--><!--/-->"#
]];
expect.assert_eq(&res);
}
}