Merge branch 'master' into many_optional_attributes

This commit is contained in:
Jonathan Kelley authored and GitHub committed 2024-01-04 10:18:57 -08:00
commit 8df7a7685e
569 files changed
+8815 -21321

No files matched your search

+3 -1
View File
@@ -6,11 +6,11 @@ edition = "2018"
description = "Dioxus render-to-string"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com/docs/0.3/guide/en/getting_started/ssr.html"
keywords = ["dom", "ui", "gui", "react", "ssr"]
[dependencies]
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-html = { workspace = true }
askama_escape = "0.10.3"
thiserror = "1.0.23"
rustc-hash = "1.1.0"
@@ -18,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 }
+4 -9
View File
@@ -6,10 +6,11 @@
</div>
## Resources
This crate is a part of the broader Dioxus ecosystem. For more resources about Dioxus, check out:
- [Getting Started](https://dioxuslabs.com/docs/0.3/guide/en/getting_started/index.html)
- [Book](https://dioxuslabs.com/docs/0.3/guide/en/)
- [Getting Started](https://dioxuslabs.com/learn/0.4/getting_started)
- [Book](https://dioxuslabs.com/learn/0.4/)
- [Examples](https://github.com/DioxusLabs/example-projects)
## Overview
@@ -26,7 +27,6 @@ let text = dioxus_ssr::render(&vdom);
assert_eq!(text, "<div>hello world!</div>")
```
## Basic Usage
The simplest example is to simply render some `rsx!` nodes to HTML. This can be done with the [`render_lazy`] API.
@@ -50,15 +50,11 @@ let _ = vdom.rebuild();
let content = dioxus_ssr::render(&vdom);
```
## Usage in pre-rendering
This crate is particularly useful in pre-generating pages server-side and then selectively loading Dioxus client-side to pick up the reactive elements.
This crate supports hydration out of the box. However, both the client and server must generate the *exact* same VirtualDOMs - the client picks up its VirtualDOM assuming that the pre-rendered page output is the same. To do this, you need to make sure that your VirtualDOM implementation is deterministic! This could involve either serializing our app state and sending it to the client, hydrating only parts of the page, or building tests to ensure what's rendered on the server is the same as the client.
This crate supports hydration out of the box. However, both the client and server must generate the _exact_ same VirtualDOMs - the client picks up its VirtualDOM assuming that the pre-rendered page output is the same. To do this, you need to make sure that your VirtualDOM implementation is deterministic! This could involve either serializing our app state and sending it to the client, hydrating only parts of the page, or building tests to ensure what's rendered on the server is the same as the client.
With pre-rendering enabled, this crate will generate element nodes with Element IDs pre-associated. During hydration, the Dioxus-WebSys renderer will attach the Virtual nodes to these real nodes after a page query.
@@ -107,5 +103,4 @@ let text = render_lazy!(rsx!( div { "hello world" } ));
Dioxus SSR is a powerful tool to generate static sites. Using Dioxus for static site generation _is_ a bit overkill, however. The new documentation generation library, Doxie, is essentially Dioxus SSR on steroids designed for static site generation with client-side hydration.
Again, simply render the VirtualDOM to a string using `render` or any of the other render methods.
+6
View File
@@ -1,6 +1,8 @@
use dioxus_core::prelude::*;
use std::fmt::Write;
use crate::renderer::{str_truthy, BOOL_ATTRS};
#[derive(Debug)]
pub struct StringCache {
pub segments: Vec<Segment>,
@@ -86,6 +88,10 @@ impl StringCache {
inner_html = Some(value);
} else if let Some("style") = namespace {
styles.push((name, value));
} else if BOOL_ATTRS.contains(name) {
if str_truthy(value) {
write!(chain, " {name}=\"{value}\"",)?;
}
} else {
write!(chain, " {name}=\"{value}\"")?;
}
+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
}
}
+4
View File
@@ -1,4 +1,6 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
mod cache;
pub mod config;
@@ -8,6 +10,7 @@ pub mod incremental;
#[cfg(feature = "incremental")]
mod incremental_cfg;
pub mod eval;
pub mod renderer;
pub mod template;
@@ -40,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)
+177 -22
View File
@@ -1,5 +1,7 @@
use super::cache::Segment;
use crate::cache::StringCache;
use dioxus_core::Attribute;
use dioxus_core::{prelude::*, AttributeValue, DynamicNode, RenderReturn};
use std::collections::HashMap;
use std::fmt::Write;
@@ -83,18 +85,13 @@ impl Renderer {
inner_html = Some(attr);
} else if attr.namespace == Some("style") {
accumulated_dynamic_styles.push(attr);
} else if BOOL_ATTRS.contains(&attr.name) {
if truthy(&attr.value) {
write!(buf, " {}=", attr.name)?;
write_value(buf, &attr.value)?;
}
} else {
match attr.value {
AttributeValue::Text(value) => {
write!(buf, " {}=\"{}\"", attr.name, value)?
}
AttributeValue::Bool(value) => write!(buf, " {}={}", attr.name, value)?,
AttributeValue::Int(value) => write!(buf, " {}={}", attr.name, value)?,
AttributeValue::Float(value) => {
write!(buf, " {}={}", attr.name, value)?
}
_ => {}
};
write_attribute(buf, attr)?;
}
}
Segment::Node(idx) => match &template.dynamic_nodes[*idx] {
@@ -153,17 +150,9 @@ impl Renderer {
write!(buf, " style=\"")?;
}
for attr in &accumulated_dynamic_styles {
match attr.value {
AttributeValue::Text(value) => {
write!(buf, "{}:{};", attr.name, value)?
}
AttributeValue::Bool(value) => {
write!(buf, "{}:{};", attr.name, value)?
}
AttributeValue::Float(f) => write!(buf, "{}:{};", attr.name, f)?,
AttributeValue::Int(i) => write!(buf, "{}:{};", attr.name, i)?,
_ => {}
};
write!(buf, "{}:", attr.name)?;
write_value_unquoted(buf, &attr.value)?;
write!(buf, ";")?;
}
if !*inside_style_tag {
write!(buf, "\"")?;
@@ -248,3 +237,169 @@ 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",
"async",
"autofocus",
"autoplay",
"checked",
"controls",
"default",
"defer",
"disabled",
"formnovalidate",
"hidden",
"ismap",
"itemscope",
"loop",
"multiple",
"muted",
"nomodule",
"novalidate",
"open",
"playsinline",
"readonly",
"required",
"reversed",
"selected",
"truespeed",
"webkitdirectory",
];
pub(crate) fn str_truthy(value: &str) -> bool {
!value.is_empty() && value != "0" && value.to_lowercase() != "false"
}
pub(crate) fn truthy(value: &AttributeValue) -> bool {
match value {
AttributeValue::Text(value) => str_truthy(value),
AttributeValue::Bool(value) => *value,
AttributeValue::Int(value) => *value != 0,
AttributeValue::Float(value) => *value != 0.0,
_ => false,
}
}
pub(crate) fn write_attribute(buf: &mut impl Write, attr: &Attribute) -> std::fmt::Result {
let name = &attr.name;
match attr.value {
AttributeValue::Text(value) => write!(buf, " {name}=\"{value}\""),
AttributeValue::Bool(value) => write!(buf, " {name}={value}"),
AttributeValue::Int(value) => write!(buf, " {name}={value}"),
AttributeValue::Float(value) => write!(buf, " {name}={value}"),
_ => Ok(()),
}
}
pub(crate) fn write_value(buf: &mut impl Write, value: &AttributeValue) -> std::fmt::Result {
match value {
AttributeValue::Text(value) => write!(buf, "\"{}\"", value),
AttributeValue::Bool(value) => write!(buf, "{}", value),
AttributeValue::Int(value) => write!(buf, "{}", value),
AttributeValue::Float(value) => write!(buf, "{}", value),
_ => Ok(()),
}
}
pub(crate) fn write_value_unquoted(
buf: &mut impl Write,
value: &AttributeValue,
) -> std::fmt::Result {
match value {
AttributeValue::Text(value) => write!(buf, "{}", value),
AttributeValue::Bool(value) => write!(buf, "{}", value),
AttributeValue::Int(value) => write!(buf, "{}", value),
AttributeValue::Float(value) => write!(buf, "{}", value),
_ => Ok(()),
}
}
+37
View File
@@ -0,0 +1,37 @@
use dioxus::prelude::*;
#[test]
fn static_boolean_attributs() {
fn app(cx: Scope) -> Element {
render! {
div { hidden: "false" }
div { hidden: "true" }
}
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
assert_eq!(
dioxus_ssr::render(&dom),
r#"<div></div><div hidden="true"></div>"#
);
}
#[test]
fn dynamic_boolean_attributs() {
fn app(cx: Scope) -> Element {
render! {
div { hidden: false }
div { hidden: true }
}
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
assert_eq!(
dioxus_ssr::render(&dom),
r#"<div></div><div hidden=true></div>"#
);
}
+6 -5
View File
@@ -2,11 +2,12 @@ use dioxus::prelude::*;
#[test]
fn simple() {
fn app(cx: Scope) -> Element {
#[component]
fn App(cx: Scope) -> Element {
render! { div { "hello!" } }
}
let mut dom = VirtualDom::new(app);
let mut dom = VirtualDom::new(App);
_ = dom.rebuild();
assert_eq!(dioxus_ssr::render(&dom), "<div>hello!</div>");
@@ -44,8 +45,8 @@ fn dynamic() {
#[test]
fn components() {
#[inline_props]
fn my_component(cx: Scope, name: i32) -> Element {
#[component]
fn MyComponent(cx: Scope, name: i32) -> Element {
render! {
div { "component {name}" }
}
@@ -55,7 +56,7 @@ fn components() {
dioxus_ssr::render_lazy(rsx! {
div {
(0..5).map(|name| rsx! {
my_component { name: name }
MyComponent { name: name }
})
}
}),