Improve compile times by splitting out rsx-hotreload from rsx (#2971)

* fix imports, migrate over rsx-hotreload
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2024-09-16 17:35:05 -07:00
1 parent b67122dd2a
commit 463e67cd12
41 files changed
+188 -190

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
[package]
name = "dioxus-rsx-hotreload"
edition = "2021"
version.workspace = true
[dependencies]
dioxus-rsx = { workspace = true }
internment = { workspace = true }
proc-macro2 = { workspace = true, features = ["span-locations"] }
proc-macro2-diagnostics = { workspace = true }
quote = { workspace = true }
syn = { workspace = true, features = ["full", "extra-traits", "visit", "visit-mut"] }
tracing = { workspace = true }
dioxus-core = { workspace = true }
dioxus-core-types = { workspace = true }
+145
View File
@@ -0,0 +1,145 @@
//! Compare two files and find any rsx calls that have changed
//!
//! This is used to determine if a hotreload is needed.
//! We use a special syn visitor to find all the rsx! calls in the file and then compare them to see
//! if they are the same. This visitor will actually remove the rsx! calls and replace them with a
//! dummy rsx! call. The final file type is thus mutated in place, leaving the original file idents
//! in place. We then compare the two files to see if they are the same. We're able to differentiate
//! between rust code changes and rsx code changes with much less code than the previous manual diff
//! approach.
use syn::visit_mut::VisitMut;
use syn::{File, Macro};
#[derive(Debug)]
pub struct ChangedRsx {
/// The old macro - the original RSX from the original file
pub old: Macro,
/// The new macro
pub new: Macro,
}
/// Find any rsx calls in the given file and return a list of all the rsx calls that have changed.
///
/// Takes in the two files, clones them, removes the rsx! contents and prunes any doc comments.
/// Then it compares the two files to see if they are different - if they are, the code changed.
/// Otherwise, the code is the same and we can move on to handling the changed rsx
///
/// Returns `None` if the files are the same and `Some` if they are different
/// If there are no rsx! calls in the files, the vec will be empty.
pub fn diff_rsx(new: &File, old: &File) -> Option<Vec<ChangedRsx>> {
// Make a clone of these files in place so we don't have to worry about mutating the original
let mut old = old.clone();
let mut new = new.clone();
// Collect all the rsx! macros from the old file - modifying the files in place
let old_macros = collect_from_file(&mut old);
let new_macros = collect_from_file(&mut new);
// If the number of rsx! macros is different, then it's not hotreloadable
if old_macros.len() != new_macros.len() {
return None;
}
// If the files are not the same, then it's not hotreloadable
if old != new {
return None;
}
Some(
old_macros
.into_iter()
.zip(new_macros)
.map(|(old, new)| ChangedRsx { old, new })
.collect(),
)
}
pub fn collect_from_file(file: &mut File) -> Vec<Macro> {
struct MacroCollector(Vec<Macro>);
impl VisitMut for MacroCollector {
/// Take out the rsx! macros, leaving a default in their place
fn visit_macro_mut(&mut self, dest: &mut syn::Macro) {
let name = &dest.path.segments.last().map(|i| i.ident.to_string());
if let Some("rsx" | "render") = name.as_deref() {
let mut default: syn::Macro = syn::parse_quote! { rsx! {} };
std::mem::swap(dest, &mut default);
self.0.push(default)
}
}
/// Ignore doc comments by swapping them out with a default
fn visit_attribute_mut(&mut self, i: &mut syn::Attribute) {
if i.path().is_ident("doc") {
*i = syn::parse_quote! { #[doc = ""] };
}
}
}
let mut macros = MacroCollector(vec![]);
macros.visit_file_mut(file);
macros.0
}
#[test]
fn changing_files() {
let old = r#"
use dioxus::prelude::*;
/// some comment
pub fn CoolChild() -> Element {
let a = 123;
rsx! {
div {
{some_expr()}
}
}
}"#;
let new = r#"
use dioxus::prelude::*;
/// some comment
pub fn CoolChild() -> Element {
rsx! {
div {
{some_expr()}
}
}
}"#;
let same = r#"
use dioxus::prelude::*;
/// some comment!!!!!
pub fn CoolChild() -> Element {
let a = 123;
rsx! {
div {
{some_expr()}
}
}
}"#;
let old = syn::parse_file(old).unwrap();
let new = syn::parse_file(new).unwrap();
let same = syn::parse_file(same).unwrap();
assert!(
diff_rsx(&old, &new).is_none(),
"Files with different expressions should not be hotreloadable"
);
assert!(
diff_rsx(&new, &new).is_some(),
"The same file should be reloadable with itself"
);
assert!(
diff_rsx(&old, &same).is_some(),
"Files with changed comments should be hotreloadable"
);
}
+637
View File
@@ -0,0 +1,637 @@
//! This module contains the diffing logic for rsx hot reloading.
//!
//! There's a few details that I wish we could've gotten right but we can revisit later:
//!
//! - Expanding an if chain is not possible - only its contents can be hot reloaded
//!
//! - Components that don't start with children can't be hot reloaded - IE going from `Comp {}` to `Comp { "foo" }`
//! is not possible. We could in theory allow this by seeding all Components with a `children` field.
//!
//! - Cross-templates hot reloading is not possible - multiple templates don't share the dynamic pool. This would require handling aliases
//! in hot reload diffing.
//!
//! - We've proven that binary patching is feasible but has a longer path to stabilization for all platforms.
//! Binary patching is pretty quick, actually, and *might* remove the need to literal hot reloading.
//! However, you could imagine a scenario where literal hot reloading would be useful without the
//! compiler in the loop. Ideally we can slash most of this code once patching is stable.
//!
//! ## Assigning/Scoring Templates
//!
//! We can clone most dynamic items from the last full rebuild:
//! - Dynamic text segments: `div { width: "{x}%" } -> div { width: "{x}%", height: "{x}%" }`
//! - Dynamic attributes: `div { width: dynamic } -> div { width: dynamic, height: dynamic }`
//! - Dynamic nodes: `div { {children} } -> div { {children} {children} }`
//!
//! But we cannot clone rsx bodies themselves because we cannot hot reload the new rsx body:
//! - `div { Component { "{text}" } } -> div { Component { "{text}" } Component { "hello" } }` // We can't create a template for both "{text}" and "hello"
//!
//! In some cases, two nodes with children are ambiguous. For example:
//! ```rust, ignore
//! rsx! {
//! div {
//! Component { "{text}" }
//! Component { "hello" }
//! }
//! }
//! ```
//!
//! Outside of the template, both components are compatible for hot reloading.
//!
//! After we create a list of all components with compatible names and props, we need to find the best match for the
//! template.
//!
//!
//! Dioxus uses a greedy algorithm to find the best match. We first try to create the child template with the dynamic context from the last full rebuild.
//! Then we use the child template that leaves the least unused dynamic items in the pool to create the new template.
//!
//! For the example above:
//! - Hot reloading `Component { "hello" }`:
//! - Try to hot reload the component body `"hello"` with the dynamic pool from `"{text}"`: Success with 1 unused dynamic item
//! - Try to hot reload the component body `"hello"` with the dynamic pool from `"hello"`: Success with 0 unused dynamic items
//! - We use the the template that leaves the least unused dynamic items in the pool - `"hello"`
//! - Hot reloading `Component { "{text}" }`:
//! - Try to hot reload the component body `"{text}"` with the dynamic pool from `"{text}"`: Success with 0 unused dynamic items
//! - The `"hello"` template has already been hot reloaded, so we don't try to hot reload it again
//! - We use the the template that leaves the least unused dynamic items in the pool - `"{text}"`
//!
//! Greedy algorithms are optimal when:
//! - The step we take reduces the problem size
//! - The subproblem is optimal
//!
//! In this case, hot reloading a template removes it from the pool of templates we can use to hot reload the next template which reduces the problem size.
//!
//! The subproblem is optimal because the alternative is leaving less dynamic items for the remaining templates to hot reload which just makes it
//! more difficult to match future templates.
use dioxus_core::internal::{
FmtedSegments, HotReloadAttributeValue, HotReloadDynamicAttribute, HotReloadDynamicNode,
HotReloadLiteral, HotReloadedTemplate, NamedAttribute,
};
use dioxus_core_types::HotReloadingContext;
use dioxus_rsx::*;
use std::collections::HashMap;
use crate::extensions::{html_tag_and_namespace, intern, to_template_node};
use super::last_build_state::LastBuildState;
/// A result of hot reloading
///
/// This contains information about what has changed so the hotreloader can apply the right changes
#[non_exhaustive]
#[derive(Debug, PartialEq, Clone)]
pub struct HotReloadResult {
/// The state of the last full rebuild.
full_rebuild_state: LastBuildState,
/// The child templates we have already used. As we walk through the template tree, we will run into child templates.
/// Each of those child templates also need to be hot reloaded. We keep track of which ones we've already hotreloaded
/// to avoid diffing the same template twice against different new templates.
///
/// ```rust, ignore
/// rsx! {
/// Component { class: "{class}", "{text}" } // The children of a Component is a new template
/// for item in items {
/// "{item}" // The children of a for loop is a new template
/// }
/// if true {
/// "{text}" // The children of an if chain is a new template
/// }
/// }
/// ```
///
/// If we hotreload the component, we don't need to hotreload the for loop
///
/// You should diff the result of this against the old template to see if you actually need to send down the result
pub templates: HashMap<usize, HotReloadedTemplate>,
/// The dynamic nodes for the current node
dynamic_nodes: Vec<HotReloadDynamicNode>,
/// The dynamic attributes for the current node
dynamic_attributes: Vec<HotReloadDynamicAttribute>,
/// The literal component properties for the current node
literal_component_properties: Vec<HotReloadLiteral>,
}
impl HotReloadResult {
/// Calculate the hot reload diff between two template bodies
pub fn new<Ctx: HotReloadingContext>(
full_rebuild_state: &TemplateBody,
new: &TemplateBody,
name: String,
) -> Option<Self> {
let full_rebuild_state = LastBuildState::new(full_rebuild_state, name);
let mut s = Self {
full_rebuild_state,
templates: Default::default(),
dynamic_nodes: Default::default(),
dynamic_attributes: Default::default(),
literal_component_properties: Default::default(),
};
s.hotreload_body::<Ctx>(new)?;
Some(s)
}
fn extend(&mut self, other: Self) {
self.templates.extend(other.templates);
}
/// Walk the dynamic contexts and do our best to find hot reload-able changes between the two
/// sets of dynamic nodes/attributes. If there's a change we can't hot reload, we'll return None
///
/// Otherwise, we pump out the list of templates that need to be updated. The templates will be
/// re-ordered such that the node paths will be adjusted to match the new template for every
/// existing dynamic node.
///
/// ```ignore
/// old:
/// [[0], [1], [2]]
/// rsx! {
/// "{one}"
/// "{two}"
/// "{three}"
/// }
///
/// new:
/// [[0], [2], [1, 1]]
/// rsx! {
/// "{one}"
/// div { "{three}" }
/// "{two}"
/// }
/// ```
///
/// Generally we can't hot reload a node if:
/// - We add or modify a new rust expression
/// - Adding a new formatted segment we haven't seen before
/// - Adding a new dynamic node (loop, fragment, if chain, etc)
/// - We add a new component field
/// - We remove a component field
/// - We change the type of a component field
///
/// If a dynamic node is removed, we don't necessarily need to kill hot reload - just unmounting it should be enough
/// If the dynamic node is re-added, we want to be able to find it again.
///
/// This encourages the hot reloader to hot onto DynamicContexts directly instead of the CallBody since
/// you can preserve more information about the nodes as they've changed over time.
fn hotreload_body<Ctx: HotReloadingContext>(&mut self, new: &TemplateBody) -> Option<()> {
// Quickly run through dynamic attributes first attempting to invalidate them
// Move over old IDs onto the new template
self.hotreload_attributes::<Ctx>(new)?;
let new_dynamic_attributes = std::mem::take(&mut self.dynamic_attributes);
// Now we can run through the dynamic nodes and see if we can hot reload them
// Move over old IDs onto the new template
self.hotreload_dynamic_nodes::<Ctx>(new)?;
let new_dynamic_nodes = std::mem::take(&mut self.dynamic_nodes);
let literal_component_properties = std::mem::take(&mut self.literal_component_properties);
let key = self.hot_reload_key(new)?;
let roots: Vec<_> = new
.roots
.iter()
.map(|node| to_template_node::<Ctx>(node))
.collect();
let roots: &[dioxus_core::TemplateNode] = intern(&*roots);
let template = HotReloadedTemplate::new(
key,
new_dynamic_nodes,
new_dynamic_attributes,
literal_component_properties,
roots,
);
self.templates
.insert(self.full_rebuild_state.root_index.get(), template);
Some(())
}
fn hot_reload_key(&mut self, new: &TemplateBody) -> Option<Option<FmtedSegments>> {
match new.implicit_key() {
Some(AttributeValue::AttrLiteral(HotLiteral::Fmted(value))) => Some(Some(
self.full_rebuild_state
.hot_reload_formatted_segments(value)?,
)),
None => Some(None),
_ => None,
}
}
fn hotreload_dynamic_nodes<Ctx: HotReloadingContext>(
&mut self,
new: &TemplateBody,
) -> Option<()> {
for new_node in new.dynamic_nodes() {
self.hot_reload_node::<Ctx>(new_node)?
}
Some(())
}
fn hot_reload_node<Ctx: HotReloadingContext>(&mut self, node: &BodyNode) -> Option<()> {
match node {
BodyNode::Text(text) => self.hotreload_text_node(text),
BodyNode::Component(component) => self.hotreload_component::<Ctx>(component),
BodyNode::ForLoop(forloop) => self.hotreload_for_loop::<Ctx>(forloop),
BodyNode::IfChain(ifchain) => self.hotreload_if_chain::<Ctx>(ifchain),
BodyNode::RawExpr(expr) => self.hotreload_raw_expr(expr),
BodyNode::Element(_) => Some(()),
}
}
fn hotreload_raw_expr(&mut self, expr: &ExprNode) -> Option<()> {
// Try to find the raw expr in the last build
let expr_index = self
.full_rebuild_state
.dynamic_nodes
.position(|node| match &node {
BodyNode::RawExpr(raw_expr) => raw_expr.expr == expr.expr,
_ => false,
})?;
// If we find it, push it as a dynamic node
self.dynamic_nodes
.push(HotReloadDynamicNode::Dynamic(expr_index));
Some(())
}
fn hotreload_for_loop<Ctx>(&mut self, forloop: &ForLoop) -> Option<()>
where
Ctx: HotReloadingContext,
{
// Find all for loops that have the same pattern and expression
let candidate_for_loops = self
.full_rebuild_state
.dynamic_nodes
.inner
.iter()
.enumerate()
.filter_map(|(index, node)| {
if let BodyNode::ForLoop(for_loop) = &node.inner {
if for_loop.pat == forloop.pat && for_loop.expr == forloop.expr {
return Some((index, for_loop));
}
}
None
})
.collect::<Vec<_>>();
// Then find the one that has the least wasted dynamic items when hot reloading the body
let (index, best_call_body) = self.diff_best_call_body::<Ctx>(
candidate_for_loops
.iter()
.map(|(_, for_loop)| &for_loop.body),
&forloop.body,
)?;
// Push the new for loop as a dynamic node
self.dynamic_nodes
.push(HotReloadDynamicNode::Dynamic(candidate_for_loops[index].0));
self.extend(best_call_body);
Some(())
}
fn hotreload_text_node(&mut self, text_node: &TextNode) -> Option<()> {
// If it is static, it is already included in the template and we don't need to do anything
if text_node.input.is_static() {
return Some(());
}
// Otherwise, hot reload the formatted segments and push that as a dynamic node
let formatted_segments = self
.full_rebuild_state
.hot_reload_formatted_segments(&text_node.input)?;
self.dynamic_nodes
.push(HotReloadDynamicNode::Formatted(formatted_segments));
Some(())
}
/// Find the call body that minimizes the number of wasted dynamic items
///
/// Returns the index of the best call body and the state of the best call body
fn diff_best_call_body<'a, Ctx>(
&self,
bodies: impl Iterator<Item = &'a TemplateBody>,
new_call_body: &TemplateBody,
) -> Option<(usize, Self)>
where
Ctx: HotReloadingContext,
{
let mut best_score = usize::MAX;
let mut best_output = None;
for (index, body) in bodies.enumerate() {
// Skip templates we've already hotreloaded
if self.templates.contains_key(&body.template_idx.get()) {
continue;
}
if let Some(state) =
Self::new::<Ctx>(body, new_call_body, self.full_rebuild_state.name.clone())
{
let score = state.full_rebuild_state.unused_dynamic_items();
if score < best_score {
best_score = score;
best_output = Some((index, state));
}
}
}
best_output
}
fn hotreload_component<Ctx>(&mut self, component: &Component) -> Option<()>
where
Ctx: HotReloadingContext,
{
// First we need to find the component that matches the best in the last build
// We try each build and choose the option that wastes the least dynamic items
let components_with_matching_attributes: Vec<_> = self
.full_rebuild_state
.dynamic_nodes
.inner
.iter()
.enumerate()
.filter_map(|(index, node)| {
if let BodyNode::Component(comp) = &node.inner {
return Some((
index,
comp,
self.hotreload_component_fields(comp, component)?,
));
}
None
})
.collect();
let possible_bodies = components_with_matching_attributes
.iter()
.map(|(_, comp, _)| &comp.children);
let (index, new_body) =
self.diff_best_call_body::<Ctx>(possible_bodies, &component.children)?;
let (index, _, literal_component_properties) = &components_with_matching_attributes[index];
let index = *index;
self.full_rebuild_state.dynamic_nodes.inner[index]
.used
.set(true);
self.literal_component_properties
.extend(literal_component_properties.iter().cloned());
self.extend(new_body);
// Push the new component as a dynamic node
self.dynamic_nodes
.push(HotReloadDynamicNode::Dynamic(index));
Some(())
}
fn hotreload_component_fields(
&self,
old_component: &Component,
new_component: &Component,
) -> Option<Vec<HotReloadLiteral>> {
// First check if the component is the same
if new_component.name != old_component.name {
return None;
}
// Then check if the fields are the same
let new_non_key_fields: Vec<_> = new_component.component_props().collect();
let old_non_key_fields: Vec<_> = old_component.component_props().collect();
if new_non_key_fields.len() != old_non_key_fields.len() {
return None;
}
let mut new_fields = new_non_key_fields.clone();
new_fields.sort_by_key(|attribute| attribute.name.to_string());
let mut old_fields = old_non_key_fields.iter().enumerate().collect::<Vec<_>>();
old_fields.sort_by_key(|(_, attribute)| attribute.name.to_string());
// The literal component properties for the component in same the order as the original component property literals
let mut literal_component_properties = vec![None; old_fields.len()];
for (new_field, (index, old_field)) in new_fields.iter().zip(old_fields.iter()) {
// Verify the names match
if new_field.name != old_field.name {
return None;
}
// Verify the values match
match (&new_field.value, &old_field.value) {
// If the values are both literals, we can try to hotreload them
(
AttributeValue::AttrLiteral(new_value),
AttributeValue::AttrLiteral(old_value),
) => {
// Make sure that the types are the same
if std::mem::discriminant(new_value) != std::mem::discriminant(old_value) {
return None;
}
let literal = self.full_rebuild_state.hotreload_hot_literal(new_value)?;
literal_component_properties[*index] = Some(literal);
}
_ => {
if new_field.value != old_field.value {
return None;
}
}
}
}
Some(literal_component_properties.into_iter().flatten().collect())
}
/// Hot reload an if chain
fn hotreload_if_chain<Ctx: HotReloadingContext>(
&mut self,
new_if_chain: &IfChain,
) -> Option<()> {
let mut best_if_chain = None;
let mut best_score = usize::MAX;
let if_chains = self
.full_rebuild_state
.dynamic_nodes
.inner
.iter()
.enumerate()
.filter_map(|(index, node)| {
if let BodyNode::IfChain(if_chain) = &node.inner {
return Some((index, if_chain));
}
None
});
// Find the if chain that matches all of the conditions and wastes the least dynamic items
for (index, old_if_chain) in if_chains {
let Some(chain_templates) = Self::diff_if_chains::<Ctx>(
old_if_chain,
new_if_chain,
self.full_rebuild_state.name.clone(),
) else {
continue;
};
let score = chain_templates
.iter()
.map(|t| t.full_rebuild_state.unused_dynamic_items())
.sum();
if score < best_score {
best_score = score;
best_if_chain = Some((index, chain_templates));
}
}
// If we found a hot reloadable if chain, hotreload it
let (index, chain_templates) = best_if_chain?;
// Mark the if chain as used
self.full_rebuild_state.dynamic_nodes.inner[index]
.used
.set(true);
// Merge the hot reload changes into the current state
for template in chain_templates {
self.extend(template);
}
// Push the new if chain as a dynamic node
self.dynamic_nodes
.push(HotReloadDynamicNode::Dynamic(index));
Some(())
}
/// Hot reload an if chain
fn diff_if_chains<Ctx: HotReloadingContext>(
old_if_chain: &IfChain,
new_if_chain: &IfChain,
name: String,
) -> Option<Vec<Self>> {
// Go through each part of the if chain and find the best match
let mut old_chain = old_if_chain;
let mut new_chain = new_if_chain;
let mut chain_templates = Vec::new();
loop {
// Make sure the conditions are the same
if old_chain.cond != new_chain.cond {
return None;
}
// If the branches are the same, we can hotreload them
let hot_reload =
Self::new::<Ctx>(&old_chain.then_branch, &new_chain.then_branch, name.clone())?;
chain_templates.push(hot_reload);
// Make sure the if else branches match
match (
old_chain.else_if_branch.as_ref(),
new_chain.else_if_branch.as_ref(),
) {
(Some(old), Some(new)) => {
old_chain = old;
new_chain = new;
}
(None, None) => {
break;
}
_ => return None,
}
}
// Make sure the else branches match
match (&old_chain.else_branch, &new_chain.else_branch) {
(Some(old), Some(new)) => {
let template = Self::new::<Ctx>(old, new, name.clone())?;
chain_templates.push(template);
}
(None, None) => {}
_ => return None,
}
Some(chain_templates)
}
/// Take a new template body and return the attributes that can be hot reloaded from the last build
///
/// IE if we shuffle attributes, remove attributes or add new attributes with the same dynamic segments, around we should be able to hot reload them.
///
/// ```rust, ignore
/// rsx! {
/// div { id: "{id}", class: "{class}", width, "Hi" }
/// }
///
/// rsx! {
/// div { width, class: "{class}", id: "{id} and {class}", "Hi" }
/// }
/// ```
fn hotreload_attributes<Ctx: HotReloadingContext>(&mut self, new: &TemplateBody) -> Option<()> {
// Walk through each attribute and create a new HotReloadAttribute for each one
for new_attr in new.dynamic_attributes() {
// While we're here, if it's a literal and not a perfect score, it's a mismatch and we need to
// hotreload the literal
self.hotreload_attribute::<Ctx>(new_attr)?;
}
Some(())
}
/// Try to hot reload an attribute and return the new HotReloadAttribute
fn hotreload_attribute<Ctx: HotReloadingContext>(
&mut self,
attribute: &Attribute,
) -> Option<()> {
let (tag, namespace) = html_tag_and_namespace::<Ctx>(attribute);
// If the attribute is a spread, try to grab it from the last build
// If it wasn't in the last build with the same name, we can't hot reload it
if let AttributeName::Spread(_) = &attribute.name {
let hot_reload_attribute = self
.full_rebuild_state
.dynamic_attributes
.position(|a| a.name == attribute.name && a.value == attribute.value)?;
self.dynamic_attributes
.push(HotReloadDynamicAttribute::Dynamic(hot_reload_attribute));
return Some(());
}
// Otherwise the attribute is named, try to hot reload the value
let value = match &attribute.value {
// If the attribute is a literal, we can generally hot reload it if the formatted segments exist in the last build
AttributeValue::AttrLiteral(literal) => {
// If it is static, it is already included in the template and we don't need to do anything
if literal.is_static() {
return Some(());
}
// Otherwise, hot reload the literal and push that as a dynamic attribute
let hot_reload_literal = self.full_rebuild_state.hotreload_hot_literal(literal)?;
HotReloadAttributeValue::Literal(hot_reload_literal)
}
// If it isn't a literal, try to find an exact match for the attribute value from the last build
_ => {
let value_index = self.full_rebuild_state.dynamic_attributes.position(|a| {
!matches!(a.name, AttributeName::Spread(_)) && a.value == attribute.value
})?;
HotReloadAttributeValue::Dynamic(value_index)
}
};
self.dynamic_attributes
.push(HotReloadDynamicAttribute::Named(NamedAttribute::new(
tag, namespace, value,
)));
Some(())
}
}
+111
View File
@@ -0,0 +1,111 @@
use dioxus_core::TemplateNode;
use dioxus_core_types::HotReloadingContext;
use dioxus_rsx::*;
use internment::Intern;
use std::hash::Hash;
// interns a object into a static object, reusing the value if it already exists
pub(crate) fn intern<T: Eq + Hash + Send + Sync + ?Sized + 'static>(
s: impl Into<Intern<T>>,
) -> &'static T {
s.into().as_ref()
}
pub(crate) fn html_tag_and_namespace<Ctx: HotReloadingContext>(
attr: &Attribute,
) -> (&'static str, Option<&'static str>) {
let attribute_name_rust = attr.name.to_string();
let element_name = attr.el_name.as_ref().unwrap();
let rust_name = match element_name {
ElementName::Ident(i) => i.to_string(),
// If this is a web component, just use the name of the elements instead of mapping the attribute
// through the hot reloading context
ElementName::Custom(_) => return (intern(attribute_name_rust.as_str()), None),
};
Ctx::map_attribute(&rust_name, &attribute_name_rust)
.unwrap_or((intern(attribute_name_rust.as_str()), None))
}
pub fn to_template_attribute<Ctx: HotReloadingContext>(
attr: &Attribute,
) -> dioxus_core::TemplateAttribute {
use dioxus_core::TemplateAttribute;
// If it's a dynamic node, just return it
// 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
if !attr.is_static_str_literal() {
let id = attr.dyn_idx.get();
return TemplateAttribute::Dynamic { id };
}
// Otherwise it's a static node and we can build it
let (_, value) = attr.as_static_str_literal().unwrap();
let (name, namespace) = html_tag_and_namespace::<Ctx>(attr);
TemplateAttribute::Static {
name,
namespace,
value: intern(value.to_static().unwrap().as_str()),
}
}
/// Convert this BodyNode into a TemplateNode.
///
/// dioxus-core uses this to understand templates at compiletime
pub fn to_template_node<Ctx: HotReloadingContext>(node: &BodyNode) -> dioxus_core::TemplateNode {
use dioxus_core::TemplateNode;
match node {
BodyNode::Element(el) => {
let rust_name = el.name.to_string();
let (tag, namespace) =
Ctx::map_element(&rust_name).unwrap_or((intern(rust_name.as_str()), None));
TemplateNode::Element {
tag,
namespace,
children: intern(
el.children
.iter()
.map(|c| to_template_node::<Ctx>(c))
.collect::<Vec<_>>(),
),
attrs: intern(
el.merged_attributes
.iter()
.map(|attr| to_template_attribute::<Ctx>(attr))
.collect::<Vec<_>>(),
),
}
}
BodyNode::Text(text) => text_to_template_node(text),
BodyNode::RawExpr(exp) => TemplateNode::Dynamic {
id: exp.dyn_idx.get(),
},
BodyNode::Component(comp) => TemplateNode::Dynamic {
id: comp.dyn_idx.get(),
},
BodyNode::ForLoop(floop) => TemplateNode::Dynamic {
id: floop.dyn_idx.get(),
},
BodyNode::IfChain(chain) => TemplateNode::Dynamic {
id: chain.dyn_idx.get(),
},
}
}
pub fn text_to_template_node(node: &TextNode) -> TemplateNode {
match node.input.to_static() {
Some(text) => TemplateNode::Text {
text: intern(text.as_str()),
},
None => TemplateNode::Dynamic {
id: node.dyn_idx.get(),
},
}
}
@@ -0,0 +1,157 @@
use dioxus_core::internal::{FmtSegment, FmtedSegments, HotReloadLiteral};
use dioxus_rsx::*;
use std::cell::Cell;
/// A pool of items we can grab from during hot reloading.
/// We have three different pools we can pull from:
/// - Dynamic text segments (eg: "{class}")
/// - Dynamic nodes (eg: {children})
/// - Dynamic attributes (eg: ..spread )
///
/// As we try to create a new hot reloaded template, we will pull from these pools to create the new template. We mark
/// each item as used the first time we use it in the new template. Once the new template if fully created, we can tally
/// up how many items are unused to determine how well the new template matches the old template.
///
/// The template that matches best will leave the least unused items in the pool.
#[derive(Debug, PartialEq, Clone)]
pub(crate) struct BakedPool<T> {
pub inner: Vec<BakedItem<T>>,
}
impl<T> BakedPool<T> {
/// Create a new baked pool from an iterator of items
fn new(inner: impl IntoIterator<Item = T>) -> Self {
Self {
inner: inner.into_iter().map(BakedItem::new).collect(),
}
}
/// Find the first item in the pool that matches the condition and mark it as used
pub fn position(&self, condition: impl Fn(&T) -> bool) -> Option<usize> {
for (idx, baked_item) in self.inner.iter().enumerate() {
if condition(&baked_item.inner) {
baked_item.used.set(true);
return Some(idx);
}
}
None
}
/// Find the number of unused items in the pool
fn unused_dynamic_items(&self) -> usize {
self.inner
.iter()
.filter(|baked_item| !baked_item.used.get())
.count()
}
}
/// A single item in the baked item pool. We keep track if which items are used for scoring how well two templates match.
#[derive(Debug, PartialEq, Clone)]
pub(crate) struct BakedItem<T> {
pub inner: T,
pub used: Cell<bool>,
}
impl<T> BakedItem<T> {
fn new(inner: T) -> Self {
Self {
inner,
used: Cell::new(false),
}
}
}
/// The state of the last full rebuild.
/// This object contains the pool of compiled dynamic segments we can pull from for hot reloading
#[derive(Debug, PartialEq, Clone)]
pub(crate) struct LastBuildState {
/// The formatted segments that were used in the last build. Eg: "{class}", "{id}"
///
/// We are free to use each of these segments many times in the same build.
/// We just clone the result (assuming display + debug have no side effects)
pub dynamic_text_segments: BakedPool<FormattedSegment>,
/// The dynamic nodes that were used in the last build. Eg: div { {children} }
///
/// We are also free to clone these nodes many times in the same build.
pub dynamic_nodes: BakedPool<BodyNode>,
/// The attributes that were used in the last build. Eg: div { class: "{class}" }
///
/// We are also free to clone these nodes many times in the same build.
pub dynamic_attributes: BakedPool<Attribute>,
/// The component literal properties we can hot reload from the last build. Eg: Component { class: "{class}" }
///
/// In the new build, we must assign each of these a value even if we no longer use the component.
/// The type must be the same as the last time we compiled the property
pub component_properties: Vec<HotLiteral>,
/// The root indexes of the last build
pub root_index: DynIdx,
/// The name of the original template
pub name: String,
}
impl LastBuildState {
/// Create a new LastBuildState from the given [`TemplateBody`]
pub fn new(body: &TemplateBody, name: String) -> Self {
let dynamic_text_segments = body.dynamic_text_segments.iter().cloned();
let dynamic_nodes = body.dynamic_nodes().cloned();
let dynamic_attributes = body.dynamic_attributes().cloned();
let component_properties = body.literal_component_properties().cloned().collect();
Self {
dynamic_text_segments: BakedPool::new(dynamic_text_segments),
dynamic_nodes: BakedPool::new(dynamic_nodes),
dynamic_attributes: BakedPool::new(dynamic_attributes),
component_properties,
root_index: body.template_idx.clone(),
name,
}
}
/// Return the number of unused dynamic items in the pool
pub fn unused_dynamic_items(&self) -> usize {
self.dynamic_text_segments.unused_dynamic_items()
+ self.dynamic_nodes.unused_dynamic_items()
+ self.dynamic_attributes.unused_dynamic_items()
}
/// Hot reload a hot literal
pub fn hotreload_hot_literal(&self, hot_literal: &HotLiteral) -> Option<HotReloadLiteral> {
match hot_literal {
// If the literal is a formatted segment, map the segments to the new formatted segments
HotLiteral::Fmted(segments) => {
let new_segments = self.hot_reload_formatted_segments(segments)?;
Some(HotReloadLiteral::Fmted(new_segments))
}
// Otherwise just pass the literal through unchanged
HotLiteral::Bool(b) => Some(HotReloadLiteral::Bool(b.value())),
HotLiteral::Float(f) => Some(HotReloadLiteral::Float(f.base10_parse().ok()?)),
HotLiteral::Int(i) => Some(HotReloadLiteral::Int(i.base10_parse().ok()?)),
}
}
pub fn hot_reload_formatted_segments(
&self,
new: &HotReloadFormattedSegment,
) -> Option<FmtedSegments> {
// Go through each dynamic segment and look for a match in the formatted segments pool.
// If we find a match, we can hot reload the segment otherwise we need to do a full rebuild
let mut segments = Vec::new();
for segment in &new.segments {
match segment {
// If it is a literal, we can always hot reload it. Just add it to the segments
Segment::Literal(value) => {
segments.push(FmtSegment::Literal {
value: Box::leak(value.clone().into_boxed_str()),
});
} // If it is a dynamic segment, we need to check if it exists in the formatted segments pool
Segment::Formatted(formatted) => {
let index = self.dynamic_text_segments.position(|s| s == formatted)?;
segments.push(FmtSegment::Dynamic { id: index });
}
}
}
Some(FmtedSegments::new(segments))
}
}
+9
View File
@@ -0,0 +1,9 @@
mod collect;
pub use collect::*;
mod diff;
pub use diff::*;
mod last_build_state;
mod extensions;
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,31 @@
use dioxus_rsx_hotreload::diff_rsx;
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!(diff_rsx(&new, &old).is_some());
}
};
}
fn load_files(old: &str, new: &str) -> (File, File) {
let old = syn::parse_file(old).unwrap();
let new = syn::parse_file(new).unwrap();
(old, new)
}
assert_rsx_changed![combo];
assert_rsx_changed![expr];
assert_rsx_changed![for_];
assert_rsx_changed![if_];
assert_rsx_changed![let_];
assert_rsx_changed![nested];
@@ -0,0 +1,68 @@
// This test is used by playwright configured in the root of the repo
use dioxus::prelude::*;
fn app() -> Element {
let mut num = use_signal(|| 0);
let mut eval_result = use_signal(String::new);
let a = 123;
rsx! {
div {
"hello axum! {num}"
button { class: "increment-button", onclick: move |_| num += 1, "Increment" }
}
svg {
circle {
cx: 50,
cy: 50,
r: 40,
stroke: "green",
fill: "yellow"
}
}
div {
class: "raw-attribute-div",
"raw-attribute": "raw-attribute-value"
}
div { class: "hidden-attribute-div", hidden: true }
div {
class: "dangerous-inner-html-div",
dangerous_inner_html: "<p>hello dangerous inner html</p>"
}
input { value: "hello input" }
div { class: "style-div", color: "red", "colored text" }
div { class: "style-div", color: "red", "colored text" }
button {
class: "eval-button",
onclick: move |_| async move {
let mut eval = eval(
r#"
window.document.title = 'Hello from Dioxus Eval!';
dioxus.send("returned eval value");
"#,
);
let result = eval.recv().await;
if let Ok(serde_json::Value::String(string)) = result {
eval_result.set(string);
}
},
"Eval!!!!"
"Eval!!!!"
"Eval!!!!!"
"Eval!!!!"
"Eval!!!!"
"Eval!!!!"
}
div { class: "eval-result", "{eval_result}" }
}
}
fn main() {
// tracing_wasm::set_as_global_default_with_config(
// tracing_wasm::WASMLayerConfigBuilder::default()
// .set_max_level(tracing::Level::TRACE)
// .build(),
// );
launch(app);
}
@@ -0,0 +1,57 @@
// This test is used by playwright configured in the root of the repo
use dioxus::prelude::*;
fn app() -> Element {
let mut num = use_signal(|| 0);
let mut eval_result = use_signal(String::new);
let a = 123;
rsx! {
div {
"hello axum! {num}"
button { class: "increment-button", onclick: move |_| num += 1, "Increment" }
}
svg { circle { cx: 50, cy: 50, r: 40, stroke: "green", fill: "yellow" } }
div { class: "raw-attribute-div", "raw-attribute": "raw-attribute-value" }
div { class: "hidden-attribute-div", hidden: true }
div {
class: "dangerous-inner-html-div",
dangerous_inner_html: "<p>hello dangerous inner html</p>"
}
input { value: "hello input" }
div { class: "style-div", color: "red", "colored text" }
div { class: "style-div", color: "red", "colored text" }
button {
class: "eval-button",
onclick: move |_| async move {
let mut eval = eval(
r#"
window.document.title = 'Hello from Dioxus Eval!';
dioxus.send("returned eval value");
"#,
);
let result = eval.recv().await;
if let Ok(serde_json::Value::String(string)) = result {
eval_result.set(string);
}
},
"Eval!!!!"
"Eval!!!!"
"Eval!!!!!"
"Eval!!!!"
"Eval!!!!"
}
div { class: "eval-result", "{eval_result}" }
}
}
fn main() {
// tracing_wasm::set_as_global_default_with_config(
// tracing_wasm::WASMLayerConfigBuilder::default()
// .set_max_level(tracing::Level::TRACE)
// .build(),
// );
launch(app);
}
@@ -0,0 +1,17 @@
use dioxus::prelude::*;
pub fn CoolChild() -> Element {
let head_ = rsx! {
div {
div { "asasddasdasd" }
div { "asasdd1asaassdd23asasddasd" }
div { "aasdsdsaasdsddasd" }
}
};
rsx! {
div {
{head_}
}
}
}
@@ -0,0 +1,17 @@
use dioxus::prelude::*;
pub fn CoolChild() -> Element {
let head_ = rsx! {
div {
div { "asasddasdasd" }
div { "asasdd1asaassdd23asasddasd" }
// div { "aasdsdsaasdsddasd" }
}
};
rsx! {
div {
{head_}
}
}
}
@@ -0,0 +1,9 @@
use dioxus::prelude::*;
pub fn CoolChild() -> Element {
rsx! {
for items in vec![1, 2, 3] {
div { "asasddasdasd" }
}
}
}
@@ -0,0 +1,10 @@
use dioxus::prelude::*;
pub fn CoolChild() -> Element {
rsx! {
for items in vec![1, 2, 3] {
div { "123" }
div { "asasddasdasd" }
}
}
}
@@ -0,0 +1,10 @@
use dioxus::prelude::*;
pub fn CoolChild() -> Element {
rsx! {
if cond() {
div { "123" }
div { "asasddasdasd" }
}
}
}
@@ -0,0 +1,9 @@
use dioxus::prelude::*;
pub fn CoolChild() -> Element {
rsx! {
if cond() {
div { "asasddasdasd" }
}
}
}
@@ -0,0 +1,12 @@
use dioxus::prelude::*;
pub fn CoolChild() -> Element {
let head_ = rsx! {
div {
div { "asasddasdasd" }
div { "asasdd1asaassdd23asasddasd" }
}
};
head_
}
@@ -0,0 +1,12 @@
use dioxus::prelude::*;
pub fn CoolChild() -> Element {
let head_ = rsx! {
div {
div { "asasddasdasd" }
div { "asasdd1asaassdd23asasddasdasd" }
}
};
head_
}
@@ -0,0 +1,10 @@
use dioxus::prelude::*;
pub fn CoolChild() -> Element {
rsx! {
ForLoop {
div { "123" }
div { "asasddasdasd" }
}
}
}
@@ -0,0 +1,9 @@
use dioxus::prelude::*;
pub fn CoolChild() -> Element {
rsx! {
ForLoop {
div { "asasddasdasd" }
}
}
}