mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-14 00:17:17 +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"
|
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"
|
||||||
|
|
13
Cargo.toml
13
Cargo.toml
|
@ -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"
|
||||||
|
|
|
@ -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!(
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
imports_granularity = "Crate"
|
|
|
@ -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
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 }
|
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"
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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
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 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()
|
||||||
|
|
|
@ -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]]
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
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 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
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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 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(_)));
|
|
||||||
}
|
|
||||||
|
|
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