Merge branch 'leptos_dom_v2' of https://github.com/jquesada2016/leptos into leptos_dom_v2

This commit is contained in:
Jose Quesada 2022-12-14 12:24:16 -06:00
commit 5f95776a08
11 changed files with 352 additions and 89 deletions

View File

@ -28,15 +28,16 @@ fn leptos_ssr_bench(b: &mut Bencher) {
<Counter initial=2/>
<Counter initial=3/>
</main>
}.into_view(cx).render_to_string();
}.into_view(cx).render_to_string(cx);
assert!(
!rendered.is_empty()
assert_eq!(
rendered,
"<main><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><div><button>-1</button><span>Value: <!>1<template id=\"_3\"></template>!</span><button>+1</button></div><template id=\"_1\"></template><div><button>-1</button><span>Value: <!>2<template id=\"_2\"></template>!</span><button>+1</button></div><template id=\"_0\"></template><div><button>-1</button><span>Value: <!>3<template id=\"_2\"></template>!</span><button>+1</button></div><template id=\"_0\"></template></main>"
);
});
});
}
/*
#[bench]
fn tera_ssr_bench(b: &mut Bencher) {
use tera::*;
@ -193,3 +194,4 @@ fn yew_ssr_bench(b: &mut Bencher) {
});
});
}
*/

View File

@ -1,4 +1,4 @@
use leptos::*;
pub use leptos::*;
use miniserde::*;
use web_sys::HtmlInputElement;
@ -103,7 +103,7 @@ const ESCAPE_KEY: u32 = 27;
const ENTER_KEY: u32 = 13;
#[component]
pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
pub fn TodoMVC(cx: Scope,todos: Todos) -> impl IntoView {
let mut next_id = todos
.0
.iter()

View File

@ -8,14 +8,13 @@ mod yew;
#[bench]
fn leptos_todomvc_ssr(b: &mut Bencher) {
b.iter(|| {
use self::leptos::*;
use ::leptos::*;
use crate::todomvc::leptos::*;
_ = create_scope(create_runtime(), |cx| {
let rendered = view! {
cx,
<TodoMVC todos=Todos::new(cx)/>
}.into_view(cx).render_to_string();
}.into_view(cx).render_to_string(cx);
assert!(rendered.len() > 1);
});
@ -67,7 +66,7 @@ fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
let rendered = view! {
cx,
<TodoMVC todos=Todos::new_with_1000(cx)/>
}.into_view(cx).render_to_string();
}.into_view(cx).render_to_string(cx);
assert!(rendered.len() > 1);
});

View File

@ -204,7 +204,7 @@ where IV: IntoView
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="modulepreload" href="{pkg_path}.js">
<link rel="preload" href="{pkg_path}.wasm" as="fetch" type="application/wasm" crossorigin="">
<link rel="preload" href="{pkg_path}_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init('{pkg_path}_bg.wasm').then(hydrate);</script>
{leptos_autoreload}
"#

View File

@ -133,3 +133,13 @@ pub fn window_event_listener(
window().add_event_listener_with_callback(event_name, cb.unchecked_ref());
}
}
#[doc(hidden)]
/// This exists only to enable type inference on event listeners when in SSR mode.
pub fn ssr_event_listener<E: crate::ev::EventDescriptor + 'static>(
event: E,
event_handler: impl FnMut(E::EventType) + 'static,
) {
_ = event;
_ = event_handler;
}

View File

