Improve compile times by splitting out rsx-hotreload from rsx (#2971)
* fix imports, migrate over rsx-hotreload
This commit is contained in:
1 parent
b67122dd2a
commit
463e67cd12
41 files changed
+188
-190
No files matched your search
@@ -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 }
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user