Fix hot reloading components with keys (#2886)

* Fix hot reloading components with keys

* include component formatted segment keys, but not dynamic component value

* Fix component_literal_dyn_idx index

* add a new test for hot reloading components with keys

* Even more tests

* make clippy happy

* fix typo
This commit is contained in:
Evan Almloff 2024-08-29 01:53:11 +02:00 committed by GitHub
parent 0e937daaa1
commit 4676171861
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 176 additions and 56 deletions

View file

@ -69,6 +69,8 @@ impl<'a> DynIdVisitor<'a> {
if let HotLiteral::Fmted(segments) = literal { if let HotLiteral::Fmted(segments) = literal {
self.assign_formatted_segment(segments); self.assign_formatted_segment(segments);
} }
// Don't include keys in the component dynamic pool
if !property.name.is_likely_key() {
component.component_literal_dyn_idx[index] component.component_literal_dyn_idx[index]
.set(self.component_literal_index); .set(self.component_literal_index);
self.component_literal_index += 1; self.component_literal_index += 1;
@ -76,6 +78,7 @@ impl<'a> DynIdVisitor<'a> {
} }
} }
} }
}
}; };
} }

View file

@ -406,6 +406,10 @@ impl AttributeName {
matches!(self, Self::BuiltIn(ident) if ident.to_string().starts_with("on")) matches!(self, Self::BuiltIn(ident) if ident.to_string().starts_with("on"))
} }
pub fn is_likely_key(&self) -> bool {
matches!(self, Self::BuiltIn(ident) if ident == "key")
}
pub fn span(&self) -> proc_macro2::Span { pub fn span(&self) -> proc_macro2::Span {
match self { match self {
Self::Custom(lit) => lit.span(), Self::Custom(lit) => lit.span(),

View file

@ -171,10 +171,10 @@ impl Component {
} }
pub fn get_key(&self) -> Option<&AttributeValue> { pub fn get_key(&self) -> Option<&AttributeValue> {
self.fields.iter().find_map(|attr| match &attr.name { self.fields
AttributeName::BuiltIn(key) if key == "key" => Some(&attr.value), .iter()
_ => None, .find(|attr| attr.name.is_likely_key())
}) .map(|attr| &attr.value)
} }
/// Ensure there's no duplicate props - this will be a compile error but we can move it to a /// Ensure there's no duplicate props - this will be a compile error but we can move it to a
@ -252,28 +252,21 @@ impl Component {
self.spreads.first().map(|spread| &spread.expr) self.spreads.first().map(|spread| &spread.expr)
} }
fn make_field_idents(&self) -> Vec<(TokenStream2, TokenStream2)> { // Iterate over the props of the component (without spreads, key, and custom attributes)
let mut dynamic_literal_index = 0; pub(crate) fn component_props(&self) -> impl Iterator<Item = &Attribute> {
self.fields self.fields
.iter() .iter()
.filter_map(move |attr| { .filter(move |attr| !attr.name.is_likely_key())
let Attribute { name, value, .. } = attr;
let attr = match name {
AttributeName::BuiltIn(k) => {
if k == "key" {
return None;
} }
quote! { #k }
}
AttributeName::Custom(_) => return None,
AttributeName::Spread(_) => return None,
};
let release_value = value.to_token_stream(); fn make_field_idents(&self) -> Vec<(TokenStream2, TokenStream2)> {
let mut dynamic_literal_index = 0;
self.component_props()
.map(|attribute| {
let release_value = attribute.value.to_token_stream();
// In debug mode, we try to grab the value from the dynamic literal pool if possible // In debug mode, we try to grab the value from the dynamic literal pool if possible
let value = if let AttributeValue::AttrLiteral(literal) = &value { let value = if let AttributeValue::AttrLiteral(literal) = &attribute.value {
let idx = self.component_literal_dyn_idx[dynamic_literal_index].get(); let idx = self.component_literal_dyn_idx[dynamic_literal_index].get();
dynamic_literal_index += 1; dynamic_literal_index += 1;
let debug_value = quote! { __dynamic_literal_pool.component_property(#idx, &*__template_read, #literal) }; let debug_value = quote! { __dynamic_literal_pool.component_property(#idx, &*__template_read, #literal) };
@ -293,7 +286,7 @@ impl Component {
release_value release_value
}; };
Some((attr, value)) (attribute.name.to_token_stream(), value)
}) })
.collect() .collect()
} }
@ -345,6 +338,7 @@ fn normalize_path(name: &mut syn::Path) -> Option<AngleBracketedGenericArguments
mod tests { mod tests {
use super::*; use super::*;
use prettier_please::PrettyUnparse; use prettier_please::PrettyUnparse;
use syn::parse_quote;
/// Ensure we can parse a component /// Ensure we can parse a component
#[test] #[test]
@ -482,4 +476,23 @@ mod tests {
let _parsed: syn::Path = syn::parse2(input).unwrap(); let _parsed: syn::Path = syn::parse2(input).unwrap();
} }
#[test]
fn identifies_key() {
let input = quote! {
Link { key: "{value}", to: Route::List, class: "pure-button", "Go back" }
};
let component: Component = syn::parse2(input).unwrap();
// The key should exist
assert_eq!(component.get_key(), Some(&parse_quote!("{value}")));
// The key should not be included in the properties
let properties = component
.component_props()
.map(|attr| attr.name.to_string())
.collect::<Vec<_>>();
assert_eq!(properties, ["to", "class"]);
}
} }

View file

@ -237,7 +237,7 @@ impl Element {
} }
for attr in attrs { for attr in attrs {
if attr.name.to_string() == "key" { if attr.name.is_likely_key() {
continue; continue;
} }
@ -301,15 +301,10 @@ impl Element {
} }
pub(crate) fn key(&self) -> Option<&AttributeValue> { pub(crate) fn key(&self) -> Option<&AttributeValue> {
for attr in &self.raw_attributes { self.raw_attributes
if let AttributeName::BuiltIn(name) = &attr.name { .iter()
if name == "key" { .find(|attr| attr.name.is_likely_key())
return Some(&attr.value); .map(|attr| &attr.value)
}
}
}
None
} }
fn completion_hints(&self) -> TokenStream2 { fn completion_hints(&self) -> TokenStream2 {

View file

@ -406,13 +406,15 @@ impl HotReloadResult {
} }
// Then check if the fields are the same // Then check if the fields are the same
if new_component.fields.len() != old_component.fields.len() { 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; return None;
} }
let mut new_fields = new_component.fields.clone(); let mut new_fields = new_non_key_fields.clone();
new_fields.sort_by_key(|attribute| attribute.name.to_string()); new_fields.sort_by_key(|attribute| attribute.name.to_string());
let mut old_fields = old_component.fields.iter().enumerate().collect::<Vec<_>>(); let mut old_fields = old_non_key_fields.iter().enumerate().collect::<Vec<_>>();
old_fields.sort_by_key(|(_, attribute)| attribute.name.to_string()); 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 // The literal component properties for the component in same the order as the original component property literals

View file

@ -320,7 +320,7 @@ impl TemplateBody {
} }
}) })
.flat_map(|component| { .flat_map(|component| {
component.fields.iter().filter_map(|field| { component.component_props().filter_map(|field| {
if let AttributeValue::AttrLiteral(literal) = &field.value { if let AttributeValue::AttrLiteral(literal) = &field.value {
Some(literal) Some(literal)
} else { } else {

View file

@ -1085,6 +1085,109 @@ fn component_with_handlers() {
); );
} }
#[test]
fn component_remove_key() {
let a = quote! {
Component {
key: "{key}",
class: 123,
id: 456.789,
other: true,
dynamic1,
dynamic2,
blah: "hello {world}",
onclick: |e| { println!("clicked") },
}
};
// changing lit values
let b = quote! {
Component {
class: 456,
id: 789.456,
other: false,
dynamic1,
dynamic2,
blah: "goodbye {world}",
onclick: |e| { println!("clicked") },
}
};
let hot_reload = hot_reload_from_tokens(a, b).unwrap();
let template = hot_reload.get(&0).unwrap();
assert_eq!(
template.component_values,
&[
HotReloadLiteral::Int(456),
HotReloadLiteral::Float(789.456),
HotReloadLiteral::Bool(false),
HotReloadLiteral::Fmted(FmtedSegments::new(vec![
FmtSegment::Literal { value: "goodbye " },
FmtSegment::Dynamic { id: 1 }
]))
]
);
}
#[test]
fn component_modify_key() {
let a = quote! {
Component {
key: "{key}",
class: 123,
id: 456.789,
other: true,
dynamic1,
dynamic2,
blah1: "hello {world123}",
blah2: "hello {world}",
onclick: |e| { println!("clicked") },
}
};
// changing lit values
let b = quote! {
Component {
key: "{key}-{world}",
class: 456,
id: 789.456,
other: false,
dynamic1,
dynamic2,
blah1: "hello {world123}",
blah2: "hello {world}",
onclick: |e| { println!("clicked") },
}
};
let hot_reload = hot_reload_from_tokens(a, b).unwrap();
let template = hot_reload.get(&0).unwrap();
assert_eq!(
template.key,
Some(FmtedSegments::new(vec![
FmtSegment::Dynamic { id: 0 },
FmtSegment::Literal { value: "-" },
FmtSegment::Dynamic { id: 2 },
]))
);
assert_eq!(
template.component_values,
&[
HotReloadLiteral::Int(456),
HotReloadLiteral::Float(789.456),
HotReloadLiteral::Bool(false),
HotReloadLiteral::Fmted(FmtedSegments::new(vec![
FmtSegment::Literal { value: "hello " },
FmtSegment::Dynamic { id: 1 }
])),
HotReloadLiteral::Fmted(FmtedSegments::new(vec![
FmtSegment::Literal { value: "hello " },
FmtSegment::Dynamic { id: 2 }
]))
]
);
}
#[test] #[test]
fn duplicating_dynamic_nodes() { fn duplicating_dynamic_nodes() {
let a = quote! { let a = quote! {