Add a ton of comments to rsx/hotreload, add snapshot testing, refactor a bit to simplify the crate (#2130)

Merge dynamic context and dynamic mapping, clean up the rsx hotreload logic, and add location data to the Rsx objects
This commit is contained in:
Jonathan Kelley 2024-03-24 13:31:26 -07:00 committed by GitHub
parent b19a546c0a
commit eb79e61642
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1413 additions and 811 deletions

29
Cargo.lock generated
View file

@ -2469,6 +2469,7 @@ name = "dioxus-rsx"
version = "0.5.0-alpha.2" version = "0.5.0-alpha.2"
dependencies = [ dependencies = [
"dioxus-core 0.5.0-alpha.2", "dioxus-core 0.5.0-alpha.2",
"insta",
"internment", "internment",
"krates", "krates",
"proc-macro2", "proc-macro2",
@ -4728,6 +4729,19 @@ dependencies = [
"generic-array 0.14.7", "generic-array 0.14.7",
] ]
[[package]]
name = "insta"
version = "1.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a7c22c4d34ef4788c351e971c52bfdfe7ea2766f8c5466bc175dd46e52ac22e"
dependencies = [
"console",
"lazy_static",
"linked-hash-map",
"similar",
"yaml-rust",
]
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.12" version = "0.1.12"
@ -7931,6 +7945,12 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
[[package]]
name = "similar"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21"
[[package]] [[package]]
name = "simple_logger" name = "simple_logger"
version = "4.3.3" version = "4.3.3"
@ -10250,6 +10270,15 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03" checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03"
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]] [[package]]
name = "yansi" name = "yansi"
version = "0.5.1" version = "0.5.1"

View file

@ -114,6 +114,19 @@ axum-extra = "0.9.2"
reqwest = "0.11.24" reqwest = "0.11.24"
owo-colors = "4.0.0" owo-colors = "4.0.0"
[workspace.dev-dependencies]
isnta = "1.36.1"
# speed up some macros by optimizing them
[profile.dev.package.insta]
opt-level = 3
[profile.dev.package.similar]
opt-level = 3
[profile.dev.package.dioxus-core-macro]
opt-level = 3
# Enable a small amount of optimization in debug mode # Enable a small amount of optimization in debug mode
[profile.cli-dev] [profile.cli-dev]
inherits = "dev" inherits = "dev"

View file

@ -262,6 +262,7 @@ impl<'a> Writer<'a> {
then_branch, then_branch,
else_if_branch, else_if_branch,
else_branch, else_branch,
..
} = chain; } = chain;
write!( write!(

View file

@ -1 +0,0 @@
imports_granularity = "Crate"

View file

@ -143,6 +143,7 @@ pub fn collect_svgs(children: &mut [BodyNode], out: &mut Vec<BodyNode>) {
manual_props: None, manual_props: None,
key: None, key: None,
brace: Default::default(), brace: Default::default(),
location: Default::default(),
}); });
std::mem::swap(child, &mut new_comp); std::mem::swap(child, &mut new_comp);

8
packages/rsx/.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
"rust-analyzer.check.workspace": false,
// "rust-analyzer.check.extraArgs": [
// "--features",
// "hot_reload"
// ],
"rust-analyzer.cargo.features": "all"
}

View file

@ -23,7 +23,11 @@ krates = { version = "0.16.6", optional = true }
tracing = { workspace = true } tracing = { workspace = true }
[features] [features]
default = ["html"] default = ["html", "hot_reload"]
hot_reload = ["krates", "internment", "dioxus-core"] hot_reload = ["krates", "internment", "dioxus-core"]
serde = ["dep:serde"] serde = ["dep:serde"]
html = [] html = []
[dev-dependencies]
insta = "1.36.1"

View file

@ -8,7 +8,10 @@ use syn::{parse_quote, spanned::Spanned, Expr, ExprIf, Ident, LitStr};
#[derive(PartialEq, Eq, Clone, Debug, Hash)] #[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub enum AttributeType { pub enum AttributeType {
/// An attribute that is known
Named(ElementAttrNamed), Named(ElementAttrNamed),
/// An attribute that's being spread in via the `..` syntax
Spread(Expr), Spread(Expr),
} }
@ -68,6 +71,24 @@ impl AttributeType {
} }
} }
} }
pub fn as_static_str_literal(&self) -> Option<(&ElementAttrName, &IfmtInput)> {
match self {
AttributeType::Named(ElementAttrNamed {
attr:
ElementAttr {
value: ElementAttrValue::AttrLiteral(value),
name,
},
..
}) if value.is_static() => Some((name, value)),
_ => None,
}
}
pub fn is_static_str_literal(&self) -> bool {
self.as_static_str_literal().is_some()
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]

View file

