Beginning work on SSR/hydration

This commit is contained in:
Greg Johnston
2022-09-01 10:39:04 -04:00
parent dea973dac6
commit 54f37095dc
17 changed files with 517 additions and 128 deletions

View File

@@ -8,6 +8,11 @@
- [ ] Router example
- [ ] Styling and formatting
- [ ] Get transitions working
- [ ] SSR
- [x] SSR
- [ ] Hydration
- [ ] Streaming HTML from server
- [ ] Streaming `Resource`s
- [ ] Docs (and clippy warning to insist on docs)
- [ ] Read through + understand...
- [ ] `Props` macro
@@ -18,10 +23,6 @@
- [ ] Scheduling effects/run effects at end of render
- [ ] `batch()`
- [ ] Portals
- [ ] SSR
- [ ] Macro
- [ ] Streaming HTML from server
- [ ] Streaming `Resource`s
- [ ] Loaders
- [ ] Tutorials + website
- [ ] Scoped CSS

View File

@@ -0,0 +1,18 @@
[package]
name = "counter-hydrate"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["hydrate"] }
wee_alloc = "0.4"
console_log = "0.2"
log = "0.4"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
[profile.release]
codegen-units = 1
lto = true
opt-level = 'z'

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
</head>
<body>
<div id="hydrated" data-hk="0"><button>-1</button><span>Value: <!--#-->0<!--/-->!</span><button>+1</button></div>
<div id="mounted"></div>
</body>
</html>

View File

@@ -0,0 +1,14 @@
use leptos::*;
pub fn simple_counter(cx: Scope) -> web_sys::Element {
let (value, set_value) = create_signal(cx, 0);
log::debug!("ok");
view! {
<div>
<button on:click=move |_| set_value(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value().to_string()} "!"</span>
<button on:click=move |_| set_value(|value| *value += 1)>"+1"</button>
</div>
}
}

View File

@@ -0,0 +1,23 @@
use counter_hydrate::simple_counter;
use leptos::*;
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
pub fn main() {
console_log::init_with_level(log::Level::Debug);
hydrate(
document()
.get_element_by_id("hydrated")
.unwrap()
.unchecked_into(),
simple_counter,
);
mount(
document()
.get_element_by_id("mounted")
.unwrap()
.unchecked_into(),
simple_counter,
);
}

View File

@@ -11,5 +11,8 @@ leptos_reactive = { path = "../leptos_reactive" }
leptos_router = { path = "../router" }
[features]
default = ["browser"]
browser = ["leptos_router/browser", "leptos_reactive/browser"]
default = ["browser", "csr"]
browser = ["leptos_router/browser", "leptos_reactive/browser"]
csr = ["leptos_macro/client"]
ssr = ["leptos_macro/ssr", "leptos_dom/server", "leptos_router/server"]
hydrate = ["leptos_macro/hydrate"]

View File

