From 34bdcd15cf7dc03fb4622ea1c8684c23ea9f1586 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 30 Jul 2024 19:16:27 +0200 Subject: [PATCH] Switch to a pool of dynamic values for hot reloading (#2705) * create the dynamic value pool * assign ids to dynamic formatted segments * separate the rendering and literal pools * rsx output compiles again * more examples compiling with new rsx expansion * update template body explanation * all workspace examples compile * fix formatted segments in keys * start hot reload diffing * fix component literal hot reloading * start integrate new hot reloading with the CLI * simple hot reloads working * Fix hot reloading blocks with components * implement hot reloading for if chains * Fix hot reloading after a template requires a full rebuild * Fix hot reloading any attribute values * remove unsafe from hot reload utils * Fix hot reloading empty rsx * add more hot reloading tests * reorganize hot reload module * fix web hydration * fix empty rsx nodes in autoformatting * fix tests * remove path sorting logic from core * make template names more consistent in debug mode * fix quote_as_hot_reload_literal for explicitly typed literals * fix can_be_shorthand for string literals * fix formatted single dynamic expression * Fix usize component properties and playwright tests * remove default implementation for TemplateBody * add a bunch more comments for diffing, scoring and why this scoring system is optimal --- .../cli/src/serve/hot_reloading_file_map.rs | 79 +-- packages/core/src/diff/iterator.rs | 2 +- packages/core/src/diff/mod.rs | 6 +- packages/core/src/diff/node.rs | 70 +- packages/core/src/hotreload_utils.rs | 436 +++++++++++- packages/core/src/lib.rs | 5 +- packages/core/src/nodes.rs | 131 +--- packages/core/src/virtual_dom.rs | 68 +- packages/core/tests/attributes_pass.rs | 2 +- packages/core/tests/hotreload.rs | 38 - packages/hot-reload/src/client.rs | 43 +- packages/hot-reload/src/lib.rs | 4 +- packages/html/src/document/head.rs | 14 +- packages/rsx-rosetta/src/lib.rs | 1 + packages/rsx/src/assign_dyn_ids.rs | 138 ++++ packages/rsx/src/attribute.rs | 71 +- packages/rsx/src/component.rs | 75 +- packages/rsx/src/element.rs | 17 +- .../{hot_reload_diff.rs => collect.rs} | 0 .../{hot_reloading_context.rs => context.rs} | 0 packages/rsx/src/hot_reload/diff.rs | 651 ++++++++++++++++++ .../rsx/src/hot_reload/last_build_state.rs | 157 +++++ packages/rsx/src/hot_reload/mod.rs | 16 +- packages/rsx/src/hotreload.rs | 429 ------------ packages/rsx/src/ifchain.rs | 10 - packages/rsx/src/ifmt.rs | 79 +-- packages/rsx/src/lib.rs | 9 +- packages/rsx/src/literal.rs | 256 +++---- packages/rsx/src/node.rs | 8 +- packages/rsx/src/reload_stack.rs | 113 --- packages/rsx/src/rsx_call.rs | 38 +- packages/rsx/src/scoring.rs | 330 --------- packages/rsx/src/template_body.rs | 355 +++++----- packages/rsx/src/text_node.rs | 18 +- packages/rsx/src/util.rs | 2 +- packages/rsx/tests/hotreload_pattern.rs | 503 ++++++++++---- packages/rsx/tests/parsing.rs | 5 +- packages/ssr/src/cache.rs | 2 +- packages/ssr/src/renderer.rs | 2 +- packages/web/src/hydration/hydrate.rs | 2 +- 40 files changed, 2288 insertions(+), 1897 deletions(-) delete mode 100644 packages/core/tests/hotreload.rs create mode 100644 packages/rsx/src/assign_dyn_ids.rs rename packages/rsx/src/hot_reload/{hot_reload_diff.rs => collect.rs} (100%) rename packages/rsx/src/hot_reload/{hot_reloading_context.rs => context.rs} (100%) create mode 100644 packages/rsx/src/hot_reload/diff.rs create mode 100644 packages/rsx/src/hot_reload/last_build_state.rs delete mode 100644 packages/rsx/src/hotreload.rs delete mode 100644 packages/rsx/src/reload_stack.rs delete mode 100644 packages/rsx/src/scoring.rs diff --git a/packages/cli/src/serve/hot_reloading_file_map.rs b/packages/cli/src/serve/hot_reloading_file_map.rs index 1cec478b4..d34b21400 100644 --- a/packages/cli/src/serve/hot_reloading_file_map.rs +++ b/packages/cli/src/serve/hot_reloading_file_map.rs @@ -1,7 +1,7 @@ -use dioxus_core::{internal::HotReloadLiteral, Template}; +use dioxus_core::internal::{HotReloadTemplateWithLocation, HotReloadedTemplate}; use dioxus_rsx::{ hot_reload::{diff_rsx, ChangedRsx}, - CallBody, HotReloadedTemplate, HotReloadingContext, + CallBody, HotReloadingContext, }; use krates::cm::MetadataCommand; use krates::Cmd; @@ -18,8 +18,6 @@ pub struct FileMap { pub errors: Vec, pub in_workspace: HashMap>, - - pub changed_lits: HashMap, } /// A cached file that has been parsed @@ -27,7 +25,7 @@ pub struct FileMap { /// We store the templates found in this file pub struct CachedSynFile { pub raw: String, - pub templates: HashMap<&'static str, Template>, + pub templates: HashMap, } impl FileMap { @@ -56,7 +54,6 @@ impl FileMap { map, errors, in_workspace: HashMap::new(), - changed_lits: HashMap::new(), }; map.load_assets::(crate_dir.as_path()); @@ -74,12 +71,23 @@ impl FileMap { } } + /// Insert a file into the map and force a full rebuild + fn full_rebuild(&mut self, file_path: PathBuf, src: String) -> HotreloadError { + let cached_file = CachedSynFile { + raw: src.clone(), + templates: HashMap::new(), + }; + + self.map.insert(file_path, cached_file); + HotreloadError::Notreloadable + } + /// Try to update the rsx in a file pub fn update_rsx( &mut self, file_path: &Path, crate_dir: &Path, - ) -> Result, HotreloadError> { + ) -> Result, HotreloadError> { let src = std::fs::read_to_string(file_path)?; // If we can't parse the contents we want to pass it off to the build system to tell the user that there's a syntax error @@ -116,13 +124,7 @@ impl FileMap { // If the changes were some code, we should insert the file into the map and rebuild // todo: not sure we even need to put the cached file into the map, but whatever None => { - let cached_file = CachedSynFile { - raw: src.clone(), - templates: HashMap::new(), - }; - - self.map.insert(file_path.to_path_buf(), cached_file); - return Err(HotreloadError::Notreloadable); + return Err(self.full_rebuild(file_path.to_path_buf(), src)); } }; @@ -150,47 +152,48 @@ impl FileMap { continue; }; - // We leak the template since templates are a compiletime value - // This is not ideal, but also not a huge deal for hot reloading - // TODO: we could consider arena allocating the templates and dropping them when the connection is closed - let leaked_location = Box::leak(template_location(old_start, file).into_boxed_str()); + let template_location = template_location(old_start, file); // Returns a list of templates that are hotreloadable - let hotreload_result = dioxus_rsx::hotreload::HotReloadedTemplate::new::( - &old_call_body, - &new_call_body, - leaked_location, - self.changed_lits.clone(), + let hotreload_result = dioxus_rsx::hot_reload::HotReloadResult::new::( + &old_call_body.body, + &new_call_body.body, + template_location.clone(), ); // if the template is not hotreloadable, we need to do a full rebuild let Some(mut results) = hotreload_result else { - return Err(HotreloadError::Notreloadable); + return Err(self.full_rebuild(file_path.to_path_buf(), src)); }; - // self.changed_lits - // .extend(std::mem::take(&mut results.changed_lits)); - // Be careful to not send the bad templates - results.templates.retain(|template| { + results.templates.retain(|idx, template| { // dioxus cannot handle empty templates... if template.roots.is_empty() { return false; } + let template_location = format_template_name(&template_location, *idx); - // if the template is the same, don't send it - if old_cached.templates.get(template.name) == Some(template) { + // if the template is the same, don't send its + if old_cached.templates.get(&template_location) == Some(&*template) { return false; }; // Update the most recent idea of the template // This lets us know if the template has changed so we don't need to send it - old_cached.templates.insert(template.name, *template); + old_cached + .templates + .insert(template_location, template.clone()); true }); - out_templates.push(results); + out_templates.extend(results.templates.into_iter().map(|(idx, template)| { + HotReloadTemplateWithLocation { + location: format_template_name(&template_location, idx), + template, + } + })); } Ok(out_templates) @@ -228,13 +231,11 @@ pub fn template_location(old_start: proc_macro2::LineColumn, file: &Path) -> Str .collect::>() .join("/"); - path - + ":" - + line.to_string().as_str() - + ":" - + column.to_string().as_str() - // the byte index doesn't matter, but dioxus needs it - + ":0" + path + ":" + line.to_string().as_str() + ":" + column.to_string().as_str() +} + +pub fn format_template_name(name: &str, index: usize) -> String { + format!("{}:{}", name, index) } struct FileMapSearchResult { diff --git a/packages/core/src/diff/iterator.rs b/packages/core/src/diff/iterator.rs index 497526c93..d51e5f828 100644 --- a/packages/core/src/diff/iterator.rs +++ b/packages/core/src/diff/iterator.rs @@ -467,7 +467,7 @@ impl VNode { dom: &VirtualDom, to: &mut impl WriteMutations, ) -> usize { - let template = self.template.get(); + let template = self.template; let mount = dom.mounts.get(self.mount.get().0).unwrap(); diff --git a/packages/core/src/diff/mod.rs b/packages/core/src/diff/mod.rs index fcb9e54e9..6cb4cccca 100644 --- a/packages/core/src/diff/mod.rs +++ b/packages/core/src/diff/mod.rs @@ -103,11 +103,11 @@ impl VirtualDom { to: &mut impl WriteMutations, mut template: Template, ) { - if self.templates.contains_key(template.name) { + if self.templates.contains(&template.name) { return; } - _ = self.templates.insert(template.name, template); + _ = self.templates.insert(template.name); // If it's all dynamic nodes, then we don't need to register it if !template.is_completely_dynamic() { @@ -124,7 +124,7 @@ impl VirtualDom { /// - for appending children we can use AppendChildren #[allow(dead_code)] fn is_dyn_node_only_child(node: &VNode, idx: usize) -> bool { - let template = node.template.get(); + let template = node.template; let path = template.node_paths[idx]; // use a loop to index every static node's children until the path has run out diff --git a/packages/core/src/diff/node.rs b/packages/core/src/diff/node.rs index 4df630418..bf6d95e65 100644 --- a/packages/core/src/diff/node.rs +++ b/packages/core/src/diff/node.rs @@ -1,5 +1,5 @@ use crate::innerlude::MountId; -use crate::{Attribute, AttributeValue, DynamicNode::*, Template}; +use crate::{Attribute, AttributeValue, DynamicNode::*}; use crate::{VNode, VirtualDom, WriteMutations}; use core::iter::Peekable; @@ -21,21 +21,6 @@ impl VNode { // The node we are diffing from should always be mounted debug_assert!(dom.mounts.get(self.mount.get().0).is_some() || to.is_none()); - // If hot reloading is enabled, we need to make sure we're using the latest template - #[cfg(debug_assertions)] - { - let name = new.template.get().name; - if let Some(template) = dom.templates.get(name).cloned() { - new.template.set(template); - if template != self.template.get() { - let mount_id = self.mount.get(); - let parent = dom.mounts[mount_id.0].parent; - self.replace(std::slice::from_ref(new), parent, dom, to); - return; - } - } - } - // If the templates are different by name, we need to replace the entire template if self.templates_are_different(new) { return self.light_diff_templates(new, dom, to); @@ -120,7 +105,7 @@ impl VNode { &self, root_idx: usize, ) -> Option<(usize, &DynamicNode)> { - self.template.get().roots[root_idx] + self.template.roots[root_idx] .dynamic_id() .map(|id| (id, &self.dynamic_nodes[id])) } @@ -148,7 +133,7 @@ impl VNode { pub(crate) fn find_last_element(&self, dom: &VirtualDom) -> ElementId { let mount = &dom.mounts[self.mount.get().0]; - let last_root_index = self.template.get().roots.len() - 1; + let last_root_index = self.template.roots.len() - 1; match self.get_dynamic_root_node_and_id(last_root_index) { // This node is static, just get the root id None | Some((_, Placeholder(_) | Text(_))) => mount.root_ids[last_root_index], @@ -272,7 +257,7 @@ impl VNode { destroy_component_state: bool, replace_with: Option, ) { - let roots = self.template.get().roots; + let roots = self.template.roots; for (idx, node) in roots.iter().enumerate() { let last_node = idx == roots.len() - 1; if let Some(id) = node.dynamic_id() { @@ -305,7 +290,7 @@ impl VNode { dom: &mut VirtualDom, destroy_component_state: bool, ) { - let template = self.template.get(); + let template = self.template; for (idx, dyn_node) in self.dynamic_nodes.iter().enumerate() { let path_len = template.node_paths.get(idx).map(|path| path.len()); // Roots are cleaned up automatically above and nodes with a empty path are placeholders @@ -361,14 +346,14 @@ impl VNode { } fn templates_are_different(&self, other: &VNode) -> bool { - let self_node_name = self.template.get().id(); - let other_node_name = other.template.get().id(); + let self_node_name = self.template.id(); + let other_node_name = other.template.id(); self_node_name != other_node_name } pub(super) fn reclaim_attributes(&self, mount: MountId, dom: &mut VirtualDom) { let mut next_id = None; - for (idx, path) in self.template.get().attr_paths.iter().enumerate() { + for (idx, path) in self.template.attr_paths.iter().enumerate() { // We clean up the roots in the next step, so don't worry about them here if path.len() <= 1 { continue; @@ -399,7 +384,7 @@ impl VNode { let mut old_attributes_iter = old_attrs.iter().peekable(); let mut new_attributes_iter = new_attrs.iter().peekable(); let attribute_id = dom.mounts[mount_id.0].mounted_attributes[idx]; - let path = self.template.get().attr_paths[idx]; + let path = self.template.attr_paths[idx]; loop { match (old_attributes_iter.peek(), new_attributes_iter.peek()) { @@ -566,21 +551,6 @@ impl VNode { } } - /// Get the most up to date template for this rsx block - #[allow(unused)] - pub(crate) fn template(&self, dom: &VirtualDom) -> Template { - // check for a overridden template - #[cfg(debug_assertions)] - { - let template = self.template.get(); - if let Some(new_template) = dom.templates.get(template.name) { - self.template.set(*new_template); - } - }; - - self.template.get() - } - /// Create this rsx block. This will create scopes from components that this rsx block contains, but it will not write anything to the DOM. pub(crate) fn create( &self, @@ -589,7 +559,7 @@ impl VNode { mut to: Option<&mut impl WriteMutations>, ) -> usize { // Get the most up to date template - let template = self.template(dom); + let template = self.template; // Initialize the mount information for this vnode if it isn't already mounted if !self.mount.get().mounted() { @@ -616,13 +586,9 @@ impl VNode { } // Walk the roots, creating nodes and assigning IDs - // nodes in an iterator of (dynamic_node_index, path) - - let nodes_sorted = template.breadth_first_node_paths(); - let attrs_sorted = template.breadth_first_attribute_paths(); - - let mut nodes = nodes_sorted.peekable(); - let mut attrs = attrs_sorted.peekable(); + // nodes in an iterator of (dynamic_node_index, path) and attrs in an iterator of (attr_index, path) + let mut nodes = template.node_paths.iter().copied().enumerate().peekable(); + let mut attrs = template.attr_paths.iter().copied().enumerate().peekable(); // Get the mounted id of this block // At this point, we should have already mounted the block @@ -690,7 +656,7 @@ impl VNode { fn reference_to_dynamic_node(&self, mount: MountId, dynamic_node_id: usize) -> ElementRef { ElementRef { path: ElementPath { - path: self.template.get().node_paths[dynamic_node_id], + path: self.template.node_paths[dynamic_node_id], }, mount, } @@ -834,7 +800,7 @@ impl VNode { let this_id = dom.next_element(); dom.mounts[mount.0].root_ids[root_idx] = this_id; - to.load_template(self.template.get().name, root_idx, this_id); + to.load_template(self.template.name, root_idx, this_id); this_id } @@ -874,7 +840,7 @@ impl VNode { dom: &mut VirtualDom, ) -> (ElementId, &'static [u8]) { // Add the mutation to the list - let path = self.template.get().node_paths[idx]; + let path = self.template.node_paths[idx]; // Allocate a dynamic element reference for this text node let new_id = mount.mount_node(idx, dom); @@ -940,8 +906,8 @@ fn matching_components<'a>( left: &'a VNode, right: &'a VNode, ) -> Option> { - let left_node = left.template.get(); - let right_node = right.template.get(); + let left_node = left.template; + let right_node = right.template; if left_node.roots.len() != right_node.roots.len() { return None; } diff --git a/packages/core/src/hotreload_utils.rs b/packages/core/src/hotreload_utils.rs index 43dde887c..0fdab6413 100644 --- a/packages/core/src/hotreload_utils.rs +++ b/packages/core/src/hotreload_utils.rs @@ -1,15 +1,26 @@ -#[doc(hidden)] +use std::{ + any::{Any, TypeId}, + hash::{Hash, Hasher}, +}; + +#[cfg(feature = "serialize")] +use crate::nodes::deserialize_string_leaky; +use crate::{ + Attribute, AttributeValue, DynamicNode, Template, TemplateAttribute, TemplateNode, VNode, VText, +}; + #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))] +#[doc(hidden)] #[derive(Debug, PartialEq, Clone)] pub struct HotreloadedLiteral { pub name: String, pub value: HotReloadLiteral, } -#[doc(hidden)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))] +#[doc(hidden)] #[derive(Debug, PartialEq, Clone)] pub enum HotReloadLiteral { Fmted(FmtedSegments), @@ -18,12 +29,53 @@ pub enum HotReloadLiteral { Bool(bool), } -#[doc(hidden)] +impl HotReloadLiteral { + pub fn as_fmted(&self) -> Option<&FmtedSegments> { + match self { + Self::Fmted(segments) => Some(segments), + _ => None, + } + } + + pub fn as_float(&self) -> Option { + match self { + Self::Float(f) => Some(*f), + _ => None, + } + } + + pub fn as_int(&self) -> Option { + match self { + Self::Int(i) => Some(*i), + _ => None, + } + } + + pub fn as_bool(&self) -> Option { + match self { + Self::Bool(b) => Some(*b), + _ => None, + } + } +} + +impl Hash for HotReloadLiteral { + fn hash(&self, state: &mut H) { + match self { + Self::Fmted(segments) => segments.hash(state), + Self::Float(f) => f.to_bits().hash(state), + Self::Int(i) => i.hash(state), + Self::Bool(b) => b.hash(state), + } + } +} + #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))] -#[derive(Debug, PartialEq, Eq, Clone)] +#[doc(hidden)] +#[derive(Debug, PartialEq, Eq, Clone, Hash)] pub struct FmtedSegments { - pub segments: Vec, + pub(crate) segments: Vec, } impl FmtedSegments { @@ -32,33 +84,23 @@ impl FmtedSegments { } /// Render the formatted string by stitching together the segments - pub fn render_with(&self, dynamic_nodes: Vec) -> String { + pub(crate) fn render_with(&self, dynamic_text: &[String]) -> String { let mut out = String::new(); for segment in &self.segments { match segment { FmtSegment::Literal { value } => out.push_str(value), - FmtSegment::Dynamic { id } => out.push_str(&dynamic_nodes[*id]), + FmtSegment::Dynamic { id } => out.push_str(&dynamic_text[*id]), } } out } - - /// Update the segments with new segments - /// - /// this will change how we render the formatted string - pub fn update_segments(&mut self, new_segments: Vec) { - self.segments = new_segments; - } } -#[cfg(feature = "serialize")] -use crate::nodes::deserialize_string_leaky; - -#[doc(hidden)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, PartialEq, Eq, Clone)] +#[doc(hidden)] +#[derive(Debug, PartialEq, Eq, Clone, Hash)] pub enum FmtSegment { Literal { #[cfg_attr( @@ -71,3 +113,359 @@ pub enum FmtSegment { id: usize, }, } + +// let __pool = DynamicValuePool::new( +// vec![...], +// vec![...], +// vec![...], +// ); +// VNode::new( +// None, +// Template { +// name: "...", +// roots: &[...], +// node_paths: &[..], +// attr_paths: &[...], +// }, +// Box::new([...]), +// Box::new([...]), +// ) + +// Open questions: +// - How do we handle type coercion for different sized component property integers? +// - Should non-string hot literals go through the centralized pool? +// - Should formatted strings be a runtime concept? + +#[doc(hidden)] +pub struct DynamicLiteralPool { + dynamic_text: Box<[String]>, +} + +impl DynamicLiteralPool { + pub fn new(dynamic_text: Vec) -> Self { + Self { + dynamic_text: dynamic_text.into_boxed_slice(), + } + } + + pub fn get_component_property<'a, T>( + &self, + id: usize, + hot_reload: &'a HotReloadedTemplate, + f: impl FnOnce(&'a HotReloadLiteral) -> Option, + ) -> Option { + f(hot_reload.component_values.get(id)?) + } + + /// Get a component property of a specific type at the component property index + pub fn component_property( + &mut self, + id: usize, + hot_reload: &HotReloadedTemplate, + // We pass in the original value for better type inference + // For example, if the original literal is `0i128`, we know the output must be the type `i128` + _coherse_type: T, + ) -> T { + fn assert_type(t: T) -> T2 { + *(Box::new(t) as Box).downcast::().unwrap() + } + let grab_float = || { + self.get_component_property(id, hot_reload, HotReloadLiteral::as_float).expect("Expected a float component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.") + }; + let grab_int = || { + self.get_component_property(id, hot_reload, HotReloadLiteral::as_int).expect("Expected an int component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.") + }; + let grab_bool = || { + self.get_component_property(id, hot_reload, HotReloadLiteral::as_bool).expect("Expected a bool component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.") + }; + let grab_fmted = || { + self.get_component_property(id, hot_reload, |fmted| HotReloadLiteral::as_fmted(fmted).map(|segments| self.render_formatted(segments))).expect("Expected a string component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.") + }; + match TypeId::of::() { + // Any string types that accept a literal + _ if TypeId::of::() == TypeId::of::() => assert_type(grab_fmted()), + _ if TypeId::of::<&'static str>() == TypeId::of::() => { + assert_type(Box::leak(grab_fmted().into_boxed_str()) as &'static str) + } + // Any integer types that accept a literal + _ if TypeId::of::() == TypeId::of::() => assert_type(grab_int() as i128), + _ if TypeId::of::() == TypeId::of::() => assert_type(grab_int()), + _ if TypeId::of::() == TypeId::of::() => assert_type(grab_int() as i32), + _ if TypeId::of::() == TypeId::of::() => assert_type(grab_int() as i16), + _ if TypeId::of::() == TypeId::of::() => assert_type(grab_int() as i8), + _ if TypeId::of::() == TypeId::of::() => assert_type(grab_int() as isize), + _ if TypeId::of::() == TypeId::of::() => assert_type(grab_int() as u128), + _ if TypeId::of::() == TypeId::of::() => assert_type(grab_int() as u64), + _ if TypeId::of::() == TypeId::of::() => assert_type(grab_int() as u32), + _ if TypeId::of::() == TypeId::of::() => assert_type(grab_int() as u16), + _ if TypeId::of::() == TypeId::of::() => assert_type(grab_int() as u8), + _ if TypeId::of::() == TypeId::of::() => assert_type(grab_int() as usize), + // Any float types that accept a literal + _ if TypeId::of::() == TypeId::of::() => assert_type(grab_float()), + _ if TypeId::of::() == TypeId::of::() => assert_type(grab_float() as f32), + // Any bool types that accept a literal + _ if TypeId::of::() == TypeId::of::() => assert_type(grab_bool()), + _ => panic!("Unsupported component property type"), + } + } + + pub fn render_formatted(&self, segments: &FmtedSegments) -> String { + segments.render_with(&self.dynamic_text) + } +} +#[doc(hidden)] +pub struct DynamicValuePool { + dynamic_attributes: Box<[Box<[Attribute]>]>, + dynamic_nodes: Box<[DynamicNode]>, + literal_pool: DynamicLiteralPool, +} + +impl DynamicValuePool { + pub fn new( + dynamic_nodes: Vec, + dynamic_attributes: Vec>, + literal_pool: DynamicLiteralPool, + ) -> Self { + Self { + dynamic_attributes: dynamic_attributes.into_boxed_slice(), + dynamic_nodes: dynamic_nodes.into_boxed_slice(), + literal_pool, + } + } + + pub fn render_with(&mut self, hot_reload: &HotReloadedTemplate) -> VNode { + // Get the node_paths from a depth first traversal of the template + let node_paths = hot_reload.node_paths(); + let attr_paths = hot_reload.attr_paths(); + + let template = Template { + name: hot_reload.name, + roots: hot_reload.roots, + node_paths, + attr_paths, + }; + let key = hot_reload + .key + .as_ref() + .map(|key| self.literal_pool.render_formatted(key)); + let dynamic_nodes = hot_reload + .dynamic_nodes + .iter() + .map(|node| self.render_dynamic_node(node)) + .collect(); + let dynamic_attrs = hot_reload + .dynamic_attributes + .iter() + .map(|attr| self.render_attribute(attr)) + .collect(); + + VNode::new(key, template, dynamic_nodes, dynamic_attrs) + } + + fn render_dynamic_node(&mut self, node: &HotReloadDynamicNode) -> DynamicNode { + match node { + // If the node is dynamic, take it from the pool and return it + HotReloadDynamicNode::Dynamic(id) => self.dynamic_nodes[*id].clone(), + // Otherwise, format the text node and return it + HotReloadDynamicNode::Formatted(segments) => DynamicNode::Text(VText { + value: self.literal_pool.render_formatted(segments), + }), + } + } + + fn render_attribute(&mut self, attr: &HotReloadDynamicAttribute) -> Box<[Attribute]> { + match attr { + HotReloadDynamicAttribute::Dynamic(id) => self.dynamic_attributes[*id].clone(), + HotReloadDynamicAttribute::Named(NamedAttribute { + name, + namespace, + value, + }) => Box::new([Attribute { + name, + namespace: *namespace, + value: match value { + HotReloadAttributeValue::Literal(HotReloadLiteral::Fmted(segments)) => { + AttributeValue::Text(self.literal_pool.render_formatted(segments)) + } + HotReloadAttributeValue::Literal(HotReloadLiteral::Float(f)) => { + AttributeValue::Float(*f) + } + HotReloadAttributeValue::Literal(HotReloadLiteral::Int(i)) => { + AttributeValue::Int(*i) + } + HotReloadAttributeValue::Literal(HotReloadLiteral::Bool(b)) => { + AttributeValue::Bool(*b) + } + HotReloadAttributeValue::Dynamic(id) => { + self.dynamic_attributes[*id][0].value.clone() + } + }, + volatile: false, + }]), + } + } +} + +#[doc(hidden)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))] +pub struct HotReloadTemplateWithLocation { + pub location: String, + pub template: HotReloadedTemplate, +} + +#[doc(hidden)] +#[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct HotReloadedTemplate { + pub name: &'static str, + pub key: Option, + pub dynamic_nodes: Vec, + pub dynamic_attributes: Vec, + pub component_values: Vec, + #[cfg_attr( + feature = "serialize", + serde(deserialize_with = "crate::nodes::deserialize_leaky") + )] + pub roots: &'static [TemplateNode], +} + +impl HotReloadedTemplate { + pub fn new( + name: &'static str, + key: Option, + dynamic_nodes: Vec, + dynamic_attributes: Vec, + component_values: Vec, + roots: &'static [TemplateNode], + ) -> Self { + Self { + name, + key, + dynamic_nodes, + dynamic_attributes, + component_values, + roots, + } + } + + fn node_paths(&self) -> &'static [&'static [u8]] { + fn add_node_paths( + roots: &[TemplateNode], + node_paths: &mut Vec<&'static [u8]>, + current_path: Vec, + ) { + for (idx, node) in roots.iter().enumerate() { + let mut path = current_path.clone(); + path.push(idx as u8); + match node { + TemplateNode::Element { children, .. } => { + add_node_paths(children, node_paths, path); + } + TemplateNode::Text { .. } => {} + TemplateNode::Dynamic { id } => { + debug_assert_eq!(node_paths.len(), *id); + node_paths.push(Box::leak(path.into_boxed_slice())); + } + } + } + } + + let mut node_paths = Vec::new(); + add_node_paths(self.roots, &mut node_paths, Vec::new()); + let leaked: &'static [&'static [u8]] = Box::leak(node_paths.into_boxed_slice()); + leaked + } + + fn attr_paths(&self) -> &'static [&'static [u8]] { + fn add_attr_paths( + roots: &[TemplateNode], + attr_paths: &mut Vec<&'static [u8]>, + current_path: Vec, + ) { + for (idx, node) in roots.iter().enumerate() { + let mut path = current_path.clone(); + path.push(idx as u8); + if let TemplateNode::Element { + children, attrs, .. + } = node + { + for attr in *attrs { + if let TemplateAttribute::Dynamic { id } = attr { + debug_assert_eq!(attr_paths.len(), *id); + attr_paths.push(Box::leak(path.clone().into_boxed_slice())); + } + } + add_attr_paths(children, attr_paths, path); + } + } + } + + let mut attr_paths = Vec::new(); + add_attr_paths(self.roots, &mut attr_paths, Vec::new()); + let leaked: &'static [&'static [u8]] = Box::leak(attr_paths.into_boxed_slice()); + leaked + } +} + +#[doc(hidden)] +#[derive(Debug, PartialEq, Clone, Hash)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))] +pub enum HotReloadDynamicNode { + Dynamic(usize), + Formatted(FmtedSegments), +} + +#[doc(hidden)] +#[derive(Debug, PartialEq, Clone, Hash)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))] +pub enum HotReloadDynamicAttribute { + Dynamic(usize), + Named(NamedAttribute), +} + +#[doc(hidden)] +#[derive(Debug, PartialEq, Clone, Hash)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct NamedAttribute { + /// The name of this attribute. + #[cfg_attr( + feature = "serialize", + serde(deserialize_with = "crate::nodes::deserialize_string_leaky") + )] + name: &'static str, + /// The namespace of this attribute. Does not exist in the HTML spec + #[cfg_attr( + feature = "serialize", + serde(deserialize_with = "crate::nodes::deserialize_option_leaky") + )] + namespace: Option<&'static str>, + + value: HotReloadAttributeValue, +} + +impl NamedAttribute { + pub fn new( + name: &'static str, + namespace: Option<&'static str>, + value: HotReloadAttributeValue, + ) -> Self { + Self { + name, + namespace, + value, + } + } +} + +#[doc(hidden)] +#[derive(Debug, PartialEq, Clone, Hash)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))] +pub enum HotReloadAttributeValue { + Literal(HotReloadLiteral), + Dynamic(usize), +} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 47023bed2..381c8b735 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -34,8 +34,11 @@ mod hotreload_utils; pub mod internal { pub use crate::properties::verify_component_called_as_component; + #[doc(hidden)] pub use crate::hotreload_utils::{ - FmtSegment, FmtedSegments, HotReloadLiteral, HotreloadedLiteral, + DynamicLiteralPool, DynamicValuePool, FmtSegment, FmtedSegments, HotReloadAttributeValue, + HotReloadDynamicAttribute, HotReloadDynamicNode, HotReloadLiteral, + HotReloadTemplateWithLocation, HotReloadedTemplate, HotreloadedLiteral, NamedAttribute, }; } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 1303b391b..798e04244 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -6,7 +6,6 @@ use crate::{ properties::ComponentFunction, }; use crate::{Properties, ScopeId, VirtualDom}; -use core::panic; use std::ops::{Deref, DerefMut}; use std::rc::Rc; use std::vec; @@ -122,7 +121,7 @@ pub struct VNodeInner { pub key: Option, /// The static nodes and static descriptor of the template - pub template: Cell