@ -205,6 +205,8 @@ cfg_if! {
#[educe(Debug(ignore))]
#[allow(clippy::type_complexity)]
pub(crate) children: SmallVec<[View; 4]>,
#[educe(Debug(ignore))]
pub(crate) prerendered: Option<Cow<'static, str>>
}
}
}
@ -235,11 +237,24 @@ impl<El: IntoElement> HtmlElement<El> {
attrs: smallvec![],
children: smallvec![],
element,
prerendered: None
}
}
}
}
#[doc(hidden)]
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub fn from_html(cx: Scope, element: El, html: impl Into<Cow<'static, str>>) -> Self {
Self {
cx,
attrs: smallvec![],
children: smallvec![],
element,
prerendered: Some(html.into())
}
}
/// Converts this element into [`HtmlElement<AnyElement>`].
pub fn into_any(self) -> HtmlElement<AnyElement> {
cfg_if! {
@ -263,12 +278,14 @@ impl<El: IntoElement> HtmlElement<El> {
attrs,
children,
element,
prerendered
} = self;
HtmlElement {
cx,
attrs,
children,
prerendered,
element: AnyElement {
name: element.name(),
is_void: element.is_void(),
@ -546,6 +563,7 @@ impl<El: IntoElement> IntoView for HtmlElement<El> {
element,
mut attrs,
children,
prerendered,
..
} = self;
@ -562,6 +580,7 @@ impl<El: IntoElement> IntoView for HtmlElement<El> {
element.attrs = attrs;
element.children.extend(children);
element.prerendered = prerendered;
View::Element(element)
}

View File

@ -46,11 +46,12 @@ impl HydrationCtx {
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) fn set_id(cx: Scope) {
let new_id = if let Some(id) = cx.get_hydration_key() {
/* let new_id = if let Some(id) = cx.get_hydration_key() {
id + 1
} else {
0
};
}; */
let new_id = 0;
println!("setting ID to {new_id}");
unsafe { ID = new_id };
}

View File

@ -148,6 +148,7 @@ cfg_if! {
is_void: bool,
attrs: SmallVec<[(Cow<'static, str>, Cow<'static, str>); 4]>,
children: Vec<View>,
prerendered: Option<Cow<'static, str>>,
id: usize,
}
@ -203,6 +204,7 @@ impl Element {
attrs,
children,
id,
prerendered
} = self;
let element = AnyElement { name, is_void, id };
@ -212,6 +214,7 @@ impl Element {
element,
attrs,
children: children.into_iter().collect(),
prerendered
}
}
}
@ -242,10 +245,24 @@ impl Element {
attrs: Default::default(),
children: Default::default(),
id: el.hydration_id(),
prerendered: None
}
}
}
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
#[track_caller]
fn from_html<El: IntoElement>(el: El, html: impl Into<Cow<'static, str>>) -> Self {
Self {
name: el.name(),
is_void: el.is_void(),
attrs: Default::default(),
children: Default::default(),
id: el.hydration_id(),
prerendered: Some(html.into())
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]

View File

@ -154,12 +154,12 @@ pub fn render_to_stream_with_prefix(
impl View {
/// Consumes the node and renders it into an HTML string.
pub fn render_to_string(self, cx: Scope) -> Cow<'static, str> {
cx.set_hydration_key(HydrationCtx::current_id());
//cx.set_hydration_key(HydrationCtx::current_id());
HydrationCtx::set_id(cx);
let s = self.render_to_string_helper();
cx.set_hydration_key(HydrationCtx::current_id());
//cx.set_hydration_key(HydrationCtx::current_id());
s
}
@ -279,34 +279,38 @@ impl View {
}
}
View::Element(el) => {
let tag_name = el.name;
let attrs = el
.attrs
.into_iter()
.map(|(name, value)| -> Cow<'static, str> {
if value.is_empty() {
format!(" {name}").into()
} else {
format!(
" {name}=\"{}\"",
html_escape::encode_double_quoted_attribute(&value)
)
.into()
}
})
.join("");
if el.is_void {
format!("<{tag_name}{attrs}/>").into()
if let Some(prerendered) = el.prerendered {
prerendered
} else {
let children = el
.children
.into_iter()
.map(|node| node.render_to_string_helper())
.join("");
let tag_name = el.name;
format!("<{tag_name}{attrs}>{children}</{tag_name}>").into()
let attrs = el
.attrs
.into_iter()
.map(|(name, value)| -> Cow<'static, str> {
if value.is_empty() {
format!(" {name}").into()
} else {
format!(
" {name}=\"{}\"",
html_escape::encode_double_quoted_attribute(&value)
)
.into()
}
})
.join("");
if el.is_void {
format!("<{tag_name}{attrs}/>").into()
} else {
let children = el
.children
.into_iter()
.map(|node| node.render_to_string_helper())
.join("");
format!("<{tag_name}{attrs}>{children}</{tag_name}>").into()
}
}
}
View::Transparent(_) => Default::default(),

View File

@ -233,7 +233,9 @@ pub fn view(tokens: TokenStream) -> TokenStream {
Ok(nodes) => render_view(
&proc_macro2::Ident::new(&cx.to_string(), cx.span().into()),
&nodes,
Mode::default(),
// swap to Mode::default() to use faster SSR templating
Mode::Client
//Mode::default(),
),
Err(error) => error.to_compile_error(),
}

View File

@ -139,26 +139,58 @@ pub(crate) fn render_view(
nodes: &[Node],
mode: Mode,
) -> TokenStream {
if nodes.is_empty() {
let span = Span::call_site();
quote_spanned! {
span => leptos::Unit
if mode == Mode::Ssr {
if nodes.is_empty() {
let span = Span::call_site();
quote_spanned! {
span => leptos::Unit
}
} else if nodes.len() == 1 {
root_node_to_tokens_ssr(cx, &nodes[0])
} else {
fragment_to_tokens_ssr(cx, Span::call_site(), nodes)
}
} else if nodes.len() == 1 {
node_to_tokens(cx, &nodes[0], mode)
} else {
fragment_to_tokens(cx, Span::call_site(), nodes, mode)
if nodes.is_empty() {
let span = Span::call_site();
quote_spanned! {
span => leptos::Unit
}
} else if nodes.len() == 1 {
node_to_tokens(cx, &nodes[0])
} else {
fragment_to_tokens(cx, Span::call_site(), nodes)
}
}
}
fn fragment_to_tokens(
cx: &Ident,
span: Span,
nodes: &[Node],
mode: Mode,
) -> TokenStream {
fn root_node_to_tokens_ssr(cx: &Ident, node: &Node) -> TokenStream {
match node {
Node::Fragment(fragment) => {
fragment_to_tokens_ssr(cx, Span::call_site(), &fragment.children)
}
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
Node::Text(node) => {
let span = node.value.span();
let value = node.value.as_ref();
quote_spanned! {
span => leptos::text(#value)
}
}
Node::Block(node) => {
let span = node.value.span();
let value = node.value.as_ref();
quote_spanned! {
span => #value
}
}
Node::Element(node) => root_element_to_tokens_ssr(cx, node),
}
}
fn fragment_to_tokens_ssr(cx: &Ident, span: Span, nodes: &[Node]) -> TokenStream {
let nodes = nodes.iter().map(|node| {
let node = node_to_tokens(cx, node, mode);
let node = root_node_to_tokens_ssr(cx, node);
let span = node.span();
quote_spanned! {
span => #node.into_view(#cx),
@ -173,17 +205,195 @@ fn fragment_to_tokens(
}
}
fn node_to_tokens(cx: &Ident, node: &Node, mode: Mode) -> TokenStream {
fn root_element_to_tokens_ssr(cx: &Ident, node: &NodeElement) -> TokenStream {
let mut template = String::new();
let mut holes = Vec::<TokenStream>::new();
let mut exprs_for_compiler = Vec::<TokenStream>::new();
let span = node.name.span();
element_to_tokens_ssr(cx, node, &mut template, &mut holes, &mut exprs_for_compiler);
let template = if holes.is_empty() {
quote! {
#template
}
} else {
quote! {
format!(
#template,
#(#holes)*
)
}
};
// TODO get proper element types for return-type purposes
quote_spanned! {
span => {
#(#exprs_for_compiler)*
::leptos::HtmlElement::from_html(cx, leptos::Div::default(), #template)
}
}
}
fn element_to_tokens_ssr(cx: &Ident, node: &NodeElement, template: &mut String, holes: &mut Vec<TokenStream>, exprs_for_compiler: &mut Vec<TokenStream>) {
let span = node.name.span();
if is_component_node(node) {
template.push_str("{}");
let component = component_to_tokens(cx, node);
holes.push(quote_spanned! {
span => {#component}.into_view(cx).render_to_string(cx),
})
} else {
template.push('<');
template.push_str(&node.name.to_string());
for attr in &node.attributes {
if let Node::Attribute(attr) = attr {
attribute_to_tokens_ssr(cx, attr, template, holes, exprs_for_compiler);
}
}
// TODO: handle classes
if is_self_closing(node) {
template.push_str("/>");
} else {
template.push('>');
for child in &node.children {
match child {
Node::Element(child) => element_to_tokens_ssr(cx, child, template, holes, exprs_for_compiler),
Node::Text(text) => {
if let Some(value) = value_to_string(&text.value) {
template.push_str(&value);
} else {
template.push_str("{}");
let value = text.value.as_ref();
let span = text.value.span();
holes.push(quote_spanned! {
span => #value.into_view(#cx).render_to_string(#cx),
})
}
},
Node::Block(block) => {
if let Some(value) = value_to_string(&block.value) {
template.push_str(&value);
} else {
template.push_str("{}");
let value = block.value.as_ref();
let span = block.value.span();
holes.push(quote_spanned! {
span => #value.into_view(#cx).render_to_string(#cx),
})
}
},
Node::Fragment(_) => todo!(),
_ => {}
}
}
template.push_str("</");
template.push_str(&node.name.to_string());
template.push('>');
}
}
}
fn value_to_string(value: &syn_rsx::NodeValueExpr) -> Option<String> {
match &value.as_ref() {
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Str(s) => Some(s.value()),
syn::Lit::Char(c) => Some(c.value().to_string()),
syn::Lit::Int(i) => Some(i.base10_digits().to_string()),
syn::Lit::Float(f) => Some(f.base10_digits().to_string()),
_ => None,
},
_ => None,
}
}
fn attribute_to_tokens_ssr(cx: &Ident, node: &NodeAttribute, template: &mut String, holes: &mut Vec<TokenStream>, exprs_for_compiler: &mut Vec<TokenStream>) {
let span = node.key.span();
let name = node.key.to_string();
if name == "ref" || name == "_ref" {
// ignore refs on SSR
} else if let Some(name) = name.strip_prefix("on:") {
let span = name.span();
let handler = node
.value
.as_ref()
.expect("event listener attributes need a value")
.as_ref();
let event_type = TYPED_EVENTS
.iter()
.find(|e| **e == name)
.copied()
.unwrap_or("Custom");
let event_type = event_type
.parse::<TokenStream>()
.expect("couldn't parse event name");
exprs_for_compiler.push(quote_spanned! {
span => leptos::ssr_event_listener(leptos::ev::#event_type, #handler);
})
} else if let Some(name) = name.strip_prefix("prop:") {
// ignore props for SSR
} else if let Some(name) = name.strip_prefix("class:") {
// ignore classes: we'll handle these separately
} else {
let name = name.replacen("attr:", "", 1);
template.push(' ');
template.push_str(&name);
if let Some(value) = node.value.as_ref() {
if let Some(value) = value_to_string(value) {
template.push_str(&value);
} else {
template.push_str("{}");
let span = value.span();
let value = value.as_ref();
holes.push(quote_spanned! {
span => leptos::escape_attr(&{#value}.into_attribute(#cx).as_value_string(#name)),
})
}
template.push('"');
}
}
}
fn fragment_to_tokens(
cx: &Ident,
span: Span,
nodes: &[Node],
) -> TokenStream {
let nodes = nodes.iter().map(|node| {
let node = node_to_tokens(cx, node);
let span = node.span();
quote_spanned! {
span => #node.into_view(#cx),
}
});
quote_spanned! {
span => {
leptos::Fragment::new(vec![
#(#nodes)*
])
}
}
}
fn node_to_tokens(cx: &Ident, node: &Node) -> TokenStream {
match node {
Node::Fragment(fragment) => {
fragment_to_tokens(cx, Span::call_site(), &fragment.children, mode)
fragment_to_tokens(cx, Span::call_site(), &fragment.children)
}
Node::Comment(_) | Node::Doctype(_) => quote! {},
Node::Text(node) => {
let span = node.value.span();
let value = node.value.as_ref();
quote_spanned! {
span => text(#value)
span => leptos::text(#value)
}
}
Node::Block(node) => {
@ -193,19 +403,18 @@ fn node_to_tokens(cx: &Ident, node: &Node, mode: Mode) -> TokenStream {
span => #value
}
}
Node::Attribute(node) => attribute_to_tokens(cx, node, mode),
Node::Element(node) => element_to_tokens(cx, node, mode),
Node::Attribute(node) => attribute_to_tokens(cx, node),
Node::Element(node) => element_to_tokens(cx, node),
}
}
fn element_to_tokens(
cx: &Ident,
node: &NodeElement,
mode: Mode,
) -> TokenStream {
let span = node.name.span();
if is_component_node(node) {
component_to_tokens(cx, node, mode)
component_to_tokens(cx, node)
} else {
let name = if is_custom_element(&node.name) {
let name = node.name.to_string();
@ -216,7 +425,7 @@ fn element_to_tokens(
};
let attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
Some(attribute_to_tokens(cx, node, mode))
Some(attribute_to_tokens(cx, node))
} else {
None
}
@ -224,7 +433,7 @@ fn element_to_tokens(
let children = node.children.iter().map(|node| {
let child = match node {
Node::Fragment(fragment) => {
fragment_to_tokens(cx, Span::call_site(), &fragment.children, mode)
fragment_to_tokens(cx, Span::call_site(), &fragment.children)
}
Node::Text(node) => {
let span = node.value.span();
@ -240,7 +449,7 @@ fn element_to_tokens(
span => #[allow(unused_braces)] #value
}
}
Node::Element(node) => element_to_tokens(cx, node, mode),
Node::Element(node) => element_to_tokens(cx, node),
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
};
quote! {
@ -258,12 +467,10 @@ fn element_to_tokens(
fn attribute_to_tokens(
cx: &Ident,
node: &NodeAttribute,
_mode: Mode,
) -> TokenStream {
let span = node.key.span();
let name = node.key.to_string();
if name == "ref" || name == "_ref" {
//if mode != Mode::Ssr {
let value = node
.value
.as_ref()
@ -272,11 +479,7 @@ fn attribute_to_tokens(
quote_spanned! {
span => .node_ref(#value)
}
/* } else {
todo!()
} */
} else if let Some(name) = name.strip_prefix("on:") {
//if mode != Mode::Ssr {
let span = name.span();
let handler = node
.value
@ -295,35 +498,24 @@ fn attribute_to_tokens(
quote_spanned! {
span => .on(leptos::ev::#event_type, #handler)
}
/* } else {
todo!()
} */
} else if let Some(name) = name.strip_prefix("prop:") {
let value = node
.value
.as_ref()
.expect("prop: attributes need a value")
.as_ref();
//if mode != Mode::Ssr {
quote_spanned! {
span => .prop(#name, (#cx, #[allow(unused_braces)] #value))
}
/* } else {
todo!()
} */
} else if let Some(name) = name.strip_prefix("class:") {
let value = node
.value
.as_ref()
.expect("class: attributes need a value")
.as_ref();
//if mode != Mode::Ssr {
quote_spanned! {
span => .class(#name, (#cx, #[allow(unused_braces)] #value))
}
/* } else {
todo!()
} */
} else {
let name = name.replacen("attr:", "", 1);
let value = match node.value.as_ref() {
@ -334,20 +526,15 @@ fn attribute_to_tokens(
}
None => quote_spanned! { span => "" },
};
//if mode != Mode::Ssr {
quote_spanned! {
span => .attr(#name, (#cx, #value))
}
/* } else {
quote! { }
} */
}
}
fn component_to_tokens(
cx: &Ident,
node: &NodeElement,
mode: Mode,
) -> TokenStream {
let name = &node.name;
let component_name = ident_from_tag_name(&node.name);
@ -358,7 +545,7 @@ fn component_to_tokens(
let children = if node.children.is_empty() {
quote! { }
} else {
let children = fragment_to_tokens(cx, span, &node.children, mode);
let children = fragment_to_tokens(cx, span, &node.children);
quote! { .children(Box::new(move |#cx| #children)) }
};
@ -434,3 +621,25 @@ fn expr_to_ident(expr: &syn::Expr) -> Option<&ExprPath> {
fn is_custom_element(name: &NodeName) -> bool {
name.to_string().contains('-')
}
fn is_self_closing(node: &NodeElement) -> bool {
// self-closing tags
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
matches!(
node.name.to_string().as_str(),
"area"
| "base"
| "br"
| "col"
| "embed"
| "hr"
| "img"
| "input"
| "link"
| "meta"
| "param"
| "source"
| "track"
| "wbr"
)
}