@ -11,6 +11,8 @@
//! - [ ] Keys //! - [ ] Keys
//! - [ ] Properties spreading with with `..` syntax //! - [ ] Properties spreading with with `..` syntax
use self::{location::CallerLocation, renderer::TemplateRenderer};
use super::*; use super::*;
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
@ -32,6 +34,7 @@ pub struct Component {
pub children: Vec<BodyNode>, pub children: Vec<BodyNode>,
pub manual_props: Option<Expr>, pub manual_props: Option<Expr>,
pub brace: syn::token::Brace, pub brace: syn::token::Brace,
pub location: CallerLocation,
} }
impl Parse for Component { impl Parse for Component {
@ -88,6 +91,7 @@ impl Parse for Component {
} }
Ok(Self { Ok(Self {
location: CallerLocation::default(),
name, name,
prop_gen_args, prop_gen_args,
fields, fields,
@ -179,20 +183,10 @@ impl Component {
toks.append_all(quote! {#field}) toks.append_all(quote! {#field})
} }
if !self.children.is_empty() { if !self.children.is_empty() {
let renderer: TemplateRenderer = TemplateRenderer { let renderer = TemplateRenderer::as_tokens(&self.children, None);
roots: &self.children, toks.append_all(quote! { .children( Some({ #renderer }) ) });
location: None,
};
toks.append_all(quote! {
.children(
Some({ #renderer })
)
});
} }
toks.append_all(quote! { toks.append_all(quote! { .build() });
.build()
});
toks toks
} }

416
packages/rsx/src/context.rs Normal file
View file

@ -0,0 +1,416 @@
use std::collections::HashMap;
use crate::*;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
/// As we create the dynamic nodes, we want to keep track of them in a linear fashion
/// We'll use the size of the vecs to determine the index of the dynamic node in the final output
#[derive(Default, Debug)]
pub struct DynamicContext<'a> {
pub dynamic_nodes: Vec<&'a BodyNode>,
pub dynamic_attributes: Vec<Vec<&'a AttributeType>>,
pub current_path: Vec<u8>,
pub node_paths: Vec<Vec<u8>>,
pub attr_paths: Vec<Vec<u8>>,
/// Mapping variables used to map the old template to the new template
///
/// This tracks whether or not we're tracking some nodes or attributes
/// If we're tracking, then we'll attempt to use the old mapping
is_tracking: bool,
/// The mapping of node to its index in the dynamic_nodes list
/// We use the fact that BodyNode is Hash/PartialEq to track the nodes when we run into them
node_to_idx: HashMap<BodyNode, Vec<usize>>,
last_element_idx: usize,
/// The mapping of attribute to its index in the dynamic_attributes list
/// We use the fact that AttributeType is Hash/PartialEq to track the attributes when we run into them
attribute_to_idx: HashMap<AttributeType, Vec<usize>>,
last_attribute_idx: usize,
}
impl<'a> DynamicContext<'a> {
pub fn new_with_old(template: Option<CallBody>) -> Self {
let mut new = Self::default();
if let Some(call) = template {
for node in call.roots {
new.track_node(node);
}
new.is_tracking = true;
}
new
}
/// Populate the dynamic context with our own roots
///
/// This will call update_node on each root, attempting to build us a list of TemplateNodes that
/// we can render out.
///
/// These TemplateNodes are the same one used in Dioxus core! We just serialize them out and then
/// they'll get picked up after codegen for compilation. Cool stuff.
///
/// If updating fails (IE the root is a dynamic node that has changed), then we return None.
pub fn populate_by_updating<Ctx: HotReloadingContext>(
&mut self,
roots: &'a [BodyNode],
) -> Option<Vec<TemplateNode>> {
// Create a list of new roots that we'll spit out
let mut roots_ = Vec::new();
// Populate the dynamic context with our own roots
for (idx, root) in roots.iter().enumerate() {
self.current_path.push(idx as u8);
roots_.push(self.update_node::<Ctx>(root)?);
self.current_path.pop();
}
Some(roots_)
}
/// Render a portion of an rsx callbody to a TemplateNode call
///
/// We're assembling the templatenodes
pub fn render_static_node(&mut self, root: &'a BodyNode) -> TokenStream2 {
match root {
BodyNode::Element(el) => self.render_static_element(el),
BodyNode::Text(text) if text.is_static() => {
let text = text.to_static().unwrap();
quote! { dioxus_core::TemplateNode::Text { text: #text } }
}
BodyNode::ForLoop(for_loop) => self.render_for_loop(root, for_loop),
BodyNode::RawExpr(_)
| BodyNode::Text(_)
| BodyNode::IfChain(_)
| BodyNode::Component(_) => self.render_dynamic_node(root),
}
}
/// Render a for loop to a token stream
///
/// This is basically just rendering a dynamic node, but with some extra bookkepping to track the
/// contents of the for loop in case we want to hot reload it
fn render_for_loop(&mut self, root: &'a BodyNode, _for_loop: &ForLoop) -> TokenStream2 {
self.render_dynamic_node(root)
}
fn render_dynamic_node(&mut self, root: &'a BodyNode) -> TokenStream2 {
let ct = self.dynamic_nodes.len();
self.dynamic_nodes.push(root);
self.node_paths.push(self.current_path.clone());
match root {
BodyNode::Text(_) => quote! { dioxus_core::TemplateNode::DynamicText { id: #ct } },
_ => quote! { dioxus_core::TemplateNode::Dynamic { id: #ct } },
}
}
fn render_static_element(&mut self, el: &'a Element) -> TokenStream2 {
let el_name = &el.name;
let ns = |name| match el_name {
ElementName::Ident(i) => quote! { dioxus_elements::#i::#name },
ElementName::Custom(_) => quote! { None },
};
let static_attrs = el
.merged_attributes
.iter()
.map(|attr| self.render_merged_attributes(attr, ns, el_name))
.collect::<Vec<_>>();
let children = el
.children
.iter()
.enumerate()
.map(|(idx, root)| self.render_children_nodes(idx, root))
.collect::<Vec<_>>();
let ns = ns(quote!(NAME_SPACE));
let el_name = el_name.tag_name();
quote! {
dioxus_core::TemplateNode::Element {
tag: #el_name,
namespace: #ns,
attrs: &[ #(#static_attrs)* ],
children: &[ #(#children),* ],
}
}
}
pub fn render_children_nodes(&mut self, idx: usize, root: &'a BodyNode) -> TokenStream2 {
self.current_path.push(idx as u8);
let out = self.render_static_node(root);
self.current_path.pop();
out
}
/// Render the attributes of an element
fn render_merged_attributes(
&mut self,
attr: &'a AttributeType,
ns: impl Fn(TokenStream2) -> TokenStream2,
el_name: &ElementName,
) -> TokenStream2 {
// Rendering static attributes requires a bit more work than just a dynamic attrs
match attr.as_static_str_literal() {
// If it's static, we'll take this little optimization
Some((name, value)) => Self::render_static_attr(value, name, ns, el_name),
// Otherwise, we'll just render it as a dynamic attribute
// This will also insert the attribute into the dynamic_attributes list to assemble the final template
_ => self.render_dynamic_attr(attr),
}
}
fn render_static_attr(
value: &IfmtInput,
name: &ElementAttrName,
ns: impl Fn(TokenStream2) -> TokenStream2,
el_name: &ElementName,
) -> TokenStream2 {
let value = value.to_static().unwrap();
let ns = match name {
ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)),
ElementAttrName::Custom(_) => quote!(None),
};
let name = match (el_name, name) {
(ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => quote! { #el_name::#name.0 },
_ => {
//hmmmm I think we could just totokens this, but the to_string might be inserting quotes
let as_string = name.to_string();
quote! { #as_string }
}
};
quote! {
dioxus_core::TemplateAttribute::Static {
name: #name,
namespace: #ns,
value: #value,
// todo: we don't diff these so we never apply the volatile flag
// volatile: dioxus_elements::#el_name::#name.2,
},
}
}
/// If the attr is dynamic, we save it to the tracked attributes list
/// This will let us use this context at a later point in time to update the template
fn render_dynamic_attr(&mut self, attr: &'a AttributeType) -> TokenStream2 {
let ct = self.dynamic_attributes.len();
self.dynamic_attributes.push(vec![attr]);
self.attr_paths.push(self.current_path.clone());
quote! { dioxus_core::TemplateAttribute::Dynamic { id: #ct }, }
}
#[cfg(feature = "hot_reload")]
pub fn update_node<Ctx: HotReloadingContext>(
&mut self,
root: &'a BodyNode,
) -> Option<TemplateNode> {
match root {
// The user is moving a static node around in the template
// Check this is a bit more complex but we can likely handle it
BodyNode::Element(el) => self.update_element::<Ctx>(el),
BodyNode::Text(text) if text.is_static() => {
let text = text.source.as_ref().unwrap();
let text = intern(text.value().as_str());
Some(TemplateNode::Text { text })
}
// The user is moving a dynamic node around in the template
// We *might* be able to handle it, but you never really know
BodyNode::RawExpr(_)
| BodyNode::Text(_)
| BodyNode::ForLoop(_)
| BodyNode::IfChain(_)
| BodyNode::Component(_) => self.update_dynamic_node(root),
}
}
/// Attempt to update a dynamic node in the template
///
/// If the change between the old and new template results in a mapping that doesn't exist, then we need to bail out.
/// Basically if we *had* a mapping of `[0, 1]` and the new template is `[1, 2]`, then we need to bail out, since
/// the new mapping doesn't exist in the original.
fn update_dynamic_node(&mut self, root: &'a BodyNode) -> Option<TemplateNode> {
let idx = match self.has_tracked_nodes() {
// Bail out if the mapping doesn't exist
// The user put it new code in the template, and that code is not hotreloadable
true => self.tracked_node_idx(root)?,
false => self.dynamic_nodes.len(),
};
// Put the node in the dynamic nodes list
self.dynamic_nodes.push(root);
// Fill in as many paths as we need - might have to fill in more since the old tempate shrunk some and let some paths be empty
if self.node_paths.len() <= idx {
self.node_paths.resize_with(idx + 1, Vec::new);
}
// And then set the path of this node to the current path (which we're hitting during traversal)
self.node_paths[idx] = self.current_path.clone();
Some(match root {
BodyNode::Text(_) => TemplateNode::DynamicText { id: idx },
_ => TemplateNode::Dynamic { id: idx },
})
}
fn update_element<Ctx: HotReloadingContext>(
&mut self,
el: &'a Element,
) -> Option<TemplateNode> {
let rust_name = el.name.to_string();
let mut static_attr_array = Vec::new();
for attr in &el.merged_attributes {
let template_attr = match attr.as_static_str_literal() {
// For static attributes, we don't need to pull in any mapping or anything
// We can just build them directly
Some((name, value)) => Self::make_static_attribute::<Ctx>(value, name, &rust_name),
// For dynamic attributes, we need to check the mapping to see if that mapping exists
// todo: one day we could generate new dynamic attributes on the fly if they're a literal,
// or something sufficiently serializable
// (ie `checked`` being a bool and bools being interpretable)
//
// For now, just give up if that attribute doesn't exist in the mapping
None => {
let id = self.update_dynamic_attribute(attr)?;
TemplateAttribute::Dynamic { id }
}
};
static_attr_array.push(template_attr);
}
let children = self.populate_by_updating::<Ctx>(el.children.as_slice())?;
let (tag, namespace) =
Ctx::map_element(&rust_name).unwrap_or((intern(rust_name.as_str()), None));
Some(TemplateNode::Element {
tag,
namespace,
attrs: intern(static_attr_array.into_boxed_slice()),
children: intern(children.as_slice()),
})
}
fn update_dynamic_attribute(&mut self, attr: &'a AttributeType) -> Option<usize> {
let idx = match self.has_tracked_nodes() {
true => self.tracked_attribute_idx(attr)?,
false => self.dynamic_attributes.len(),
};
self.dynamic_attributes.push(vec![attr]);
if self.attr_paths.len() <= idx {
self.attr_paths.resize_with(idx + 1, Vec::new);
}
self.attr_paths[idx] = self.current_path.clone();
Some(idx)
}
fn make_static_attribute<Ctx: HotReloadingContext>(
value: &IfmtInput,
name: &ElementAttrName,
element_name_rust: &str,
) -> TemplateAttribute {
let value = value.source.as_ref().unwrap();
let attribute_name_rust = name.to_string();
let (name, namespace) = Ctx::map_attribute(element_name_rust, &attribute_name_rust)
.unwrap_or((intern(attribute_name_rust.as_str()), None));
let static_attr = TemplateAttribute::Static {
name,
namespace,
value: intern(value.value().as_str()),
};
static_attr
}
/// Check if we're tracking any nodes
///
/// If we're tracking, then we'll attempt to use the old mapping
fn has_tracked_nodes(&self) -> bool {
self.is_tracking
}
/// Track a BodyNode
///
/// This will save the any dynamic nodes that we find.
/// We need to be careful around saving if/for/components since we want to hotreload their contents
/// provided that their rust portions haven't changed.
pub(crate) fn track_node(&mut self, node: BodyNode) {
match node {
// If the node is a static element, we just want to merge its attributes into the dynamic mapping
BodyNode::Element(el) => self.track_element(el),
// We skip static nodes since they'll get written out by the template during the diffing phase
BodyNode::Text(text) if text.is_static() => {}
BodyNode::RawExpr(_)
| BodyNode::Text(_)
| BodyNode::ForLoop(_)
| BodyNode::IfChain(_)
| BodyNode::Component(_) => {
self.track_dynamic_node(node);
}
}
}
fn track_element(&mut self, el: Element) {
for attr in el.merged_attributes {
// If the attribute is a static string literal, we don't need to insert it since the attribute
// will be written out during the diffing phase (since it's static)
if !attr.is_static_str_literal() {
self.track_attribute(attr);
}
}
for child in el.children {
self.track_node(child);
}
}
pub(crate) fn track_attribute(&mut self, attr: AttributeType) -> usize {
let idx = self.last_attribute_idx;
self.last_attribute_idx += 1;
self.attribute_to_idx.entry(attr).or_default().push(idx);
idx
}
pub(crate) fn track_dynamic_node(&mut self, node: BodyNode) -> usize {
let idx = self.last_element_idx;
self.last_element_idx += 1;
self.node_to_idx.entry(node).or_default().push(idx);
idx
}
pub(crate) fn tracked_attribute_idx(&mut self, attr: &AttributeType) -> Option<usize> {
self.attribute_to_idx
.get_mut(attr)
.and_then(|idxs| idxs.pop())
}
pub(crate) fn tracked_node_idx(&mut self, node: &BodyNode) -> Option<usize> {
self.node_to_idx.get_mut(node).and_then(|idxs| idxs.pop())
}
}

View file

@ -1,3 +1,7 @@
use super::{
hot_reload_diff::{diff_rsx, DiffResult},
ChangedRsx,
};
use crate::{CallBody, HotReloadingContext}; use crate::{CallBody, HotReloadingContext};
use dioxus_core::{ use dioxus_core::{
prelude::{TemplateAttribute, TemplateNode}, prelude::{TemplateAttribute, TemplateNode},
@ -9,17 +13,11 @@ pub use proc_macro2::TokenStream;
pub use std::collections::HashMap; pub use std::collections::HashMap;
pub use std::sync::Mutex; pub use std::sync::Mutex;
pub use std::time::SystemTime; pub use std::time::SystemTime;
use std::{collections::HashSet, ffi::OsStr, path::PathBuf}; use std::{collections::HashSet, ffi::OsStr, marker::PhantomData, path::PathBuf};
pub use std::{fs, io, path::Path}; pub use std::{fs, io, path::Path};
pub use std::{fs::File, io::Read}; pub use std::{fs::File, io::Read};
pub use syn::__private::ToTokens;
use syn::spanned::Spanned; use syn::spanned::Spanned;
use super::{
hot_reload_diff::{diff_rsx, DiffResult},
ChangedRsx,
};
pub enum UpdateResult { pub enum UpdateResult {
UpdatedRsx(Vec<Template>), UpdatedRsx(Vec<Template>),
@ -40,7 +38,7 @@ pub struct FileMap<Ctx: HotReloadingContext> {
in_workspace: HashMap<PathBuf, Option<PathBuf>>, in_workspace: HashMap<PathBuf, Option<PathBuf>>,
phantom: std::marker::PhantomData<Ctx>, phantom: PhantomData<Ctx>,
} }
/// A cached file that has been parsed /// A cached file that has been parsed
@ -75,7 +73,7 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
let mut map = Self { let mut map = Self {
map, map,
in_workspace: HashMap::new(), in_workspace: HashMap::new(),
phantom: std::marker::PhantomData, phantom: PhantomData,
}; };
map.load_assets(crate_dir.as_path()); map.load_assets(crate_dir.as_path());
@ -303,7 +301,7 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
} }
} }
fn template_location(old_start: proc_macro2::LineColumn, file: &Path) -> String { pub fn template_location(old_start: proc_macro2::LineColumn, file: &Path) -> String {
let line = old_start.line; let line = old_start.line;
let column = old_start.column + 1; let column = old_start.column + 1;
let location = file.display().to_string() let location = file.display().to_string()

View file

@ -19,143 +19,111 @@ mod errors;
mod attribute; mod attribute;
mod component; mod component;
mod element; mod element;
#[cfg(feature = "hot_reload")]
pub mod hot_reload;
mod ifmt; mod ifmt;
mod location;
mod node; mod node;
mod util;
use std::{fmt::Debug, hash::Hash}; pub(crate) mod context;
pub(crate) mod renderer;
mod sub_templates;
// Re-export the namespaces into each other // Re-export the namespaces into each other
pub use attribute::*; pub use attribute::*;
pub use component::*; pub use component::*;
#[cfg(feature = "hot_reload")] pub use context::DynamicContext;
use dioxus_core::{Template, TemplateAttribute, TemplateNode};
pub use element::*; pub use element::*;
#[cfg(feature = "hot_reload")]
pub use hot_reload::HotReloadingContext;
pub use ifmt::*; pub use ifmt::*;
#[cfg(feature = "hot_reload")]
use internment::Intern;
pub use node::*; pub use node::*;
// imports #[cfg(feature = "hot_reload")]
pub mod hot_reload;
#[cfg(feature = "hot_reload")]
use dioxus_core::{Template, TemplateAttribute, TemplateNode};
#[cfg(feature = "hot_reload")]
pub use hot_reload::HotReloadingContext;
#[cfg(feature = "hot_reload")]
use internment::Intern;
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt}; use quote::{quote, ToTokens, TokenStreamExt};
use renderer::TemplateRenderer;
use std::{fmt::Debug, hash::Hash};
use syn::{ use syn::{
parse::{Parse, ParseStream}, parse::{Parse, ParseStream},
Result, Token, Result, Token,
}; };
#[cfg(feature = "hot_reload")] /// The Callbody is the contents of the rsx! macro
// interns a object into a static object, resusing the value if it already exists ///
fn intern<T: Eq + Hash + Send + Sync + ?Sized + 'static>(s: impl Into<Intern<T>>) -> &'static T { /// It is a list of BodyNodes, which are the different parts of the template.
s.into().as_ref() /// The Callbody contains no information about how the template will be rendered, only information about the parsed tokens.
} ///
/// Every callbody should be valid, so you can use it to build a template.
/// Fundametnally, every CallBody is a template /// To generate the code used to render the template, use the ToTokens impl on the Callbody, or with the `render_with_location` method.
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct CallBody { pub struct CallBody {
pub roots: Vec<BodyNode>, pub roots: Vec<BodyNode>,
} }
impl CallBody { impl CallBody {
#[cfg(feature = "hot_reload")]
/// This will try to create a new template from the current body and the previous body. This will return None if the rsx has some dynamic part that has changed.
/// This function intentionally leaks memory to create a static template.
/// Keeping the template static allows us to simplify the core of dioxus and leaking memory in dev mode is less of an issue.
/// the previous_location is the location of the previous template at the time the template was originally compiled.
pub fn update_template<Ctx: HotReloadingContext>(
&self,
template: Option<CallBody>,
location: &'static str,
) -> Option<Template> {
let mut renderer: TemplateRenderer = TemplateRenderer {
roots: &self.roots,
location: None,
};
renderer.update_template::<Ctx>(template, location)
}
/// Render the template with a manually set file location. This should be used when multiple rsx! calls are used in the same macro /// Render the template with a manually set file location. This should be used when multiple rsx! calls are used in the same macro
pub fn render_with_location(&self, location: String) -> TokenStream2 { pub fn render_with_location(&self, location: String) -> TokenStream2 {
let body = TemplateRenderer {
roots: &self.roots,
location: Some(location),
};
// Empty templates just are placeholders for "none" // Empty templates just are placeholders for "none"
if self.roots.is_empty() { if self.roots.is_empty() {
return quote! { None }; return quote! { None };
} }
quote! { let body = TemplateRenderer::as_tokens(&self.roots, Some(location));
Some({ #body })
}
}
}
impl Parse for CallBody { quote! { Some({ #body }) }
fn parse(input: ParseStream) -> Result<Self> {
let mut roots = Vec::new();
while !input.is_empty() {
let node = input.parse::<BodyNode>()?;
if input.peek(Token![,]) {
let _ = input.parse::<Token![,]>();
} }
roots.push(node); /// This will try to create a new template from the current body and the previous body. This will return None if the
} /// rsx has some dynamic part that has changed.
///
Ok(Self { roots }) /// The previous_location is the location of the previous template at the time the template was originally compiled.
} /// It's up to you the implementor to trace the template location back to the original source code. Generally you
} /// can simply just match the location from the syn::File type to the template map living in the renderer.
///
impl ToTokens for CallBody { /// When you implement hotreloading, you're likely just going to parse the source code into the Syn::File type, which
fn to_tokens(&self, out_tokens: &mut TokenStream2) { /// should make retrieving the template location easy.
let body: TemplateRenderer = TemplateRenderer { ///
roots: &self.roots, /// ## Note:
location: None, ///
}; /// - This function intentionally leaks memory to create a static template.
/// - Keeping the template static allows us to simplify the core of dioxus and leaking memory in dev mode is less of an issue.
// Empty templates just are placeholders for "none" ///
if self.roots.is_empty() { /// ## Longer note about sub templates:
return out_tokens.append_all(quote! { None }); ///
} /// Sub templates when expanded in rustc use the same file/lin/col information as the parent template. This can
/// be annoying when you're trying to get a location for a sub template and it's pretending that it's its parent.
out_tokens.append_all(quote! { /// The new implementation of this aggregates all subtemplates into the TemplateRenderer and then assigns them
Some({ #body }) /// unique IDs based on the byte index of the template, working around this issue.
}) ///
} /// ## TODO:
} ///
/// A longer term goal would be to provide some sort of diagnostics to the user as to why the template was not
pub struct TemplateRenderer<'a> { /// updated, giving them an option to revert to the previous template as to not require a full rebuild.
pub roots: &'a [BodyNode],
pub location: Option<String>,
}
impl<'a> TemplateRenderer<'a> {
#[cfg(feature = "hot_reload")] #[cfg(feature = "hot_reload")]
fn update_template<Ctx: HotReloadingContext>( pub fn update_template<Ctx: HotReloadingContext>(
&mut self, &self,
previous_call: Option<CallBody>, old: Option<CallBody>,
location: &'static str, location: &'static str,
) -> Option<Template> { ) -> Option<Template> {
let mut mapping = previous_call.map(|call| DynamicMapping::from(call.roots)); // Create a context that will be used to update the template
let mut context = DynamicContext::new_with_old(old);
let mut context = DynamicContext::default(); // Force the template node to generate us TemplateNodes, and fill in the location information
let roots = context.populate_by_updating::<Ctx>(&self.roots)?;
let mut roots = Vec::new();
for (idx, root) in self.roots.iter().enumerate() {
context.current_path.push(idx as u8);
roots.push(context.update_node::<Ctx>(root, &mut mapping)?);
context.current_path.pop();
}
// We've received the dioxus-core TemplateNodess, and need to assemble them into a Template
// We could just use them directly, but we want to intern them to do our best to avoid
// egregious memory leaks. We're sitll leaking memory, but at least we can blame it on
// the `Intern` crate and not just the fact that we call Box::leak.
//
// We should also note that order of these nodes could be all scrambeled
Some(Template { Some(Template {
name: location, name: location,
roots: intern(roots.as_slice()), roots: intern(roots.as_slice()),
@ -179,586 +147,41 @@ impl<'a> TemplateRenderer<'a> {
} }
} }
impl<'a> ToTokens for TemplateRenderer<'a> { impl Parse for CallBody {
fn parse(input: ParseStream) -> Result<Self> {
let mut roots = Vec::new();
while !input.is_empty() {
let node = input.parse::<BodyNode>()?;
if input.peek(Token![,]) {
let _ = input.parse::<Token![,]>();
}
roots.push(node);
}
Ok(CallBody { roots })
}
}
impl ToTokens for CallBody {
fn to_tokens(&self, out_tokens: &mut TokenStream2) { fn to_tokens(&self, out_tokens: &mut TokenStream2) {
let mut context = DynamicContext::default(); // Empty templates just are placeholders for "none"
match self.roots.is_empty() {
let key = match self.roots.first() { true => out_tokens.append_all(quote! { None }),
Some(BodyNode::Element(el)) if self.roots.len() == 1 => el.key.clone(), false => {
Some(BodyNode::Component(comp)) if self.roots.len() == 1 => comp.key().cloned(), let body = TemplateRenderer::as_tokens(&self.roots, None);
_ => None, out_tokens.append_all(quote! { Some({ #body }) })
};
let key_tokens = match key {
Some(tok) => quote! { Some( #tok.to_string() ) },
None => quote! { None },
};
let root_col = match self.roots.first() {
Some(first_root) => {
let first_root_span = format!("{:?}", first_root.span());
first_root_span
.rsplit_once("..")
.and_then(|(_, after)| after.split_once(')').map(|(before, _)| before))
.unwrap_or_default()
.to_string()
}
_ => "0".to_string(),
};
let root_printer = self.roots.iter().enumerate().map(|(idx, root)| {
context.current_path.push(idx as u8);
let out = context.render_static_node(root);
context.current_path.pop();
out
});
let name = match self.location {
Some(ref loc) => quote! { #loc },
None => quote! {
concat!(
file!(),
":",
line!(),
":",
column!(),
":",
#root_col
)
},
};
// Render and release the mutable borrow on context
let roots = quote! { #( #root_printer ),* };
let node_printer = &context.dynamic_nodes;
let dyn_attr_printer = context
.dynamic_attributes
.iter()
.map(|attrs| AttributeType::merge_quote(attrs));
let node_paths = context.node_paths.iter().map(|it| quote!(&[#(#it),*]));
let attr_paths = context.attr_paths.iter().map(|it| quote!(&[#(#it),*]));
out_tokens.append_all(quote! {
static TEMPLATE: dioxus_core::Template = dioxus_core::Template {
name: #name,
roots: &[ #roots ],
node_paths: &[ #(#node_paths),* ],
attr_paths: &[ #(#attr_paths),* ],
};
{
// NOTE: Allocating a temporary is important to make reads within rsx drop before the value is returned
let __vnodes = dioxus_core::VNode::new(
#key_tokens,
TEMPLATE,
Box::new([ #( #node_printer),* ]),
Box::new([ #(#dyn_attr_printer),* ]),
);
__vnodes
}
});
}
}
#[cfg(feature = "hot_reload")]
#[derive(Default, Debug)]
struct DynamicMapping {
attribute_to_idx: std::collections::HashMap<AttributeType, Vec<usize>>,
last_attribute_idx: usize,
node_to_idx: std::collections::HashMap<BodyNode, Vec<usize>>,
last_element_idx: usize,
}
#[cfg(feature = "hot_reload")]
impl DynamicMapping {
fn from(nodes: Vec<BodyNode>) -> Self {
let mut new = Self::default();
for node in nodes {
new.add_node(node);
}
new
}
fn get_attribute_idx(&mut self, attr: &AttributeType) -> Option<usize> {
self.attribute_to_idx
.get_mut(attr)
.and_then(|idxs| idxs.pop())
}
fn get_node_idx(&mut self, node: &BodyNode) -> Option<usize> {
self.node_to_idx.get_mut(node).and_then(|idxs| idxs.pop())
}
fn insert_attribute(&mut self, attr: AttributeType) -> usize {
let idx = self.last_attribute_idx;
self.last_attribute_idx += 1;
self.attribute_to_idx.entry(attr).or_default().push(idx);
idx
}
fn insert_node(&mut self, node: BodyNode) -> usize {
let idx = self.last_element_idx;
self.last_element_idx += 1;
self.node_to_idx.entry(node).or_default().push(idx);
idx
}
fn add_node(&mut self, node: BodyNode) {
match node {
BodyNode::Element(el) => {
for attr in el.merged_attributes {
match &attr {
AttributeType::Named(ElementAttrNamed {
attr:
ElementAttr {
value: ElementAttrValue::AttrLiteral(input),
..
},
..
}) if input.is_static() => {}
_ => {
self.insert_attribute(attr);
}
}
}
for child in el.children {
self.add_node(child);
}
}
BodyNode::Text(text) if text.is_static() => {}
BodyNode::RawExpr(_)
| BodyNode::Text(_)
| BodyNode::ForLoop(_)
| BodyNode::IfChain(_)
| BodyNode::Component(_) => {
self.insert_node(node);
}
}
}
}
// As we create the dynamic nodes, we want to keep track of them in a linear fashion
// We'll use the size of the vecs to determine the index of the dynamic node in the final output
#[derive(Default, Debug)]
pub struct DynamicContext<'a> {
dynamic_nodes: Vec<&'a BodyNode>,
dynamic_attributes: Vec<Vec<&'a AttributeType>>,
current_path: Vec<u8>,
node_paths: Vec<Vec<u8>>,
attr_paths: Vec<Vec<u8>>,
}
impl<'a> DynamicContext<'a> {
#[cfg(feature = "hot_reload")]
fn update_node<Ctx: HotReloadingContext>(
&mut self,
root: &'a BodyNode,
mapping: &mut Option<DynamicMapping>,
) -> Option<TemplateNode> {
match root {
BodyNode::Element(el) => {
let element_name_rust = el.name.to_string();
let mut static_attrs = Vec::new();
for attr in &el.merged_attributes {
match &attr {
AttributeType::Named(ElementAttrNamed {
attr:
ElementAttr {
value: ElementAttrValue::AttrLiteral(value),
name,
},
..
}) if value.is_static() => {
let value = value.source.as_ref().unwrap();
let attribute_name_rust = name.to_string();
let (name, namespace) =
Ctx::map_attribute(&element_name_rust, &attribute_name_rust)
.unwrap_or((intern(attribute_name_rust.as_str()), None));
static_attrs.push(TemplateAttribute::Static {
name,
namespace,
value: intern(value.value().as_str()),
})
}
_ => {
let idx = match mapping {
Some(mapping) => mapping.get_attribute_idx(attr)?,
None => self.dynamic_attributes.len(),
};
self.dynamic_attributes.push(vec![attr]);
if self.attr_paths.len() <= idx {
self.attr_paths.resize_with(idx + 1, Vec::new);
}
self.attr_paths[idx] = self.current_path.clone();
static_attrs.push(TemplateAttribute::Dynamic { id: idx })
}
}
}
let mut children = Vec::new();
for (idx, root) in el.children.iter().enumerate() {
self.current_path.push(idx as u8);
children.push(self.update_node::<Ctx>(root, mapping)?);
self.current_path.pop();
}
let (tag, namespace) = Ctx::map_element(&element_name_rust)
.unwrap_or((intern(element_name_rust.as_str()), None));
Some(TemplateNode::Element {
tag,
namespace,
attrs: intern(static_attrs.into_boxed_slice()),
children: intern(children.as_slice()),
})
}
BodyNode::Text(text) if text.is_static() => {
let text = text.source.as_ref().unwrap();
Some(TemplateNode::Text {
text: intern(text.value().as_str()),
})
}
BodyNode::RawExpr(_)
| BodyNode::Text(_)
| BodyNode::ForLoop(_)
| BodyNode::IfChain(_)
| BodyNode::Component(_) => {
let idx = match mapping {
Some(mapping) => mapping.get_node_idx(root)?,
None => self.dynamic_nodes.len(),
};
self.dynamic_nodes.push(root);
if self.node_paths.len() <= idx {
self.node_paths.resize_with(idx + 1, Vec::new);
}
self.node_paths[idx] = self.current_path.clone();
Some(match root {
BodyNode::Text(_) => TemplateNode::DynamicText { id: idx },
_ => TemplateNode::Dynamic { id: idx },
})
}
}
}
/// Render a portion of an rsx callbody to a token stream
pub fn render_static_node(&mut self, root: &'a BodyNode) -> TokenStream2 {
match root {
BodyNode::Element(el) => {
let el_name = &el.name;
let ns = |name| match el_name {
ElementName::Ident(i) => quote! { dioxus_elements::#i::#name },
ElementName::Custom(_) => quote! { None },
};
let static_attrs = el.merged_attributes.iter().map(|attr| match attr {
AttributeType::Named(ElementAttrNamed {
attr:
ElementAttr {
value: ElementAttrValue::AttrLiteral(value),
name,
},
..
}) if value.is_static() => {
let value = value.to_static().unwrap();
let ns = {
match name {
ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)),
ElementAttrName::Custom(_) => quote!(None),
}
};
let name = match (el_name, name) {
(ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => {
quote! { #el_name::#name.0 }
}
_ => {
let as_string = name.to_string();
quote! { #as_string }
}
};
quote! {
dioxus_core::TemplateAttribute::Static {
name: #name,
namespace: #ns,
value: #value,
// todo: we don't diff these so we never apply the volatile flag
// volatile: dioxus_elements::#el_name::#name.2,
},
}
}
_ => {
let ct = self.dynamic_attributes.len();
self.dynamic_attributes.push(vec![attr]);
self.attr_paths.push(self.current_path.clone());
quote! { dioxus_core::TemplateAttribute::Dynamic { id: #ct }, }
}
});
let attrs = quote! { #(#static_attrs)* };
let children = el.children.iter().enumerate().map(|(idx, root)| {
self.current_path.push(idx as u8);
let out = self.render_static_node(root);
self.current_path.pop();
out
});
let children = quote! { #(#children),* };
let ns = ns(quote!(NAME_SPACE));
let el_name = el_name.tag_name();
quote! {
dioxus_core::TemplateNode::Element {
tag: #el_name,
namespace: #ns,
attrs: &[ #attrs ],
children: &[ #children ],
}
}
}
BodyNode::Text(text) if text.is_static() => {
let text = text.to_static().unwrap();
quote! { dioxus_core::TemplateNode::Text{ text: #text } }
}
BodyNode::RawExpr(_)
| BodyNode::Text(_)
| BodyNode::ForLoop(_)
| BodyNode::IfChain(_)
| BodyNode::Component(_) => {
let ct = self.dynamic_nodes.len();
self.dynamic_nodes.push(root);
self.node_paths.push(self.current_path.clone());
match root {
BodyNode::Text(_) => {
quote! { dioxus_core::TemplateNode::DynamicText { id: #ct } }
}
_ => quote! { dioxus_core::TemplateNode::Dynamic { id: #ct } },
}
} }
} }
} }
} }
#[cfg(feature = "hot_reload")] #[cfg(feature = "hot_reload")]
#[test] // interns a object into a static object, resusing the value if it already exists
fn create_template() { pub(crate) fn intern<T: Eq + Hash + Send + Sync + ?Sized + 'static>(
let input = quote! { s: impl Into<Intern<T>>,
svg { ) -> &'static T {
width: 100, s.into().as_ref()
height: "100px",
"width2": 100,
"height2": "100px",
p {
"hello world"
}
{(0..10).map(|i| rsx!{"{i}"})}
}
};
struct Mock;
impl HotReloadingContext for Mock {
fn map_attribute(
element_name_rust: &str,
attribute_name_rust: &str,
) -> Option<(&'static str, Option<&'static str>)> {
match element_name_rust {
"svg" => match attribute_name_rust {
"width" => Some(("width", Some("style"))),
"height" => Some(("height", Some("style"))),
_ => None,
},
_ => None,
}
}
fn map_element(element_name_rust: &str) -> Option<(&'static str, Option<&'static str>)> {
match element_name_rust {
"svg" => Some(("svg", Some("svg"))),
_ => None,
}
}
}
let call_body: CallBody = syn::parse2(input).unwrap();
let template = call_body.update_template::<Mock>(None, "testing").unwrap();
dbg!(template);
assert_eq!(
template,
Template {
name: "testing",
roots: &[TemplateNode::Element {
tag: "svg",
namespace: Some("svg"),
attrs: &[
TemplateAttribute::Dynamic { id: 0 },
TemplateAttribute::Static {
name: "height",
namespace: Some("style"),
value: "100px",
},
TemplateAttribute::Dynamic { id: 1 },
TemplateAttribute::Static {
name: "height2",
namespace: None,
value: "100px",
},
],
children: &[
TemplateNode::Element {
tag: "p",
namespace: None,
attrs: &[],
children: &[TemplateNode::Text {
text: "hello world",
}],
},
TemplateNode::Dynamic { id: 0 }
],
}],
node_paths: &[&[0, 1,],],
attr_paths: &[&[0,], &[0,],],
},
)
}
#[cfg(feature = "hot_reload")]
#[test]
fn diff_template() {
#[allow(unused, non_snake_case)]
fn Comp() -> dioxus_core::Element {
None
}
let input = quote! {
svg {
width: 100,
height: "100px",
"width2": 100,
"height2": "100px",
p {
"hello world"
}
{(0..10).map(|i| rsx!{"{i}"})},
{(0..10).map(|i| rsx!{"{i}"})},
{(0..11).map(|i| rsx!{"{i}"})},
Comp{}
}
};
#[derive(Debug)]
struct Mock;
impl HotReloadingContext for Mock {
fn map_attribute(
element_name_rust: &str,
attribute_name_rust: &str,
) -> Option<(&'static str, Option<&'static str>)> {
match element_name_rust {
"svg" => match attribute_name_rust {
"width" => Some(("width", Some("style"))),
"height" => Some(("height", Some("style"))),
_ => None,
},
_ => None,
}
}
fn map_element(element_name_rust: &str) -> Option<(&'static str, Option<&'static str>)> {
match element_name_rust {
"svg" => Some(("svg", Some("svg"))),
_ => None,
}
}
}
let call_body1: CallBody = syn::parse2(input).unwrap();
let template = call_body1.update_template::<Mock>(None, "testing").unwrap();
dbg!(template);
// scrambling the attributes should not cause a full rebuild
let input = quote! {
div {
"width2": 100,
height: "100px",
"height2": "100px",
width: 100,
Comp{}
{(0..11).map(|i| rsx!{"{i}"})},
{(0..10).map(|i| rsx!{"{i}"})},
{(0..10).map(|i| rsx!{"{i}"})},
p {
"hello world"
}
}
};
let call_body2: CallBody = syn::parse2(input).unwrap();
let template = call_body2
.update_template::<Mock>(Some(call_body1), "testing")
.unwrap();
dbg!(template);
assert_eq!(
template,
Template {
name: "testing",
roots: &[TemplateNode::Element {
tag: "div",
namespace: None,
attrs: &[
TemplateAttribute::Dynamic { id: 1 },
TemplateAttribute::Static {
name: "height",
namespace: None,
value: "100px",
},
TemplateAttribute::Static {
name: "height2",
namespace: None,
value: "100px",
},
TemplateAttribute::Dynamic { id: 0 },
],
children: &[
TemplateNode::Dynamic { id: 3 },
TemplateNode::Dynamic { id: 2 },
TemplateNode::Dynamic { id: 1 },
TemplateNode::Dynamic { id: 0 },
TemplateNode::Element {
tag: "p",
namespace: None,
attrs: &[],
children: &[TemplateNode::Text {
text: "hello world",
}],
},
],
}],
node_paths: &[&[0, 3], &[0, 2], &[0, 1], &[0, 0]],
attr_paths: &[&[0], &[0]]
},
)
} }

View file

@ -0,0 +1,8 @@
/// Information about the location of the call to a component
///
/// This will be filled in when the dynamiccontext is built, filling in the file:line:column:id format
///
#[derive(PartialEq, Eq, Clone, Debug, Hash, Default)]
pub struct CallerLocation {
inner: Option<String>,
}

View file

@ -1,3 +1,5 @@
use self::location::CallerLocation;
use super::*; use super::*;
use proc_macro2::{Span, TokenStream as TokenStream2}; use proc_macro2::{Span, TokenStream as TokenStream2};
@ -21,11 +23,12 @@ Parse
#[derive(PartialEq, Eq, Clone, Debug, Hash)] #[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub enum BodyNode { pub enum BodyNode {
Element(Element), Element(Element),
Text(IfmtInput),
RawExpr(Expr),
Component(Component), Component(Component),
ForLoop(ForLoop), ForLoop(ForLoop),
IfChain(IfChain), IfChain(IfChain),
Text(IfmtInput),
RawExpr(Expr),
} }
impl BodyNode { impl BodyNode {
@ -138,92 +141,45 @@ impl Parse for BodyNode {
impl ToTokens for BodyNode { impl ToTokens for BodyNode {
fn to_tokens(&self, tokens: &mut TokenStream2) { fn to_tokens(&self, tokens: &mut TokenStream2) {
match &self { match self {
BodyNode::Element(_) => { BodyNode::Element(_) => {
unimplemented!("Elements are statically created in the template") unimplemented!("Elements are statically created in the template")
} }
BodyNode::Component(comp) => comp.to_tokens(tokens),
// Text is simple, just write it out
BodyNode::Text(txt) => tokens.append_all(quote! { BodyNode::Text(txt) => tokens.append_all(quote! {
dioxus_core::DynamicNode::Text(dioxus_core::VText::new(#txt.to_string())) dioxus_core::DynamicNode::Text(dioxus_core::VText::new(#txt.to_string()))
}), }),
// Expressons too
BodyNode::RawExpr(exp) => tokens.append_all(quote! { BodyNode::RawExpr(exp) => tokens.append_all(quote! {
{ {
let ___nodes = (#exp).into_dyn_node(); let ___nodes = (#exp).into_dyn_node();
___nodes ___nodes
} }
}), }),
BodyNode::ForLoop(exp) => {
let ForLoop {
pat, expr, body, ..
} = exp;
let renderer: TemplateRenderer = TemplateRenderer { // todo:
roots: body, //
location: None, // Component children should also participate in hotreloading
}; // This is a *little* hard since components might not be able to take children in the
// first place. I'm sure there's a hacky way to allow this... but it's not quite as
// straightforward as a for loop.
//
// It might involve always generating a `children` field on the component and always
// populating it with an empty template. This might lose the typesafety of whether
// or not a component can even accept children - essentially allowing childrne in
// every component - so it'd be breaking - but it would/could work.
BodyNode::Component(comp) => tokens.append_all(quote! { #comp }),
// Signals expose an issue with temporary lifetimes BodyNode::ForLoop(exp) => tokens.append_all(quote! { #exp }),
// We need to directly render out the nodes first to collapse their lifetime to <'a>
// And then we can return them into the dyn loop
tokens.append_all(quote! {
{
let ___nodes = (#expr).into_iter().map(|#pat| { #renderer }).into_dyn_node();
___nodes
}
})
}
BodyNode::IfChain(chain) => {
let mut body = TokenStream2::new();
let mut terminated = false;
let mut elif = Some(chain); BodyNode::IfChain(chain) => tokens.append_all(quote! { #chain }),
while let Some(chain) = elif {
let IfChain {
if_token,
cond,
then_branch,
else_if_branch,
else_branch,
} = chain;
let mut renderer: TemplateRenderer = TemplateRenderer {
roots: then_branch,
location: None,
};
body.append_all(quote! { #if_token #cond { Some({#renderer}) } });
if let Some(next) = else_if_branch {
body.append_all(quote! { else });
elif = Some(next);
} else if let Some(else_branch) = else_branch {
renderer.roots = else_branch;
body.append_all(quote! { else { Some({#renderer}) } });
terminated = true;
break;
} else {
elif = None;
}
}
if !terminated {
body.append_all(quote! {
else { None }
});
}
tokens.append_all(quote! {
{
let ___nodes = (#body).into_dyn_node();
___nodes
}
});
}
} }
} }
} }
#[non_exhaustive]
#[derive(PartialEq, Eq, Clone, Debug, Hash)] #[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub struct ForLoop { pub struct ForLoop {
pub for_token: Token![for], pub for_token: Token![for],
@ -232,6 +188,7 @@ pub struct ForLoop {
pub expr: Box<Expr>, pub expr: Box<Expr>,
pub body: Vec<BodyNode>, pub body: Vec<BodyNode>,
pub brace_token: token::Brace, pub brace_token: token::Brace,
pub location: CallerLocation,
} }
impl Parse for ForLoop { impl Parse for ForLoop {
@ -251,11 +208,33 @@ impl Parse for ForLoop {
in_token, in_token,
body, body,
brace_token, brace_token,
location: CallerLocation::default(),
expr: Box::new(expr), expr: Box::new(expr),
}) })
} }
} }
impl ToTokens for ForLoop {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let ForLoop {
pat, expr, body, ..
} = self;
let renderer = TemplateRenderer::as_tokens(body, None);
// Signals expose an issue with temporary lifetimes
// We need to directly render out the nodes first to collapse their lifetime to <'a>
// And then we can return them into the dyn loop
tokens.append_all(quote! {
{
let ___nodes = (#expr).into_iter().map(|#pat| { #renderer }).into_dyn_node();
___nodes
}
})
}
}
#[non_exhaustive]
#[derive(PartialEq, Eq, Clone, Debug, Hash)] #[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub struct IfChain { pub struct IfChain {
pub if_token: Token![if], pub if_token: Token![if],
@ -263,6 +242,7 @@ pub struct IfChain {
pub then_branch: Vec<BodyNode>, pub then_branch: Vec<BodyNode>,
pub else_if_branch: Option<Box<IfChain>>, pub else_if_branch: Option<Box<IfChain>>,
pub else_branch: Option<Vec<BodyNode>>, pub else_branch: Option<Vec<BodyNode>>,
pub location: CallerLocation,
} }
impl Parse for IfChain { impl Parse for IfChain {
@ -294,6 +274,56 @@ impl Parse for IfChain {
then_branch, then_branch,
else_if_branch, else_if_branch,
else_branch, else_branch,
location: CallerLocation::default(),
})
}
}
impl ToTokens for IfChain {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let mut body = TokenStream2::new();
let mut terminated = false;
let mut elif = Some(self);
while let Some(chain) = elif {
let IfChain {
if_token,
cond,
then_branch,
else_if_branch,
else_branch,
..
} = chain;
let renderer = TemplateRenderer::as_tokens(then_branch, None);
body.append_all(quote! { #if_token #cond { Some({#renderer}) } });
if let Some(next) = else_if_branch {
body.append_all(quote! { else });
elif = Some(next);
} else if let Some(else_branch) = else_branch {
let renderer = TemplateRenderer::as_tokens(else_branch, None);
body.append_all(quote! { else { Some({#renderer}) } });
terminated = true;
break;
} else {
elif = None;
}
}
if !terminated {
body.append_all(quote! {
else { None }
});
}
tokens.append_all(quote! {
{
let ___nodes = (#body).into_dyn_node();
___nodes
}
}) })
} }
} }

View file

@ -0,0 +1,126 @@
use crate::*;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
pub struct TemplateRenderer<'a> {
pub roots: &'a [BodyNode],
location: Option<String>,
}
impl<'a> TemplateRenderer<'a> {
/// Render the contents of the callbody out with a specific location
///
/// This will cascade location information down the tree if it already hasn't been set
pub fn as_tokens(roots: &'a [BodyNode], location: Option<String>) -> TokenStream2 {
TemplateRenderer::render(Self { roots, location })
}
fn render(mut self) -> TokenStream2 {
// Create a new dynamic context that tracks the state of all the dynamic nodes
// We have no old template, to seed it with, so it'll just be used for rendering
let mut context = DynamicContext::default();
// If we have an implicit key, then we need to write its tokens
let key_tokens = match self.implicit_key() {
Some(tok) => quote! { Some( #tok.to_string() ) },
None => quote! { None },
};
// Get the tokens we'll use as the ID of the template
// This follows the file:line:column:id format
let name = self.get_template_id_tokens();
// Render the static nodes, generating the mapping of dynamic
// This will modify the bodynodes in place - sorry about that
let roots = self.render_body_nodes(&mut context);
let dynamic_nodes = &context.dynamic_nodes;
let dyn_attr_printer = context
.dynamic_attributes
.iter()
.map(|attrs| AttributeType::merge_quote(attrs));
let node_paths = context.node_paths.iter().map(|it| quote!(&[#(#it),*]));
let attr_paths = context.attr_paths.iter().map(|it| quote!(&[#(#it),*]));
quote! {
static TEMPLATE: dioxus_core::Template = dioxus_core::Template {
name: #name,
roots: #roots,
node_paths: &[ #(#node_paths),* ],
attr_paths: &[ #(#attr_paths),* ],
};
{
// NOTE: Allocating a temporary is important to make reads within rsx drop before the value is returned
let __vnodes = dioxus_core::VNode::new(
#key_tokens,
TEMPLATE,
Box::new([ #( #dynamic_nodes),* ]),
Box::new([ #(#dyn_attr_printer),* ]),
);
__vnodes
}
}
}
fn get_template_id_tokens(&self) -> TokenStream2 {
match self.location {
Some(ref loc) => quote! { #loc },
None => {
// Get the root:column:id tag we'll use as the ID of the template
let root_col = self.get_root_col_id();
quote! {
concat!(
file!(),
":",
line!(),
":",
column!(),
":",
#root_col
)
}
}
}
}
fn get_root_col_id(&self) -> String {
let root_col = match self.roots.first() {
Some(first_root) => {
let first_root_span = format!("{:?}", first_root.span());
first_root_span
.rsplit_once("..")
.and_then(|(_, after)| after.split_once(')').map(|(before, _)| before))
.unwrap_or_default()
.to_string()
}
_ => "0".to_string(),
};
root_col
}
fn implicit_key(&self) -> Option<IfmtInput> {
let key = match self.roots.first() {
Some(BodyNode::Element(el)) if self.roots.len() == 1 => el.key.clone(),
Some(BodyNode::Component(comp)) if self.roots.len() == 1 => comp.key().cloned(),
_ => None,
};
key
}
/// Render a list of BodyNodes as a static array (&[...])
pub fn render_body_nodes(&mut self, context: &mut DynamicContext<'a>) -> TokenStream2 {
let root_printer = self
.roots
.iter()
.enumerate()
.map(|(idx, root)| context.render_children_nodes(idx, root));
// Render the static nodes, generating the mapping of dynamic
quote! {
&[ #( #root_printer ),* ]
}
}
}

View file

@ -0,0 +1,45 @@
//! Logic for handlng sub-templates
//!
//! These are things like for/if and component calls that are expanded by the contaning macro but turn into templates
//! separate template calls in the final output.
//!
//! Instead of recursively expanding sub-templates in place - and thus losing location information - we collect them
//! into a list and then expand them at the end.
//!
//! The goal here is to provide a stable index for each sub-template in a way that we can find the for/if statements later.
//! This requires some sort of hashing + indexing of the sub-templates.
//!
//! When expanding the for/if statements, we still want to expand them in the correct order, but we defer the actual token
//! expansion until the end. This way the `TEMPLATE` static that subtemplate references is available *before* we convert
//! an `if` statement into an actual vnode.
//!
//! Expanding a template will generate two token streams - one for the vnode creation and one for list of templates.
//!
//! While semantically it might make sense for a macro call that sees its dynamic contents to own those static nodes,
//! core is currently designed around the assumption that the list of Dynamic attributes matches 1:1 the template it
//! belongs to. This means that whatever dynamic nodes are rendered need to match the template they are rendered in,
//! hence why we need to spit out true subtemplates rather than just building a "super template".
//!
//! I would prefer we used the "super template" strategy since it's more flexible when diffing - no need to keep track
//! of nested templates, and we can just diff the super template and the sub templates as a whole.
//!
//! However core would need to be redesigned to support this strategy, and I'm not sure if it's worth the effort.
//! It's just conceptually easier to have the sub-templates be a part of the template they are rendered in, and then
//! deal with the complicated rsx diffing logic in the rsx crate itself.
//!
//! /// The subtemplates that we've discovered that need to be updated
//! /// These will be collected just by recursing the body nodes of various items like for/if/components etc
//! ///
//! /// The locations of these templates will be the same location as the original template, with
//! /// the addition of the dynamic node index that the template belongs to.
//! ///
//! /// rsx! { <-------- this is the location of the template "file.rs:123:0:0"
//! /// div {
//! /// for i in 0..10 { <-------- dyn_node(0)
//! /// "hi" <-------- template with location "file.rs:123:0:1" (original name, but with the dynamic node index)
//! /// }
//! /// }
//! /// }
//! ///
//! /// We only track this when the template changes
//! pub discovered_templates: Vec<Template>,

1
packages/rsx/src/util.rs Normal file
View file

@ -0,0 +1 @@

View file

@ -0,0 +1,90 @@
#![allow(unused)]
use dioxus_rsx::{
hot_reload::{diff_rsx, template_location, ChangedRsx, DiffResult},
CallBody, HotReloadingContext,
};
use quote::quote;
use syn::{parse::Parse, spanned::Spanned, File};
#[derive(Debug)]
struct Mock;
impl HotReloadingContext for Mock {
fn map_attribute(
element_name_rust: &str,
attribute_name_rust: &str,
) -> Option<(&'static str, Option<&'static str>)> {
match element_name_rust {
"svg" => match attribute_name_rust {
"width" => Some(("width", Some("style"))),
"height" => Some(("height", Some("style"))),
_ => None,
},
_ => None,
}
}
fn map_element(element_name_rust: &str) -> Option<(&'static str, Option<&'static str>)> {
match element_name_rust {
"svg" => Some(("svg", Some("svg"))),
_ => None,
}
}
}
#[test]
fn testing_for_pattern() {
let old = quote! {
div {
for item in vec![1, 2, 3] {
div { "123" }
div { "asasddasdasd" }
}
}
};
let new = quote! {
div {
for item in vec![1, 2, 3] {
div { "asasddasdasd" }
}
}
};
let old: CallBody = syn::parse2(old).unwrap();
let new: CallBody = syn::parse2(new).unwrap();
let updated = old.update_template::<Mock>(Some(new), "testing");
// currently, modifying a for loop is not hot reloadable
// We want to change this...
assert!(updated.is_none());
// let updated = old.update_template::<Mock>(Some(new), "testing").unwrap();
// let old = include_str!(concat!("./valid/for_.old.rsx"));
// let new = include_str!(concat!("./valid/for_.new.rsx"));
// let (old, new) = load_files(old, new);
// let DiffResult::RsxChanged { rsx_calls } = diff_rsx(&new, &old) else {
// panic!("Expected a rsx call to be changed")
// };
// for calls in rsx_calls {
// let ChangedRsx { old, new } = calls;
// let old_start = old.span().start();
// let old_call_body = syn::parse2::<CallBody>(old.tokens).unwrap();
// let new_call_body = syn::parse2::<CallBody>(new).unwrap();
// let leaked_location = Box::leak(template_location(old_start, file).into_boxed_str());
// let hotreloadable_template =
// new_call_body.update_template::<Ctx>(Some(old_call_body), leaked_location);
// dbg!(hotreloadable_template);
// }
// dbg!(rsx_calls);
}

View file

@ -1,53 +1,49 @@
use dioxus_rsx::hot_reload::{diff_rsx, DiffResult}; use dioxus_rsx::hot_reload::{diff_rsx, DiffResult};
use syn::File; use syn::File;
macro_rules! assert_rsx_changed {
(
$( #[doc = $doc:expr] )*
$name:ident
) => {
$( #[doc = $doc] )*
#[test]
fn $name() {
let old = include_str!(concat!("./valid/", stringify!($name), ".old.rsx"));
let new = include_str!(concat!("./valid/", stringify!($name), ".new.rsx"));
let (old, new) = load_files(old, new);
assert!(matches!( diff_rsx(&new, &old), DiffResult::RsxChanged { .. }));
}
};
}
macro_rules! assert_code_changed {
(
$( #[doc = $doc:expr] )*
$name:ident
) => {
$( #[doc = $doc] )*
#[test]
fn $name() {
let old = include_str!(concat!("./invalid/", stringify!($name), ".old.rsx"));
let new = include_str!(concat!("./invalid/", stringify!($name), ".new.rsx"));
let (old, new) = load_files(old, new);
assert!(matches!(diff_rsx(&new, &old), DiffResult::CodeChanged(_) ));
}
};
}
fn load_files(old: &str, new: &str) -> (File, File) { fn load_files(old: &str, new: &str) -> (File, File) {
let old = syn::parse_file(old).unwrap(); let old = syn::parse_file(old).unwrap();
let new = syn::parse_file(new).unwrap(); let new = syn::parse_file(new).unwrap();
(old, new) (old, new)
} }
#[test] assert_rsx_changed![combo];
fn hotreloads() { assert_rsx_changed![expr];
let (old, new) = load_files( assert_rsx_changed![for_];
include_str!("./valid/expr.old.rsx"), assert_rsx_changed![if_];
include_str!("./valid/expr.new.rsx"), assert_rsx_changed![let_];
); assert_rsx_changed![nested];
assert!(matches!( assert_code_changed![changedexpr];
diff_rsx(&new, &old),
DiffResult::RsxChanged { .. }
));
let (old, new) = load_files(
include_str!("./valid/let.old.rsx"),
include_str!("./valid/let.new.rsx"),
);
assert!(matches!(
diff_rsx(&new, &old),
DiffResult::RsxChanged { .. }
));
let (old, new) = load_files(
include_str!("./valid/combo.old.rsx"),
include_str!("./valid/combo.new.rsx"),
);
assert!(matches!(
diff_rsx(&new, &old),
DiffResult::RsxChanged { .. }
));
}
#[test]
fn doesnt_hotreload() {
let (old, new) = load_files(
include_str!("./invalid/changedexpr.old.rsx"),
include_str!("./invalid/changedexpr.new.rsx"),
);
let res = diff_rsx(&new, &old);
dbg!(&res);
assert!(matches!(res, DiffResult::CodeChanged(_)));
}

View file

@ -0,0 +1,112 @@
use dioxus_rsx::{CallBody, HotReloadingContext};
use quote::quote;
struct Mock;
impl HotReloadingContext for Mock {
fn map_attribute(
element_name_rust: &str,
attribute_name_rust: &str,
) -> Option<(&'static str, Option<&'static str>)> {
match element_name_rust {
"svg" => match attribute_name_rust {
"width" => Some(("width", Some("style"))),
"height" => Some(("height", Some("style"))),
_ => None,
},
_ => None,
}
}
fn map_element(element_name_rust: &str) -> Option<(&'static str, Option<&'static str>)> {
match element_name_rust {
"svg" => Some(("svg", Some("svg"))),
_ => None,
}
}
}
#[test]
fn create_template() {
let input = quote! {
svg {
width: 100,
height: "100px",
"width2": 100,
"height2": "100px",
p { "hello world" }
{(0..10).map(|i| rsx!{"{i}"})}
}
};
let call_body: CallBody = syn::parse2(input).unwrap();
let new_template = call_body.update_template::<Mock>(None, "testing").unwrap();
insta::assert_debug_snapshot!(new_template);
}
#[test]
fn diff_template() {
#[allow(unused, non_snake_case)]
fn Comp() -> dioxus_core::Element {
None
}
let input = quote! {
svg {
width: 100,
height: "100px",
"width2": 100,
"height2": "100px",
p { "hello world" }
{(0..10).map(|i| rsx!{"{i}"})},
{(0..10).map(|i| rsx!{"{i}"})},
{(0..11).map(|i| rsx!{"{i}"})},
Comp {}
}
};
let call_body1: CallBody = syn::parse2(input).unwrap();
let created_template = call_body1.update_template::<Mock>(None, "testing").unwrap();
insta::assert_debug_snapshot!(created_template);
// scrambling the attributes should not cause a full rebuild
let input = quote! {
div {
"width2": 100,
height: "100px",
"height2": "100px",
width: 100,
Comp {}
{(0..11).map(|i| rsx!{"{i}"})},
{(0..10).map(|i| rsx!{"{i}"})},
{(0..10).map(|i| rsx!{"{i}"})},
p {
"hello world"
}
}
};
let call_body2: CallBody = syn::parse2(input).unwrap();
let new_template = call_body2
.update_template::<Mock>(Some(call_body1), "testing")
.unwrap();
insta::assert_debug_snapshot!(new_template);
}
#[test]
fn changing_forloops_is_okay() {
let input = quote! {
div {
for i in 0..10 {
div { "123" }
"asdasd"
}
}
};
let call_body: CallBody = syn::parse2(input).unwrap();
let new_template = call_body.update_template::<Mock>(None, "testing").unwrap();
dbg!(new_template);
}

View file

@ -0,0 +1,64 @@
---
source: packages/rsx/tests/rsx_diff.rs
expression: template
---
Template {
name: "testing",
roots: [
Element {
tag: "svg",
namespace: Some(
"svg",
),
attrs: [
Dynamic {
id: 0,
},
Static {
name: "height",
value: "100px",
namespace: Some(
"style",
),
},
Dynamic {
id: 1,
},
Static {
name: "height2",
value: "100px",
namespace: None,
},
],
children: [
Element {
tag: "p",
namespace: None,
attrs: [],
children: [
Text {
text: "hello world",
},
],
},
Dynamic {
id: 0,
},
],
},
],
node_paths: [
[
0,
1,
],
],
attr_paths: [
[
0,
],
[
0,
],
],
}

View file

@ -0,0 +1,81 @@
---
source: packages/rsx/tests/rsx_diff.rs
expression: new_template
---
Template {
name: "testing",
roots: [
Element {
tag: "div",
namespace: None,
attrs: [
Dynamic {
id: 1,
},
Static {
name: "height",
value: "100px",
namespace: None,
},
Static {
name: "height2",
value: "100px",
namespace: None,
},
Dynamic {
id: 0,
},
],
children: [
Dynamic {
id: 3,
},
Dynamic {
id: 2,
},
Dynamic {
id: 1,
},
Dynamic {
id: 0,
},
Element {
tag: "p",
namespace: None,
attrs: [],
children: [
Text {
text: "hello world",
},
],
},
],
},
],
node_paths: [
[
0,
3,
],
[
0,
2,
],
[
0,
1,
],
[
0,
0,
],
],
attr_paths: [
[
0,
],
[
0,
],
],
}

View file

@ -0,0 +1,85 @@
---
source: packages/rsx/tests/rsx_diff.rs
expression: created_template
---
Template {
name: "testing",
roots: [
Element {
tag: "svg",
namespace: Some(
"svg",
),
attrs: [
Dynamic {
id: 0,
},
Static {
name: "height",
value: "100px",
namespace: Some(
"style",
),
},
Dynamic {
id: 1,
},
Static {
name: "height2",
value: "100px",
namespace: None,
},
],
children: [
Element {
tag: "p",
namespace: None,
attrs: [],
children: [
Text {
text: "hello world",
},
],
},
Dynamic {
id: 0,
},
Dynamic {
id: 1,
},
Dynamic {
id: 2,
},
Dynamic {
id: 3,
},
],
},
],
node_paths: [
[
0,
1,
],
[
0,
2,
],
[
0,
3,
],
[
0,
4,
],
],
attr_paths: [
[
0,
],
[
0,
],
],
}

View file

@ -0,0 +1,9 @@
use dioxus::prelude::*;
pub fn CoolChild() -> Element {
rsx! {
for items in vec![1, 2, 3] {
div { "asasddasdasd" }
}
}
}

View file

@ -0,0 +1,10 @@
use dioxus::prelude::*;
pub fn CoolChild() -> Element {
rsx! {
for items in vec![1, 2, 3] {
div { "123" }
div { "asasddasdasd" }
}
}
}

View file

@ -0,0 +1,10 @@
use dioxus::prelude::*;
pub fn CoolChild() -> Element {
rsx! {
if cond() {
div { "123" }
div { "asasddasdasd" }
}
}
}

View file

@ -0,0 +1,9 @@
use dioxus::prelude::*;
pub fn CoolChild() -> Element {
rsx! {
if cond() {
div { "asasddasdasd" }
}
}
}

View file

@ -0,0 +1,10 @@
use dioxus::prelude::*;
pub fn CoolChild() -> Element {
rsx! {
ForLoop {
div { "123" }
div { "asasddasdasd" }
}
}
}

View file

@ -0,0 +1,9 @@
use dioxus::prelude::*;
pub fn CoolChild() -> Element {
rsx! {
ForLoop {
div { "asasddasdasd" }
}
}
}