@@ -51,4 +51,9 @@ features = [
[profile.release]
codegen-units = 1
lto = true
opt-level = 'z'
opt-level = 'z'
[features]
default = ["browser"]
browser = []
server = []

View File

@@ -10,6 +10,32 @@ pub enum Attribute {
Bool(bool),
}
impl Attribute {
pub fn as_value_string(&self, attr_name: &'static str) -> String {
match self {
Attribute::String(value) => format!("{attr_name}=\"{value}\""),
Attribute::Fn(f) => {
let mut value = f();
while let Attribute::Fn(f) = value {
value = f();
}
value.as_value_string(attr_name)
}
Attribute::Option(value) => value
.as_ref()
.map(|value| format!("{attr_name}=\"{value}\""))
.unwrap_or_default(),
Attribute::Bool(include) => {
if *include {
attr_name.to_string()
} else {
String::new()
}
}
}
}
}
impl PartialEq for Attribute {
fn eq(&self, other: &Self) -> bool {
match (self, other) {

View File

@@ -3,7 +3,7 @@ use std::rc::Rc;
use leptos_reactive::Scope;
use wasm_bindgen::JsCast;
type Node = web_sys::Node;
use crate::Node;
#[derive(Clone)]
pub enum Child {
@@ -14,6 +14,25 @@ pub enum Child {
Nodes(Vec<Node>),
}
impl Child {
#[cfg(feature = "server")]
pub fn as_child_string(&self) -> String {
match self {
Child::Null => String::new(),
Child::Text(text) => text.to_string(),
Child::Fn(f) => {
let mut value = f();
while let Child::Fn(f) = value {
value = f();
}
value.as_child_string()
}
Child::Node(node) => node.to_string(),
Child::Nodes(nodes) => nodes.iter().cloned().collect(),
}
}
}
impl std::fmt::Debug for Child {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
@@ -54,18 +73,21 @@ impl IntoChild for String {
}
}
#[cfg(not(feature = "server"))]
impl IntoChild for web_sys::Node {
fn into_child(self, _cx: Scope) -> Child {
Child::Node(self)
}
}
#[cfg(not(feature = "server"))]
impl IntoChild for web_sys::Text {
fn into_child(self, _cx: Scope) -> Child {
Child::Node(self.unchecked_into())
}
}
#[cfg(not(feature = "server"))]
impl IntoChild for web_sys::Element {
fn into_child(self, _cx: Scope) -> Child {
Child::Node(self.unchecked_into())
@@ -84,12 +106,14 @@ where
}
}
#[cfg(not(feature = "server"))]
impl IntoChild for Vec<web_sys::Node> {
fn into_child(self, _cx: Scope) -> Child {
Child::Nodes(self)
}
}
#[cfg(not(feature = "server"))]
impl IntoChild for Vec<web_sys::Element> {
fn into_child(self, _cx: Scope) -> Child {
Child::Nodes(

View File

@@ -1,40 +1,57 @@
mod attribute;
mod child;
mod class;
#[cfg(not(feature = "server"))]
mod event_delegation;
pub mod logging;
#[cfg(not(feature = "server"))]
mod operations;
mod property;
#[cfg(not(feature = "server"))]
mod reconcile;
#[cfg(not(feature = "server"))]
mod render;
pub use attribute::*;
pub use child::*;
pub use class::*;
pub use logging::*;
#[cfg(not(feature = "server"))]
pub use operations::*;
pub use property::*;
#[cfg(not(feature = "server"))]
pub use render::*;
pub use js_sys;
pub use wasm_bindgen;
pub use web_sys;
#[cfg(not(feature = "server"))]
pub type Element = web_sys::Element;
#[cfg(feature = "server")]
pub type Element = String;
#[cfg(not(feature = "server"))]
pub type Node = web_sys::Node;
#[cfg(feature = "server")]
pub type Node = String;
use leptos_reactive::{create_scope, Scope};
pub use wasm_bindgen::UnwrapThrowExt;
#[cfg(not(feature = "server"))]
pub trait Mountable {
fn mount(&self, parent: &web_sys::Element);
}
#[cfg(not(feature = "server"))]
impl Mountable for Element {
fn mount(&self, parent: &web_sys::Element) {
parent.append_child(self).unwrap_throw();
}
}
#[cfg(not(feature = "server"))]
impl Mountable for Vec<Element> {
fn mount(&self, parent: &web_sys::Element) {
for element in self {
@@ -43,6 +60,7 @@ impl Mountable for Vec<Element> {
}
}
#[cfg(not(feature = "server"))]
pub fn mount_to_body<T, F>(f: F)
where
F: Fn(Scope) -> T + 'static,
@@ -51,6 +69,7 @@ where
mount(document().body().unwrap_throw(), f)
}
#[cfg(not(feature = "server"))]
pub fn mount<T, F>(parent: web_sys::HtmlElement, f: F)
where
F: Fn(Scope) -> T + 'static,
@@ -63,6 +82,21 @@ where
});
}
#[cfg(not(feature = "server"))]
pub fn hydrate<T, F>(parent: web_sys::HtmlElement, f: F)
where
F: Fn(Scope) -> T + 'static,
T: Mountable,
{
// running "hydrate" intentionally leaks the memory,
// as the "hydrate" has no parent that can clean it up
let _ = create_scope(move |cx| {
cx.begin_hydration();
(f(cx));
cx.complete_hydration();
});
}
pub fn create_component<F, T>(cx: Scope, f: F) -> T
where
F: Fn() -> T,
@@ -74,7 +108,7 @@ where
#[macro_export]
macro_rules! is_server {
() => {
!cfg!(target_arch = "wasm32")
cfg!(feature = "server")
};
}

View File

@@ -13,3 +13,14 @@ quote = "1"
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
syn-rsx = "0.8.1"
uuid = { version = "1", features = ["v4"] }
[dev-dependencies]
leptos_core = { path = "../leptos_core" }
leptos_dom = { path = "../leptos_dom", features = ["server"] }
leptos_reactive = { path = "../leptos_reactive" }
[features]
default = ["client"]
client = []
hydrate = []
ssr = []

View File

@@ -3,43 +3,34 @@ use quote::ToTokens;
use syn::{parse_macro_input, DeriveInput};
use syn_rsx::{parse, Node, NodeType};
enum Mode {
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum Mode {
Client,
Hydrate,
Dehydrate,
Static,
Ssr,
}
impl Default for Mode {
fn default() -> Self {
Self::Client
if cfg!(feature = "ssr") {
Mode::Ssr
} else if cfg!(feature = "hydrate") {
Mode::Hydrate
} else {
Mode::Client
}
}
}
mod csr;
use csr::client_side_rendering;
mod view;
use view::render_view;
mod component;
mod props;
#[proc_macro]
pub fn view(tokens: TokenStream) -> TokenStream {
match parse(tokens) {
Ok(nodes) => {
let mode = std::env::var("LEPTOS_MODE")
.map(|mode| match mode.to_lowercase().as_str() {
"client" => Mode::Client,
"hydrate" => Mode::Hydrate,
"dehydrate" => Mode::Dehydrate,
"static" => Mode::Static,
_ => Mode::Client,
})
.unwrap_or_default();
match mode {
Mode::Client => client_side_rendering(&nodes),
_ => todo!(),
}
}
Ok(nodes) => render_view(&nodes, Mode::default()),
Err(error) => error.to_compile_error(),
}
.into()

View File

@@ -4,20 +4,20 @@ use syn::{spanned::Spanned, ExprPath};
use syn_rsx::{Node, NodeName, NodeType};
use uuid::Uuid;
use crate::is_component_node;
use crate::{is_component_node, Mode};
pub fn client_side_rendering(nodes: &[Node]) -> TokenStream {
pub(crate) fn render_view(nodes: &[Node], mode: Mode) -> TokenStream {
let template_uid = Ident::new(
&format!("TEMPLATE_{}", Uuid::new_v4().simple()),
Span::call_site(),
);
if nodes.len() == 1 {
first_node_to_tokens(&template_uid, &nodes[0])
first_node_to_tokens(&template_uid, &nodes[0], mode)
} else {
let nodes = nodes
.iter()
.map(|node| first_node_to_tokens(&template_uid, node));
.map(|node| first_node_to_tokens(&template_uid, node, mode));
quote! {
{
vec![
@@ -28,14 +28,14 @@ pub fn client_side_rendering(nodes: &[Node]) -> TokenStream {
}
}
fn first_node_to_tokens(template_uid: &Ident, node: &Node) -> TokenStream {
fn first_node_to_tokens(template_uid: &Ident, node: &Node, mode: Mode) -> TokenStream {
match node.node_type {
NodeType::Doctype | NodeType::Comment => quote! {},
NodeType::Fragment => {
let nodes = node
.children
.iter()
.map(|node| first_node_to_tokens(template_uid, node));
.map(|node| first_node_to_tokens(template_uid, node, mode));
quote! {
{
vec![
@@ -44,7 +44,7 @@ fn first_node_to_tokens(template_uid: &Ident, node: &Node) -> TokenStream {
}
}
}
NodeType::Element => root_element_to_tokens(template_uid, node),
NodeType::Element => root_element_to_tokens(template_uid, node, mode),
NodeType::Block => node
.value
.as_ref()
@@ -60,37 +60,63 @@ fn first_node_to_tokens(template_uid: &Ident, node: &Node) -> TokenStream {
}
}
fn root_element_to_tokens(template_uid: &Ident, node: &Node) -> TokenStream {
fn root_element_to_tokens(template_uid: &Ident, node: &Node, mode: Mode) -> TokenStream {
let mut template = String::new();
let mut navigations = Vec::new();
let mut expressions = Vec::new();
if is_component_node(node) {
create_component(node)
create_component(node, mode)
} else {
element_to_tokens(
node,
&Ident::new("root", Span::call_site()),
None,
&mut 0,
&mut 0,
&mut template,
&mut navigations,
&mut expressions,
true,
mode,
);
quote! {
{
thread_local! {
static #template_uid: web_sys::HtmlTemplateElement = leptos_dom::create_template(#template);
match mode {
Mode::Ssr => {
quote! {
format!(
#template,
#(#expressions),*
)
}
}
_ => {
// create the root element from which navigations and expressions will begin
let generate_root = match mode {
// SSR is just going to return a format string, so no root/navigations
Mode::Ssr => unreachable!(),
// for CSR, just clone the template and take the first child as the root
Mode::Client => quote! {
let root = #template_uid.with(|template| leptos_dom::clone_template(template)).first_element_child().unwrap_throw();
},
// for hydration, use get_next_element(), which will either draw from an SSRed node or clone the template
Mode::Hydrate => quote! {
let root = #template_uid.with(|template| cx.get_next_element(template));
},
};
let root = #template_uid.with(|template| leptos_dom::clone_template(template));
//let root = leptos_dom::clone_template(&leptos_dom::create_template(#template));
#(#navigations);*
#(#expressions);*;
quote! {
thread_local! {
static #template_uid: web_sys::HtmlTemplateElement = leptos_dom::create_template(#template);
};
// returns the first child created in the template
root.first_element_child().unwrap_throw()
#generate_root
#(#navigations);*
#(#expressions);*;
root
}
}
}
}
@@ -109,28 +135,44 @@ fn element_to_tokens(
parent: &Ident,
prev_sib: Option<Ident>,
next_el_id: &mut usize,
next_co_id: &mut usize,
template: &mut String,
navigations: &mut Vec<TokenStream>,
expressions: &mut Vec<TokenStream>,
is_root_el: bool,
mode: Mode,
) -> Ident {
// create this element
*next_el_id += 1;
let this_el_ident = child_ident(*next_el_id, node);
// TEMPLATE: open tag
// TEMPLATE: open tag — true for any mode
let name_str = node.name_as_string().unwrap();
template.push('<');
template.push_str(&name_str);
// for SSR: add a hydration key
if mode == Mode::Ssr && is_root_el {
template.push_str(" data-hk=\"{}\"");
expressions.push(quote! {
cx.next_hydration_key()
});
}
// attributes
for attr in &node.attributes {
attr_to_tokens(attr, &this_el_ident, template, expressions);
attr_to_tokens(attr, &this_el_ident, template, expressions, mode);
}
// navigation for this el
let debug_name = debug_name(node);
let span = span(node);
let this_nav = if let Some(prev_sib) = &prev_sib {
let this_nav = if is_root_el {
quote_spanned! {
span => let #this_el_ident = #debug_name;
let #this_el_ident = #parent.clone().unchecked_into::<web_sys::Node>();
}
} else if let Some(prev_sib) = &prev_sib {
quote_spanned! {
span => let #this_el_ident = #debug_name;
let #this_el_ident = #prev_sib.next_sibling().unwrap_throw();
@@ -138,7 +180,7 @@ fn element_to_tokens(
} else {
quote_spanned! {
span => let #this_el_ident = #debug_name;
let #this_el_ident = "first child wing C"; let #this_el_ident = #parent.first_child().unwrap_throw();
let #this_el_ident = #parent.first_child().unwrap_throw();
}
};
navigations.push(this_nav);
@@ -187,10 +229,12 @@ fn element_to_tokens(
if idx == 0 { None } else { prev_sib.clone() },
next_sib,
next_el_id,
next_co_id,
template,
navigations,
expressions,
multi,
mode,
);
prev_sib = match curr_id {
@@ -200,7 +244,7 @@ fn element_to_tokens(
};
}
// TEMPLATE: close tag
// TEMPLATE: close tag (same for any mode)
template.push_str("</");
template.push_str(&name_str);
template.push('>');
@@ -213,6 +257,7 @@ fn attr_to_tokens(
el_id: &Ident,
template: &mut String,
expressions: &mut Vec<TokenStream>,
mode: Mode,
) {
let name = node
.name_as_string()
@@ -234,48 +279,60 @@ fn attr_to_tokens(
// refs
if name == "ref" {
expressions.push(match &node.value {
Some(expr) => {
if let Some(ident) = expr_to_ident(expr) {
quote_spanned! {
span =>
// we can't pass by reference because the _el won't live long enough (it's dropped when template returns)
// so we clone here; this will be unnecessary if it's the last attribute, but very necessary otherwise
#ident = #el_id.clone().unchecked_into::<web_sys::Element>();
// refs mean nothing in SSR
if mode != Mode::Ssr {
expressions.push(match &node.value {
Some(expr) => {
if let Some(ident) = expr_to_ident(expr) {
quote_spanned! {
span =>
// we can't pass by reference because the _el won't live long enough (it's dropped when template returns)
// so we clone here; this will be unnecessary if it's the last attribute, but very necessary otherwise
#ident = #el_id.clone().unchecked_into::<web_sys::Element>();
}
} else {
panic!("'ref' needs to be passed a variable name")
}
} else {
panic!("'ref' needs to be passed a variable name")
}
}
_ => panic!("'ref' needs to be passed a variable name"),
})
_ => panic!("'ref' needs to be passed a variable name"),
})
}
}
// Event Handlers
else if name.starts_with("on:") {
let event_name = name.replacen("on:", "", 1);
let handler = node
.value
.as_ref()
.expect("event listener attributes need a value");
expressions.push(quote_spanned! {
span => add_event_listener(#el_id.unchecked_ref(), #event_name, #handler);
});
if mode != Mode::Ssr {
let event_name = name.replacen("on:", "", 1);
let handler = node
.value
.as_ref()
.expect("event listener attributes need a value");
expressions.push(quote_spanned! {
span => add_event_listener(#el_id.unchecked_ref(), #event_name, #handler);
});
}
}
// Properties
else if name.starts_with("prop:") {
let name = name.replacen("prop:", "", 1);
let value = node.value.as_ref().expect("prop: blocks need values");
expressions.push(quote_spanned! {
// can't set properties in SSR
if mode != Mode::Ssr {
let name = name.replacen("prop:", "", 1);
let value = node.value.as_ref().expect("prop: blocks need values");
expressions.push(quote_spanned! {
span => leptos_dom::property(cx, #el_id.unchecked_ref(), #name, #value.into_property(cx))
});
}
}
// Classes
else if name.starts_with("class:") {
let name = name.replacen("class:", "", 1);
let value = node.value.as_ref().expect("class: attributes need values");
expressions.push(quote_spanned! {
span => leptos_dom::class(cx, #el_id.unchecked_ref(), #name, #value.into_class(cx))
});
if mode == Mode::Ssr {
todo!()
} else {
let name = name.replacen("class:", "", 1);
let value = node.value.as_ref().expect("class: attributes need values");
expressions.push(quote_spanned! {
span => leptos_dom::class(cx, #el_id.unchecked_ref(), #name, #value.into_class(cx))
});
}
}
// Attributes
else {
@@ -297,12 +354,20 @@ fn attr_to_tokens(
template.push('"');
}
// For client-side rendering, dynamic attributes don't need to be rendered in the template
// They'll immediately be set synchronously before the cloned template is mounted
// Dynamic attributes are handled differently depending on the rendering mode
AttributeValue::Dynamic(value) => {
expressions.push(quote_spanned! {
span => leptos_dom::attribute(cx, #el_id.unchecked_ref(), #name, #value.into_attribute(cx))
});
if mode == Mode::Ssr {
template.push_str(" {}");
expressions.push(quote_spanned! {
span => #value.into_attribute(cx).as_value_string(#name)
});
} else {
// For client-side rendering, dynamic attributes don't need to be rendered in the template
// They'll immediately be set synchronously before the cloned template is mounted
expressions.push(quote_spanned! {
span => leptos_dom::attribute(cx, #el_id.unchecked_ref(), #name, #value.into_attribute(cx))
});
}
}
}
}
@@ -321,24 +386,37 @@ fn child_to_tokens(
prev_sib: Option<Ident>,
next_sib: Option<Ident>,
next_el_id: &mut usize,
next_co_id: &mut usize,
template: &mut String,
navigations: &mut Vec<TokenStream>,
expressions: &mut Vec<TokenStream>,
multi: bool,
mode: Mode,
) -> PrevSibChange {
match node.node_type {
NodeType::Element => {
if is_component_node(node) {
component_to_tokens(node, Some(parent), next_sib, expressions, multi)
component_to_tokens(
node,
Some(parent),
next_sib,
template,
expressions,
multi,
mode,
)
} else {
PrevSibChange::Sib(element_to_tokens(
node,
parent,
prev_sib,
next_el_id,
next_co_id,
template,
navigations,
expressions,
false,
mode,
))
}
}
@@ -370,8 +448,7 @@ fn child_to_tokens(
}
} else {
quote_spanned! {
span => let #name = "first child wing A";
let #name = #parent.first_child().unwrap_throw();
span => let #name = #parent.first_child().unwrap_throw();
}
};
navigations.push(location);
@@ -382,17 +459,43 @@ fn child_to_tokens(
*next_el_id += 1;
let name = child_ident(*next_el_id, node);
template.push_str("<!>");
let location = if let Some(sibling) = &prev_sib {
quote_spanned! {
span => let #name = #sibling.next_sibling().unwrap_throw();
}
} else {
quote_spanned! {
span => let #name = "first child wing B"; let #name = #parent.first_child().unwrap_throw();
span => let #name = #parent.first_child().unwrap_throw();
}
};
navigations.push(location);
// these markers are one of the primary templating differences across modes
match mode {
// in CSR, simply insert a comment node: it will be picked up and replaced with the value
Mode::Client => {
template.push_str("<!>");
navigations.push(location);
}
// when hydrating, a text node will be generated by SSR; in the hydration/CSR template,
// wrap it with comments that mark where it begins and ends
Mode::Hydrate => {
*next_el_id += 1;
let el = child_ident(*next_el_id, node);
*next_co_id += 1;
let co = comment_ident(*next_co_id, node);
template.push_str("<!#><!/>");
navigations.push(quote! {
#location;
let (#el, #co) = cx.get_next_marker(#name);
});
}
// in SSR, it needs to both wrap with comments and insert a hole for an existing value
Mode::Ssr => {
template.push_str("<!--#-->{}<!--/-->");
// TODO push expression here, or elsewhere?
}
}
let before = match &next_sib {
Some(child) => quote! { leptos::Marker::BeforeChild(#child.clone()) },
@@ -407,15 +510,21 @@ fn child_to_tokens(
let value = node.value_as_block().expect("no block value");
expressions.push(quote! {
leptos::insert(
cx,
#parent.clone(),
#value.into_child(cx),
#before,
None,
);
});
if mode == Mode::Ssr {
expressions.push(quote! {
#value.into_child(cx).as_child_string()
});
} else {
expressions.push(quote! {
leptos::insert(
cx,
#parent.clone(),
#value.into_child(cx),
#before,
None,
);
});
}
PrevSibChange::Sib(name)
} else {
@@ -433,15 +542,21 @@ fn child_to_tokens(
let value = node.value_as_block().expect("no block value");
expressions.push(quote! {
leptos::insert(
cx,
#parent.clone(),
#value.into_child(cx),
#before,
None,
);
});
if mode == Mode::Ssr {
expressions.push(quote! {
#value.into_child(cx).as_child_string()
});
} else {
expressions.push(quote! {
leptos::insert(
cx,
#parent.clone(),
#value.into_child(cx),
#before,
None,
);
});
}
PrevSibChange::Skip
}
@@ -455,10 +570,12 @@ fn component_to_tokens(
node: &Node,
parent: Option<&Ident>,
next_sib: Option<Ident>,
template: &mut String,
expressions: &mut Vec<TokenStream>,
multi: bool,
mode: Mode,
) -> PrevSibChange {
let create_component = create_component(node);
let create_component = create_component(node, mode);
if let Some(parent) = parent {
let before = match &next_sib {
@@ -472,15 +589,22 @@ fn component_to_tokens(
}
};
expressions.push(quote! {
leptos::insert(
cx,
#parent.clone(),
#create_component.into_child(cx),
#before,
None,
);
});
if mode == Mode::Ssr {
template.push_str("<!--#-->{}<!--/-->");
expressions.push(quote! {
#create_component.into_child(cx).as_child_string()
});
} else {
expressions.push(quote! {
leptos::insert(
cx,
#parent.clone(),
#create_component.into_child(cx),
#before,
None,
);
});
}
} else {
expressions.push(create_component)
}
@@ -488,7 +612,7 @@ fn component_to_tokens(
PrevSibChange::Skip
}
fn create_component(node: &Node) -> TokenStream {
fn create_component(node: &Node, mode: Mode) -> TokenStream {
let component_name = ident_from_tag_name(node.name.as_ref().unwrap());
let span = node.name_span().unwrap();
let component_props_name = Ident::new(&format!("{component_name}Props"), span);
@@ -496,10 +620,10 @@ fn create_component(node: &Node) -> TokenStream {
let children = if node.children.is_empty() {
quote! {}
} else if node.children.len() == 1 {
let child = client_side_rendering(&node.children);
let child = render_view(&node.children, mode);
quote! { .children(vec![#child]) }
} else {
let children = client_side_rendering(&node.children);
let children = render_view(&node.children, mode);
quote! { .children(#children) }
};
@@ -600,6 +724,15 @@ fn child_ident(el_id: usize, node: &Node) -> Ident {
}
}
fn comment_ident(co_id: usize, node: &Node) -> Ident {
let id = format!("_co{co_id}");
match node.node_type {
NodeType::Element => Ident::new(&id, node.name_span().unwrap()),
NodeType::Text | NodeType::Block => Ident::new(&id, node.value.as_ref().unwrap().span()),
_ => panic!("invalid child node type"),
}
}
fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
match tag_name {
NodeName::Path(path) => path

59
leptos_macro/tests/ssr.rs Normal file
View File

@@ -0,0 +1,59 @@
#[cfg(feature = "ssr")]
#[test]
fn simple_ssr_test() {
use leptos_dom::*;
use leptos_macro::view;
use leptos_reactive::{create_scope, create_signal};
_ = create_scope(|cx| {
let (value, set_value) = create_signal(cx, 0);
let rendered = view! {
<div>
<button on:click=move |_| set_value(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value().to_string()} "!"</span>
<button on:click=move |_| set_value(|value| *value += 1)>"+1"</button>
</div>
};
assert_eq!(
rendered,
r#"<div data-hk="0"><button>-1</button><span>Value: <!--#-->0<!--/-->!</span><button>+1</button></div>"# //r#"<div data-hk="0" id="hydrated" data-hk="0"><button>-1</button><span>Value: <!--#-->0<!--/-->!</span><button>+1</button></div>"#
);
});
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_test_with_components() {
use leptos_core as leptos;
use leptos_core::Prop;
use leptos_dom::*;
use leptos_macro::*;
use leptos_reactive::{create_scope, create_signal, Scope};
#[component]
fn Counter(cx: Scope, initial_value: i32) -> Element {
let (value, set_value) = create_signal(cx, 0);
view! {
<div>
<button on:click=move |_| set_value(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value().to_string()} "!"</span>
<button on:click=move |_| set_value(|value| *value += 1)>"+1"</button>
</div>
}
}
_ = create_scope(|cx| {
let rendered = view! {
<div class="counters">
<Counter initial_value=1/>
<Counter initial_value=2/>
</div>
};
assert_eq!(
rendered,
r#"<div data-hk="0" class="counters"><!--#--><div data-hk="1"><button>-1</button><span>Value: <!--#-->0<!--/-->!</span><button>+1</button></div><!--/--><!--#--><div data-hk="2"><button>-1</button><span>Value: <!--#-->0<!--/-->!</span><button>+1</button></div><!--/--></div>"#
);
});
}

View File

@@ -3,12 +3,14 @@ use crate::{
ScopeState, SignalId, SignalState, Subscriber, TransitionState,
};
use slotmap::SlotMap;
use std::cell::RefCell;
use std::cell::{Cell, RefCell};
use std::fmt::Debug;
use std::rc::Rc;
#[derive(Default, Debug)]
pub(crate) struct Runtime {
pub(crate) is_hydrating: Cell<bool>,
pub(crate) hydration_key: Cell<usize>,
pub(crate) stack: RefCell<Vec<Subscriber>>,
pub(crate) scopes: RefCell<SlotMap<ScopeId, Rc<ScopeState>>>,
pub(crate) transition: RefCell<Option<Rc<TransitionState>>>,
@@ -147,6 +149,24 @@ impl Runtime {
self.stack.replace(prev_stack);
untracked_result
}
pub fn is_hydrating(&self) -> bool {
self.is_hydrating.get()
}
pub fn begin_hydration(&self) {
self.is_hydrating.set(true);
}
pub fn complete_hydration(&self) {
self.is_hydrating.set(false);
}
pub fn next_hydration_key(&self) -> usize {
let next = self.hydration_key.get();
self.hydration_key.set(next + 1);
next
}
}
impl PartialEq for Runtime {

View File

@@ -83,6 +83,22 @@ impl Scope {
})
// removing from the runtime will drop this Scope, and all its Signals/Effects/Memos
}
pub fn begin_hydration(&self) {
self.runtime.begin_hydration();
}
pub fn complete_hydration(&self) {
self.runtime.complete_hydration();
}
pub fn is_hydrating(&self) -> bool {
self.runtime.is_hydrating()
}
pub fn next_hydration_key(&self) -> usize {
self.runtime.next_hydration_key()
}
}
pub struct ScopeDisposer(pub(crate) Box<dyn FnOnce()>);

View File

@@ -35,4 +35,5 @@ features = [
[features]
default = ["dep:url", "dep:regex"]
browser = ["dep:js-sys"]
browser = ["dep:js-sys", "leptos_dom/browser"]
server = ["leptos_dom/server"]