Merge branch 'master' into pr/atty303/1349

This commit is contained in:
Evan Almloff committed 2024-01-06 08:52:37 -06:00
commit e5df83ceec
278 files changed
+11299 -3674

No files matched your search

+3
View File
@@ -10,6 +10,7 @@ keywords = ["dom", "ui", "gui", "react", "ssr"]
[dependencies]
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-html = { workspace = true, features = ["eval"]}
askama_escape = "0.10.3"
thiserror = "1.0.23"
rustc-hash = "1.1.0"
@@ -17,6 +18,8 @@ lru = "0.10.0"
tracing = { workspace = true }
http = "0.2.9"
tokio = { version = "1.28", features = ["full"], optional = true }
async-trait = "0.1.58"
serde_json = { version = "1.0" }
[dev-dependencies]
dioxus = { workspace = true }
+26 -8
View File
@@ -27,6 +27,10 @@ pub enum Segment {
},
/// A marker for where to insert a dynamic inner html
InnerHtmlMarker,
/// A marker for where to insert a node id for an attribute
AttributeNodeMarker,
/// A marker for where to insert a node id for a root node
RootNodeMarker,
}
impl std::fmt::Write for StringChain {
@@ -41,13 +45,13 @@ impl std::fmt::Write for StringChain {
}
impl StringCache {
pub fn from_template(template: &VNode) -> Result<Self, std::fmt::Error> {
pub fn from_template(template: &VNode, prerender: bool) -> Result<Self, std::fmt::Error> {
let mut chain = StringChain::default();
let mut cur_path = vec![];
for (root_idx, root) in template.template.get().roots.iter().enumerate() {
Self::recurse(root, &mut cur_path, root_idx, &mut chain)?;
Self::recurse(root, &mut cur_path, root_idx, true, prerender, &mut chain)?;
}
Ok(Self {
@@ -60,6 +64,8 @@ impl StringCache {
root: &TemplateNode,
cur_path: &mut Vec<usize>,
root_idx: usize,
is_root: bool,
prerender: bool,
chain: &mut StringChain,
) -> Result<(), std::fmt::Error> {
match root {
@@ -76,7 +82,7 @@ impl StringCache {
// we need to collect the inner html and write it at the end
let mut inner_html = None;
// we need to keep track of if we have dynamic attrs to know if we need to insert a style and inner_html marker
let mut has_dynamic_attrs = false;
let mut has_dyn_attrs = false;
for attr in *attrs {
match attr {
TemplateAttribute::Static {
@@ -97,8 +103,9 @@ impl StringCache {
}
}
TemplateAttribute::Dynamic { id: index } => {
chain.segments.push(Segment::Attr(*index));
has_dynamic_attrs = true;
let index = *index;
chain.segments.push(Segment::Attr(index));
has_dyn_attrs = true
}
}
}
@@ -113,12 +120,23 @@ impl StringCache {
inside_style_tag: true,
});
write!(chain, "\"")?;
} else if has_dynamic_attrs {
} else if has_dyn_attrs {
chain.segments.push(Segment::StyleMarker {
inside_style_tag: false,
});
}
// write the id if we are prerendering and this is either a root node or a node with a dynamic attribute
if prerender && (has_dyn_attrs || is_root) {
write!(chain, " data-node-hydration=\"")?;
if has_dyn_attrs {
chain.segments.push(Segment::AttributeNodeMarker);
} else if is_root {
chain.segments.push(Segment::RootNodeMarker);
}
write!(chain, "\"")?;
}
if children.is_empty() && tag_is_self_closing(tag) {
write!(chain, "/>")?;
} else {
@@ -126,12 +144,12 @@ impl StringCache {
// Write the static inner html, or insert a marker if dynamic inner html is possible
if let Some(inner_html) = inner_html {
chain.write_str(inner_html)?;
} else if has_dynamic_attrs {
} else if has_dyn_attrs {
chain.segments.push(Segment::InnerHtmlMarker);
}
for child in *children {
Self::recurse(child, cur_path, root_idx, chain)?;
Self::recurse(child, cur_path, root_idx, false, prerender, chain)?;
}
write!(chain, "</{tag}>")?;
}
+42
View File
@@ -0,0 +1,42 @@
use async_trait::async_trait;
use dioxus_core::ScopeState;
use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator};
use std::rc::Rc;
/// Provides the SSREvalProvider through [`cx.provide_context`].
pub fn init_eval(cx: &ScopeState) {
let provider: Rc<dyn EvalProvider> = Rc::new(SSREvalProvider {});
cx.provide_context(provider);
}
/// Reprents the ssr-target's provider of evaluators.
pub struct SSREvalProvider;
impl EvalProvider for SSREvalProvider {
fn new_evaluator(&self, _: String) -> Result<Rc<dyn Evaluator>, EvalError> {
Ok(Rc::new(SSREvaluator) as Rc<dyn Evaluator + 'static>)
}
}
/// Represents a ssr-target's JavaScript evaluator.
pub struct SSREvaluator;
// In ssr rendering we never run or resolve evals.
#[async_trait(?Send)]
impl Evaluator for SSREvaluator {
/// Runs the evaluated JavaScript.
async fn join(&self) -> Result<serde_json::Value, EvalError> {
std::future::pending::<()>().await;
unreachable!()
}
/// Sends a message to the evaluated JavaScript.
fn send(&self, _el: serde_json::Value) -> Result<(), EvalError> {
Ok(())
}
/// Gets an UnboundedReceiver to receive messages from the evaluated JavaScript.
async fn recv(&self) -> Result<serde_json::Value, EvalError> {
std::future::pending::<()>().await;
unreachable!()
}
}
+1
View File
@@ -81,6 +81,7 @@ impl IncrementalRenderer {
let mut html_buffer = WriteBuffer { buffer: Vec::new() };
{
let mut vdom = VirtualDom::new_with_props(comp, props);
crate::eval::init_eval(vdom.base_scope());
rebuild_with(&mut vdom).await;
renderer.render_before_body(&mut *html_buffer)?;
+15 -1
View File
@@ -67,6 +67,7 @@ pub struct IncrementalRendererConfig {
memory_cache_limit: usize,
invalidate_after: Option<Duration>,
map_path: Option<PathMapFn>,
clear_cache: bool,
}
impl Default for IncrementalRendererConfig {
@@ -83,9 +84,16 @@ impl IncrementalRendererConfig {
memory_cache_limit: 10000,
invalidate_after: None,
map_path: None,
clear_cache: true,
}
}
/// Clear the cache on startup (default: true)
pub fn clear_cache(mut self, clear_cache: bool) -> Self {
self.clear_cache = clear_cache;
self
}
/// Set a mapping from the route to the file path. This will override the default mapping configured with `static_dir`.
/// The function should return the path to the folder to store the index.html file in.
pub fn map_path<F: Fn(&str) -> PathBuf + Send + Sync + 'static>(mut self, map_path: F) -> Self {
@@ -114,7 +122,7 @@ impl IncrementalRendererConfig {
/// Build the incremental renderer.
pub fn build(self) -> IncrementalRenderer {
let static_dir = self.static_dir.clone();
IncrementalRenderer {
let mut renderer = IncrementalRenderer {
static_dir: self.static_dir.clone(),
memory_cache: NonZeroUsize::new(self.memory_cache_limit)
.map(|limit| lru::LruCache::with_hasher(limit, Default::default())),
@@ -129,6 +137,12 @@ impl IncrementalRendererConfig {
path
})
}),
};
if self.clear_cache {
renderer.invalidate_all();
}
renderer
}
}
+2
View File
@@ -10,6 +10,7 @@ pub mod incremental;
#[cfg(feature = "incremental")]
mod incremental_cfg;
pub mod eval;
pub mod renderer;
pub mod template;
@@ -42,6 +43,7 @@ pub fn render_lazy(f: LazyNodes<'_, '_>) -> String {
};
let mut dom = VirtualDom::new_with_props(lazy_app, props);
crate::eval::init_eval(dom.base_scope());
_ = dom.rebuild();
Renderer::new().render(&dom)
+135 -5
View File
@@ -28,6 +28,9 @@ pub struct Renderer {
/// A cache of templates that have been rendered
template_cache: HashMap<&'static str, Arc<StringCache>>,
/// The current dynamic node id for hydration
dynamic_node_id: usize,
}
impl Renderer {
@@ -54,6 +57,7 @@ impl Renderer {
// We should never ever run into async or errored nodes in SSR
// Error boundaries and suspense boundaries will convert these to sync
if let RenderReturn::Ready(node) = dom.get_scope(scope).unwrap().root_node() {
self.dynamic_node_id = 0;
self.render_template(buf, dom, node)?
};
@@ -69,7 +73,10 @@ impl Renderer {
let entry = self
.template_cache
.entry(template.template.get().name)
.or_insert_with(|| Arc::new(StringCache::from_template(template).unwrap()))
.or_insert_with({
let prerender = self.pre_render;
move || Arc::new(StringCache::from_template(template, prerender).unwrap())
})
.clone();
let mut inner_html = None;
@@ -77,6 +84,9 @@ impl Renderer {
// We need to keep track of the dynamic styles so we can insert them into the right place
let mut accumulated_dynamic_styles = Vec::new();
// We need to keep track of the listeners so we can insert them into the right place
let mut accumulated_listeners = Vec::new();
for segment in entry.segments.iter() {
match segment {
Segment::Attr(idx) => {
@@ -94,6 +104,14 @@ impl Renderer {
write_attribute(buf, attr)?;
}
if self.pre_render {
if let AttributeValue::Listener(_) = &attr.value {
// The onmounted event doesn't need a DOM listener
if attr.name != "onmounted" {
accumulated_listeners.push(attr.name);
}
}
}
Ok(())
})?;
}
@@ -118,7 +136,8 @@ impl Renderer {
DynamicNode::Text(text) => {
// in SSR, we are concerned that we can't hunt down the right text node since they might get merged
if self.pre_render {
write!(buf, "<!--#-->")?;
write!(buf, "<!--node-id{}-->", self.dynamic_node_id)?;
self.dynamic_node_id += 1;
}
write!(
@@ -137,9 +156,14 @@ impl Renderer {
}
}
DynamicNode::Placeholder(_el) => {
DynamicNode::Placeholder(_) => {
if self.pre_render {
write!(buf, "<pre></pre>")?;
write!(
buf,
"<pre data-node-hydration={}></pre>",
self.dynamic_node_id
)?;
self.dynamic_node_id += 1;
}
}
},
@@ -178,6 +202,22 @@ impl Renderer {
}
}
}
Segment::AttributeNodeMarker => {
// first write the id
write!(buf, "{}", self.dynamic_node_id)?;
self.dynamic_node_id += 1;
// then write any listeners
for name in accumulated_listeners.drain(..) {
write!(buf, ",{}:", &name[2..])?;
write!(buf, "{}", dioxus_html::event_bubbles(name) as u8)?;
}
}
Segment::RootNodeMarker => {
write!(buf, "{}", self.dynamic_node_id)?;
self.dynamic_node_id += 1
}
}
}
@@ -195,7 +235,9 @@ fn to_string_works() {
render! {
div { class: "asdasdasd", class: "asdasdasd", id: "id-{dynamic}",
"Hello world 1 -->" "{dynamic}" "<-- Hello world 2"
"Hello world 1 -->"
"{dynamic}"
"<-- Hello world 2"
div { "nest 1" }
div {}
div { "nest 2" }
@@ -241,6 +283,94 @@ fn to_string_works() {
assert_eq!(out, "<div class=\"asdasdasd asdasdasd\" id=\"id-123\">Hello world 1 --&gt;123&lt;-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>&lt;/diiiiiiiiv&gt;<div>finalize 0</div><div>finalize 1</div><div>finalize 2</div><div>finalize 3</div><div>finalize 4</div></div>");
}
#[test]
fn empty_for_loop_works() {
use dioxus::prelude::*;
fn app(cx: Scope) -> Element {
render! {
div { class: "asdasdasd",
for _ in (0..5) {
}
}
}
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
let mut renderer = Renderer::new();
let out = renderer.render(&dom);
for item in renderer.template_cache.iter() {
if item.1.segments.len() > 5 {
assert_eq!(
item.1.segments,
vec![
PreRendered("<div class=\"asdasdasd\"".into(),),
Attr(0,),
StyleMarker {
inside_style_tag: false,
},
PreRendered(">".into()),
InnerHtmlMarker,
PreRendered("</div>".into(),),
]
);
}
}
use Segment::*;
assert_eq!(out, "<div class=\"asdasdasd\"></div>");
}
#[test]
fn empty_render_works() {
use dioxus::prelude::*;
fn app(cx: Scope) -> Element {
render! {}
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
let mut renderer = Renderer::new();
let out = renderer.render(&dom);
for item in renderer.template_cache.iter() {
if item.1.segments.len() > 5 {
assert_eq!(item.1.segments, vec![]);
}
}
assert_eq!(out, "");
}
#[test]
fn empty_rsx_works() {
use dioxus::prelude::*;
fn app(_: Scope) -> Element {
rsx! {};
None
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
let mut renderer = Renderer::new();
let out = renderer.render(&dom);
for item in renderer.template_cache.iter() {
if item.1.segments.len() > 5 {
assert_eq!(item.1.segments, vec![]);
}
}
assert_eq!(out, "");
}
pub(crate) const BOOL_ATTRS: &[&str] = &[
"allowfullscreen",
"allowpaymentrequest",
+199
View File
@@ -0,0 +1,199 @@
use dioxus::prelude::*;
#[test]
fn root_ids() {
fn app(cx: Scope) -> Element {
render! { div { width: "100px" } }
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
assert_eq!(
dioxus_ssr::pre_render(&dom),
r#"<div style="width:100px;" data-node-hydration="0"></div>"#
);
}
#[test]
fn dynamic_attributes() {
fn app(cx: Scope) -> Element {
let dynamic = 123;
render! {
div { width: "100px", div { width: "{dynamic}px" } }
}
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
assert_eq!(
dioxus_ssr::pre_render(&dom),
r#"<div style="width:100px;" data-node-hydration="0"><div style="width:123px;" data-node-hydration="1"></div></div>"#
);
}
#[test]
fn listeners() {
fn app(cx: Scope) -> Element {
render! {
div { width: "100px", div { onclick: |_| {} } }
}
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
assert_eq!(
dioxus_ssr::pre_render(&dom),
r#"<div style="width:100px;" data-node-hydration="0"><div data-node-hydration="1,click:1"></div></div>"#
);
fn app2(cx: Scope) -> Element {
let dynamic = 123;
render! {
div { width: "100px", div { width: "{dynamic}px", onclick: |_| {} } }
}
}
let mut dom = VirtualDom::new(app2);
_ = dom.rebuild();
assert_eq!(
dioxus_ssr::pre_render(&dom),
r#"<div style="width:100px;" data-node-hydration="0"><div style="width:123px;" data-node-hydration="1,click:1"></div></div>"#
);
}
#[test]
fn text_nodes() {
fn app(cx: Scope) -> Element {
let dynamic_text = "hello";
render! {
div { dynamic_text }
}
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
assert_eq!(
dioxus_ssr::pre_render(&dom),
r#"<div data-node-hydration="0"><!--node-id1-->hello<!--#--></div>"#
);
fn app2(cx: Scope) -> Element {
let dynamic = 123;
render! {
div { "{dynamic}", "{1234}" }
}
}
let mut dom = VirtualDom::new(app2);
_ = dom.rebuild();
assert_eq!(
dioxus_ssr::pre_render(&dom),
r#"<div data-node-hydration="0"><!--node-id1-->123<!--#--><!--node-id2-->1234<!--#--></div>"#
);
}
#[allow(non_snake_case)]
#[test]
fn components_hydrate() {
fn app(cx: Scope) -> Element {
render! { Child {} }
}
fn Child(cx: Scope) -> Element {
render! { div { "hello" } }
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
assert_eq!(
dioxus_ssr::pre_render(&dom),
r#"<div data-node-hydration="0">hello</div>"#
);
fn app2(cx: Scope) -> Element {
render! { Child2 {} }
}
fn Child2(cx: Scope) -> Element {
let dyn_text = "hello";
render! {
div { dyn_text }
}
}
let mut dom = VirtualDom::new(app2);
_ = dom.rebuild();
assert_eq!(
dioxus_ssr::pre_render(&dom),
r#"<div data-node-hydration="0"><!--node-id1-->hello<!--#--></div>"#
);
fn app3(cx: Scope) -> Element {
render! { Child3 {} }
}
fn Child3(cx: Scope) -> Element {
render! { div { width: "{1}" } }
}
let mut dom = VirtualDom::new(app3);
_ = dom.rebuild();
assert_eq!(
dioxus_ssr::pre_render(&dom),
r#"<div style="width:1;" data-node-hydration="0"></div>"#
);
fn app4(cx: Scope) -> Element {
render! { Child4 {} }
}
fn Child4(cx: Scope) -> Element {
render! {
for _ in 0..2 {
render! {
render! {
"{1}"
}
}
}
}
}
let mut dom = VirtualDom::new(app4);
_ = dom.rebuild();
assert_eq!(
dioxus_ssr::pre_render(&dom),
r#"<!--node-id0-->1<!--#--><!--node-id1-->1<!--#-->"#
);
}
#[test]
fn hello_world_hydrates() {
fn app(cx: Scope) -> Element {
let mut count = use_state(cx, || 0);
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
})
}
let mut dom = VirtualDom::new(app);
_ = dbg!(dom.rebuild());
assert_eq!(
dioxus_ssr::pre_render(&dom),
r#"<h1 data-node-hydration="0"><!--node-id1-->High-Five counter: 0<!--#--></h1><button data-node-hydration="2,click:1">Up high!</button><button data-node-hydration="3,click:1">Down low!</button>"#
);
}
+1 -3
View File
@@ -47,9 +47,7 @@ fn dynamic() {
fn components() {
#[component]
fn MyComponent(cx: Scope, name: i32) -> Element {
render! {
div { "component {name}" }
}
render! { div { "component {name}" } }
}
assert_eq!(