mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-26 14:10:20 +00:00
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:
parent
b19a546c0a
commit
eb79e61642
31 changed files with 1413 additions and 811 deletions
29
Cargo.lock
generated
29
Cargo.lock
generated
|
@ -2469,6 +2469,7 @@ name = "dioxus-rsx"
|
|||
version = "0.5.0-alpha.2"
|
||||
dependencies = [
|
||||
"dioxus-core 0.5.0-alpha.2",
|
||||
"insta",
|
||||
"internment",
|
||||
"krates",
|
||||
"proc-macro2",
|
||||
|
@ -4728,6 +4729,19 @@ dependencies = [
|
|||
"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]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
|
@ -7931,6 +7945,12 @@ version = "0.1.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21"
|
||||
|
||||
[[package]]
|
||||
name = "simple_logger"
|
||||
version = "4.3.3"
|
||||
|
@ -10250,6 +10270,15 @@ version = "0.8.10"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "yansi"
|
||||
version = "0.5.1"
|
||||
|
|
13
Cargo.toml
13
Cargo.toml
|
@ -114,6 +114,19 @@ axum-extra = "0.9.2"
|
|||
reqwest = "0.11.24"
|
||||
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
|
||||
[profile.cli-dev]
|
||||
inherits = "dev"
|
||||
|
|
|
@ -262,6 +262,7 @@ impl<'a> Writer<'a> {
|
|||
then_branch,
|
||||
else_if_branch,
|
||||
else_branch,
|
||||
..
|
||||
} = chain;
|
||||
|
||||
write!(
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
imports_granularity = "Crate"
|
|
@ -143,6 +143,7 @@ pub fn collect_svgs(children: &mut [BodyNode], out: &mut Vec<BodyNode>) {
|
|||
manual_props: None,
|
||||
key: None,
|
||||
brace: Default::default(),
|
||||
location: Default::default(),
|
||||
});
|
||||
|
||||
std::mem::swap(child, &mut new_comp);
|
||||
|
|
8
packages/rsx/.vscode/settings.json
vendored
Normal file
8
packages/rsx/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"rust-analyzer.check.workspace": false,
|
||||
// "rust-analyzer.check.extraArgs": [
|
||||
// "--features",
|
||||
// "hot_reload"
|
||||
// ],
|
||||
"rust-analyzer.cargo.features": "all"
|
||||
}
|
|
@ -23,7 +23,11 @@ krates = { version = "0.16.6", optional = true }
|
|||
tracing = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["html"]
|
||||
default = ["html", "hot_reload"]
|
||||
hot_reload = ["krates", "internment", "dioxus-core"]
|
||||
serde = ["dep:serde"]
|
||||
html = []
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1.36.1"
|
||||
|
|
|
@ -8,7 +8,10 @@ use syn::{parse_quote, spanned::Spanned, Expr, ExprIf, Ident, LitStr};
|
|||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
pub enum AttributeType {
|
||||
/// An attribute that is known
|
||||
Named(ElementAttrNamed),
|
||||
|
||||
/// An attribute that's being spread in via the `..` syntax
|
||||
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)]
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
//! - [ ] Keys
|
||||
//! - [ ] Properties spreading with with `..` syntax
|
||||
|
||||
use self::{location::CallerLocation, renderer::TemplateRenderer};
|
||||
|
||||
use super::*;
|
||||
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
|
@ -32,6 +34,7 @@ pub struct Component {
|
|||
pub children: Vec<BodyNode>,
|
||||
pub manual_props: Option<Expr>,
|
||||
pub brace: syn::token::Brace,
|
||||
pub location: CallerLocation,
|
||||
}
|
||||
|
||||
impl Parse for Component {
|
||||
|
@ -88,6 +91,7 @@ impl Parse for Component {
|
|||
}
|
||||
|
||||
Ok(Self {
|
||||
location: CallerLocation::default(),
|
||||
name,
|
||||
prop_gen_args,
|
||||
fields,
|
||||
|
@ -179,20 +183,10 @@ impl Component {
|
|||
toks.append_all(quote! {#field})
|
||||
}
|
||||
if !self.children.is_empty() {
|
||||
let renderer: TemplateRenderer = TemplateRenderer {
|
||||
roots: &self.children,
|
||||
location: None,
|
||||
};
|
||||
|
||||
toks.append_all(quote! {
|
||||
.children(
|
||||
Some({ #renderer })
|
||||
)
|
||||
});
|
||||
let renderer = TemplateRenderer::as_tokens(&self.children, None);
|
||||
toks.append_all(quote! { .children( Some({ #renderer }) ) });
|
||||
}
|
||||
toks.append_all(quote! {
|
||||
.build()
|
||||
});
|
||||
toks.append_all(quote! { .build() });
|
||||
toks
|
||||
}
|
||||
|
||||
|
|
416
packages/rsx/src/context.rs
Normal file
416
packages/rsx/src/context.rs
Normal 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())
|
||||
}
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
use super::{
|
||||
hot_reload_diff::{diff_rsx, DiffResult},
|
||||
ChangedRsx,
|
||||
};
|
||||
use crate::{CallBody, HotReloadingContext};
|
||||
use dioxus_core::{
|
||||
prelude::{TemplateAttribute, TemplateNode},
|
||||
|
@ -9,17 +13,11 @@ pub use proc_macro2::TokenStream;
|
|||
pub use std::collections::HashMap;
|
||||
pub use std::sync::Mutex;
|
||||
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::File, io::Read};
|
||||
pub use syn::__private::ToTokens;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
use super::{
|
||||
hot_reload_diff::{diff_rsx, DiffResult},
|
||||
ChangedRsx,
|
||||
};
|
||||
|
||||
pub enum UpdateResult {
|
||||
UpdatedRsx(Vec<Template>),
|
||||
|
||||
|
@ -40,7 +38,7 @@ pub struct FileMap<Ctx: HotReloadingContext> {
|
|||
|
||||
in_workspace: HashMap<PathBuf, Option<PathBuf>>,
|
||||
|
||||
phantom: std::marker::PhantomData<Ctx>,
|
||||
phantom: PhantomData<Ctx>,
|
||||
}
|
||||
|
||||
/// A cached file that has been parsed
|
||||
|
@ -75,7 +73,7 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
|
|||
let mut map = Self {
|
||||
map,
|
||||
in_workspace: HashMap::new(),
|
||||
phantom: std::marker::PhantomData,
|
||||
phantom: PhantomData,
|
||||
};
|
||||
|
||||
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 column = old_start.column + 1;
|
||||
let location = file.display().to_string()
|
||||
|
|
|
@ -19,143 +19,111 @@ mod errors;
|
|||
mod attribute;
|
||||
mod component;
|
||||
mod element;
|
||||
#[cfg(feature = "hot_reload")]
|
||||
pub mod hot_reload;
|
||||
mod ifmt;
|
||||
mod location;
|
||||
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
|
||||
pub use attribute::*;
|
||||
pub use component::*;
|
||||
#[cfg(feature = "hot_reload")]
|
||||
use dioxus_core::{Template, TemplateAttribute, TemplateNode};
|
||||
pub use context::DynamicContext;
|
||||
pub use element::*;
|
||||
#[cfg(feature = "hot_reload")]
|
||||
pub use hot_reload::HotReloadingContext;
|
||||
pub use ifmt::*;
|
||||
#[cfg(feature = "hot_reload")]
|
||||
use internment::Intern;
|
||||
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 quote::{quote, ToTokens, TokenStreamExt};
|
||||
use renderer::TemplateRenderer;
|
||||
use std::{fmt::Debug, hash::Hash};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
Result, Token,
|
||||
};
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
// 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 {
|
||||
s.into().as_ref()
|
||||
}
|
||||
|
||||
/// Fundametnally, every CallBody is a template
|
||||
/// The Callbody is the contents of the rsx! macro
|
||||
///
|
||||
/// It is a list of BodyNodes, which are the different parts of the template.
|
||||
/// 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.
|
||||
/// 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)]
|
||||
pub struct CallBody {
|
||||
pub roots: Vec<BodyNode>,
|
||||
}
|
||||
|
||||
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
|
||||
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"
|
||||
if self.roots.is_empty() {
|
||||
return quote! { None };
|
||||
}
|
||||
|
||||
quote! {
|
||||
Some({ #body })
|
||||
}
|
||||
let body = TemplateRenderer::as_tokens(&self.roots, Some(location));
|
||||
|
||||
quote! { Some({ #body }) }
|
||||
}
|
||||
}
|
||||
|
||||
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(Self { roots })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CallBody {
|
||||
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
|
||||
let body: TemplateRenderer = TemplateRenderer {
|
||||
roots: &self.roots,
|
||||
location: None,
|
||||
};
|
||||
|
||||
// Empty templates just are placeholders for "none"
|
||||
if self.roots.is_empty() {
|
||||
return out_tokens.append_all(quote! { None });
|
||||
}
|
||||
|
||||
out_tokens.append_all(quote! {
|
||||
Some({ #body })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TemplateRenderer<'a> {
|
||||
pub roots: &'a [BodyNode],
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> TemplateRenderer<'a> {
|
||||
/// 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.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// When you implement hotreloading, you're likely just going to parse the source code into the Syn::File type, which
|
||||
/// should make retrieving the template location easy.
|
||||
///
|
||||
/// ## Note:
|
||||
///
|
||||
/// - 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.
|
||||
///
|
||||
/// ## Longer note about sub templates:
|
||||
///
|
||||
/// 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.
|
||||
/// The new implementation of this aggregates all subtemplates into the TemplateRenderer and then assigns them
|
||||
/// 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
|
||||
/// updated, giving them an option to revert to the previous template as to not require a full rebuild.
|
||||
#[cfg(feature = "hot_reload")]
|
||||
fn update_template<Ctx: HotReloadingContext>(
|
||||
&mut self,
|
||||
previous_call: Option<CallBody>,
|
||||
pub fn update_template<Ctx: HotReloadingContext>(
|
||||
&self,
|
||||
old: Option<CallBody>,
|
||||
location: &'static str,
|
||||
) -> 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();
|
||||
|
||||
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();
|
||||
}
|
||||
// Force the template node to generate us TemplateNodes, and fill in the location information
|
||||
let roots = context.populate_by_updating::<Ctx>(&self.roots)?;
|
||||
|
||||
// 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 {
|
||||
name: location,
|
||||
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) {
|
||||
let mut context = DynamicContext::default();
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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 } },
|
||||
}
|
||||
// Empty templates just are placeholders for "none"
|
||||
match self.roots.is_empty() {
|
||||
true => out_tokens.append_all(quote! { None }),
|
||||
false => {
|
||||
let body = TemplateRenderer::as_tokens(&self.roots, None);
|
||||
out_tokens.append_all(quote! { Some({ #body }) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
#[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}"})}
|
||||
}
|
||||
};
|
||||
|
||||
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]]
|
||||
},
|
||||
)
|
||||
// interns a object into a static object, resusing the value if it already exists
|
||||
pub(crate) fn intern<T: Eq + Hash + Send + Sync + ?Sized + 'static>(
|
||||
s: impl Into<Intern<T>>,
|
||||
) -> &'static T {
|
||||
s.into().as_ref()
|
||||
}
|
||||
|
|
8
packages/rsx/src/location.rs
Normal file
8
packages/rsx/src/location.rs
Normal 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>,
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
use self::location::CallerLocation;
|
||||
|
||||
use super::*;
|
||||
|
||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||
|
@ -21,11 +23,12 @@ Parse
|
|||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
pub enum BodyNode {
|
||||
Element(Element),
|
||||
Text(IfmtInput),
|
||||
RawExpr(Expr),
|
||||
|
||||
Component(Component),
|
||||
ForLoop(ForLoop),
|
||||
IfChain(IfChain),
|
||||
Text(IfmtInput),
|
||||
RawExpr(Expr),
|
||||
}
|
||||
|
||||
impl BodyNode {
|
||||
|
@ -138,92 +141,45 @@ impl Parse for BodyNode {
|
|||
|
||||
impl ToTokens for BodyNode {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
match &self {
|
||||
match self {
|
||||
BodyNode::Element(_) => {
|
||||
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! {
|
||||
dioxus_core::DynamicNode::Text(dioxus_core::VText::new(#txt.to_string()))
|
||||
}),
|
||||
|
||||
// Expressons too
|
||||
BodyNode::RawExpr(exp) => tokens.append_all(quote! {
|
||||
{
|
||||
let ___nodes = (#exp).into_dyn_node();
|
||||
___nodes
|
||||
}
|
||||
}),
|
||||
BodyNode::ForLoop(exp) => {
|
||||
let ForLoop {
|
||||
pat, expr, body, ..
|
||||
} = exp;
|
||||
|
||||
let renderer: TemplateRenderer = TemplateRenderer {
|
||||
roots: body,
|
||||
location: None,
|
||||
};
|
||||
// todo:
|
||||
//
|
||||
// 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
|
||||
// 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;
|
||||
BodyNode::ForLoop(exp) => tokens.append_all(quote! { #exp }),
|
||||
|
||||
let mut elif = Some(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
|
||||
}
|
||||
});
|
||||
}
|
||||
BodyNode::IfChain(chain) => tokens.append_all(quote! { #chain }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
pub struct ForLoop {
|
||||
pub for_token: Token![for],
|
||||
|
@ -232,6 +188,7 @@ pub struct ForLoop {
|
|||
pub expr: Box<Expr>,
|
||||
pub body: Vec<BodyNode>,
|
||||
pub brace_token: token::Brace,
|
||||
pub location: CallerLocation,
|
||||
}
|
||||
|
||||
impl Parse for ForLoop {
|
||||
|
@ -251,11 +208,33 @@ impl Parse for ForLoop {
|
|||
in_token,
|
||||
body,
|
||||
brace_token,
|
||||
location: CallerLocation::default(),
|
||||
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)]
|
||||
pub struct IfChain {
|
||||
pub if_token: Token![if],
|
||||
|
@ -263,6 +242,7 @@ pub struct IfChain {
|
|||
pub then_branch: Vec<BodyNode>,
|
||||
pub else_if_branch: Option<Box<IfChain>>,
|
||||
pub else_branch: Option<Vec<BodyNode>>,
|
||||
pub location: CallerLocation,
|
||||
}
|
||||
|
||||
impl Parse for IfChain {
|
||||
|
@ -294,6 +274,56 @@ impl Parse for IfChain {
|
|||
then_branch,
|
||||
else_if_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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
126
packages/rsx/src/renderer.rs
Normal file
126
packages/rsx/src/renderer.rs
Normal 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 ),* ]
|
||||
}
|
||||
}
|
||||
}
|
45
packages/rsx/src/sub_templates.rs
Normal file
45
packages/rsx/src/sub_templates.rs
Normal 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
1
packages/rsx/src/util.rs
Normal file
|
@ -0,0 +1 @@
|
|||
|
90
packages/rsx/tests/hotreload_pattern.rs
Normal file
90
packages/rsx/tests/hotreload_pattern.rs
Normal 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);
|
||||
}
|
|
@ -1,53 +1,49 @@
|
|||
use dioxus_rsx::hot_reload::{diff_rsx, DiffResult};
|
||||
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) {
|
||||
let old = syn::parse_file(old).unwrap();
|
||||
let new = syn::parse_file(new).unwrap();
|
||||
(old, new)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hotreloads() {
|
||||
let (old, new) = load_files(
|
||||
include_str!("./valid/expr.old.rsx"),
|
||||
include_str!("./valid/expr.new.rsx"),
|
||||
);
|
||||
assert_rsx_changed![combo];
|
||||
assert_rsx_changed![expr];
|
||||
assert_rsx_changed![for_];
|
||||
assert_rsx_changed![if_];
|
||||
assert_rsx_changed![let_];
|
||||
assert_rsx_changed![nested];
|
||||
|
||||
assert!(matches!(
|
||||
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(_)));
|
||||
}
|
||||
assert_code_changed![changedexpr];
|
||||
|
|
112
packages/rsx/tests/rsx_diff.rs
Normal file
112
packages/rsx/tests/rsx_diff.rs
Normal 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);
|
||||
}
|
64
packages/rsx/tests/snapshots/rsx_diff__create_template.snap
Normal file
64
packages/rsx/tests/snapshots/rsx_diff__create_template.snap
Normal 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,
|
||||
],
|
||||
],
|
||||
}
|
81
packages/rsx/tests/snapshots/rsx_diff__diff_template-2.snap
Normal file
81
packages/rsx/tests/snapshots/rsx_diff__diff_template-2.snap
Normal 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,
|
||||
],
|
||||
],
|
||||
}
|
85
packages/rsx/tests/snapshots/rsx_diff__diff_template.snap
Normal file
85
packages/rsx/tests/snapshots/rsx_diff__diff_template.snap
Normal 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,
|
||||
],
|
||||
],
|
||||
}
|
9
packages/rsx/tests/valid/for_.new.rsx
Normal file
9
packages/rsx/tests/valid/for_.new.rsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
pub fn CoolChild() -> Element {
|
||||
rsx! {
|
||||
for items in vec![1, 2, 3] {
|
||||
div { "asasddasdasd" }
|
||||
}
|
||||
}
|
||||
}
|
10
packages/rsx/tests/valid/for_.old.rsx
Normal file
10
packages/rsx/tests/valid/for_.old.rsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
pub fn CoolChild() -> Element {
|
||||
rsx! {
|
||||
for items in vec![1, 2, 3] {
|
||||
div { "123" }
|
||||
div { "asasddasdasd" }
|
||||
}
|
||||
}
|
||||
}
|
10
packages/rsx/tests/valid/if_.new.rsx
Normal file
10
packages/rsx/tests/valid/if_.new.rsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
pub fn CoolChild() -> Element {
|
||||
rsx! {
|
||||
if cond() {
|
||||
div { "123" }
|
||||
div { "asasddasdasd" }
|
||||
}
|
||||
}
|
||||
}
|
9
packages/rsx/tests/valid/if_.old.rsx
Normal file
9
packages/rsx/tests/valid/if_.old.rsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
pub fn CoolChild() -> Element {
|
||||
rsx! {
|
||||
if cond() {
|
||||
div { "asasddasdasd" }
|
||||
}
|
||||
}
|
||||
}
|
10
packages/rsx/tests/valid/nested.new.rsx
Normal file
10
packages/rsx/tests/valid/nested.new.rsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
pub fn CoolChild() -> Element {
|
||||
rsx! {
|
||||
ForLoop {
|
||||
div { "123" }
|
||||
div { "asasddasdasd" }
|
||||
}
|
||||
}
|
||||
}
|
9
packages/rsx/tests/valid/nested.old.rsx
Normal file
9
packages/rsx/tests/valid/nested.old.rsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
pub fn CoolChild() -> Element {
|
||||
rsx! {
|
||||
ForLoop {
|
||||
div { "asasddasdasd" }
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue