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
This commit is contained in:
1 parent
f3ca1484a1
commit
34bdcd15cf
40 files changed
+2288
-1897
No files matched your search
@@ -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<f64> {
|
||||
match self {
|
||||
Self::Float(f) => Some(*f),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_int(&self) -> Option<i64> {
|
||||
match self {
|
||||
Self::Int(i) => Some(*i),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_bool(&self) -> Option<bool> {
|
||||
match self {
|
||||
Self::Bool(b) => Some(*b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for HotReloadLiteral {
|
||||
fn hash<H: Hasher>(&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<FmtSegment>,
|
||||
pub(crate) segments: Vec<FmtSegment>,
|
||||
}
|
||||
|
||||
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>) -> 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<FmtSegment>) {
|
||||
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<String>) -> 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<T>,
|
||||
) -> Option<T> {
|
||||
f(hot_reload.component_values.get(id)?)
|
||||
}
|
||||
|
||||
/// Get a component property of a specific type at the component property index
|
||||
pub fn component_property<T: 'static>(
|
||||
&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: 'static, T2: 'static>(t: T) -> T2 {
|
||||
*(Box::new(t) as Box<dyn Any>).downcast::<T2>().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::<T>() {
|
||||
// Any string types that accept a literal
|
||||
_ if TypeId::of::<String>() == TypeId::of::<T>() => assert_type(grab_fmted()),
|
||||
_ if TypeId::of::<&'static str>() == TypeId::of::<T>() => {
|
||||
assert_type(Box::leak(grab_fmted().into_boxed_str()) as &'static str)
|
||||
}
|
||||
// Any integer types that accept a literal
|
||||
_ if TypeId::of::<i128>() == TypeId::of::<T>() => assert_type(grab_int() as i128),
|
||||
_ if TypeId::of::<i64>() == TypeId::of::<T>() => assert_type(grab_int()),
|
||||
_ if TypeId::of::<i32>() == TypeId::of::<T>() => assert_type(grab_int() as i32),
|
||||
_ if TypeId::of::<i16>() == TypeId::of::<T>() => assert_type(grab_int() as i16),
|
||||
_ if TypeId::of::<i8>() == TypeId::of::<T>() => assert_type(grab_int() as i8),
|
||||
_ if TypeId::of::<isize>() == TypeId::of::<T>() => assert_type(grab_int() as isize),
|
||||
_ if TypeId::of::<u128>() == TypeId::of::<T>() => assert_type(grab_int() as u128),
|
||||
_ if TypeId::of::<u64>() == TypeId::of::<T>() => assert_type(grab_int() as u64),
|
||||
_ if TypeId::of::<u32>() == TypeId::of::<T>() => assert_type(grab_int() as u32),
|
||||
_ if TypeId::of::<u16>() == TypeId::of::<T>() => assert_type(grab_int() as u16),
|
||||
_ if TypeId::of::<u8>() == TypeId::of::<T>() => assert_type(grab_int() as u8),
|
||||
_ if TypeId::of::<usize>() == TypeId::of::<T>() => assert_type(grab_int() as usize),
|
||||
// Any float types that accept a literal
|
||||
_ if TypeId::of::<f64>() == TypeId::of::<T>() => assert_type(grab_float()),
|
||||
_ if TypeId::of::<f32>() == TypeId::of::<T>() => assert_type(grab_float() as f32),
|
||||
// Any bool types that accept a literal
|
||||
_ if TypeId::of::<bool>() == TypeId::of::<T>() => 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<DynamicNode>,
|
||||
dynamic_attributes: Vec<Box<[Attribute]>>,
|
||||
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<FmtedSegments>,
|
||||
pub dynamic_nodes: Vec<HotReloadDynamicNode>,
|
||||
pub dynamic_attributes: Vec<HotReloadDynamicAttribute>,
|
||||
pub component_values: Vec<HotReloadLiteral>,
|
||||
#[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<FmtedSegments>,
|
||||
dynamic_nodes: Vec<HotReloadDynamicNode>,
|
||||
dynamic_attributes: Vec<HotReloadDynamicAttribute>,
|
||||
component_values: Vec<HotReloadLiteral>,
|
||||
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<u8>,
|
||||
) {
|
||||
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<u8>,
|
||||
) {
|
||||
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),
|
||||
}
|
||||
Reference in new issue
Block a user