use fervid_core::{
check_attribute_name, AttributeOrBinding, ElementNode, StrOrExpr, VBindDirective, VueImports,
};
use swc_core::ecma::ast::{
CallExpr, Callee, Expr, ExprOrSpread, Ident, Lit, ObjectLit, PropOrSpread, Str,
};
use crate::CodegenContext;
impl CodegenContext {
pub fn generate_component_builtin(&mut self, element_node: &ElementNode) -> Expr {
let span = element_node.span;
let attributes = &element_node.starting_tag.attributes;
let component_is_attribute_idx = attributes
.iter()
.position(|attr| check_attribute_name(attr, "is"))
.expect("<component> should always have `is` attribute");
let component_is_attribute = &attributes[component_is_attribute_idx];
let is_attribute_expr = match component_is_attribute {
AttributeOrBinding::RegularAttribute { name, value, span } if name == "is" => {
Expr::Lit(Lit::Str(Str {
span: *span,
value: value.to_owned(),
raw: None,
}))
}
AttributeOrBinding::VBind(VBindDirective {
argument: Some(StrOrExpr::Str(name)),
value,
..
}) if name == "is" => (**value).to_owned(),
_ => unreachable!(),
};
let identifier = Expr::Call(CallExpr {
span,
callee: Callee::Expr(Box::new(Expr::Ident(Ident {
span,
sym: self.get_and_add_import_ident(VueImports::ResolveDynamicComponent),
optional: false,
}))),
args: vec![ExprOrSpread {
spread: None,
expr: Box::new(is_attribute_expr),
}],
type_args: None,
});
let component_builtin_attrs: Option<Expr> = if attributes.len() != 1 {
let attrs_first_half = &attributes[..component_is_attribute_idx];
let attrs_second_half = &attributes[(component_is_attribute_idx + 1)..];
let mut attrs: Vec<PropOrSpread> = Vec::with_capacity(attributes.len() - 1);
self.generate_attributes(attrs_first_half, &mut attrs);
self.generate_attributes(attrs_second_half, &mut attrs);
Some(Expr::Object(ObjectLit { span, props: attrs }))
} else {
None
};
let component_builtin_slots = self.generate_builtin_slots(element_node);
self.generate_componentlike(
identifier,
component_builtin_attrs,
component_builtin_slots,
&element_node.patch_hints,
true,
span,
)
}
}
#[cfg(test)]
mod tests {
use std::fmt::Debug;
use fervid_core::{BuiltinType, ElementKind, Node, StartingTag, VSlotDirective, VueDirectives};
use swc_core::common::DUMMY_SP;
use crate::test_utils::{regular_attribute, v_bind_attribute};
use super::*;
#[test]
fn it_panics_at_empty_component() {
test_panic(
|| {
test_out(
ElementNode {
kind: ElementKind::Builtin(BuiltinType::Component),
starting_tag: StartingTag {
tag_name: "component".into(),
attributes: vec![],
directives: None,
},
children: vec![],
template_scope: 0,
patch_hints: Default::default(),
span: DUMMY_SP,
},
r#""#,
);
},
"<component> should always have `is` attribute",
);
}
#[test]
fn it_generates_component_is_static() {
test_out(
ElementNode {
kind: ElementKind::Builtin(BuiltinType::Component),
starting_tag: StartingTag {
tag_name: "component".into(),
attributes: vec![regular_attribute("is", "div")],
directives: None,
},
children: vec![],
template_scope: 0,
patch_hints: Default::default(),
span: DUMMY_SP,
},
r#"(_openBlock(),_createBlock(_resolveDynamicComponent("div")))"#,
);
}
#[test]
fn it_generates_component_is_binding() {
test_out(
ElementNode {
kind: ElementKind::Builtin(BuiltinType::Component),
starting_tag: StartingTag {
tag_name: "component".into(),
attributes: vec![v_bind_attribute("is", "foo")],
directives: None,
},
children: vec![],
template_scope: 0,
patch_hints: Default::default(),
span: DUMMY_SP,
},
r#"(_openBlock(),_createBlock(_resolveDynamicComponent(foo)))"#,
);
}
#[test]
fn it_generates_component_builtin_attrs() {
test_out(
ElementNode {
kind: ElementKind::Builtin(BuiltinType::Component),
starting_tag: StartingTag {
tag_name: "component".into(),
attributes: vec![
regular_attribute("is", "div"),
regular_attribute("foo", "bar"),
v_bind_attribute("baz", "qux"),
],
directives: None,
},
children: vec![],
template_scope: 0,
patch_hints: Default::default(),
span: DUMMY_SP,
},
r#"(_openBlock(),_createBlock(_resolveDynamicComponent("div"),{foo:"bar",baz:qux}))"#,
)
}
#[test]
fn it_generates_component_builtin_default_slot() {
test_out(
ElementNode {
kind: ElementKind::Builtin(BuiltinType::Component),
starting_tag: StartingTag {
tag_name: "component".into(),
attributes: vec![regular_attribute("is", "div")],
directives: None,
},
children: vec![Node::Text("foobar".into(), DUMMY_SP)],
template_scope: 0,
patch_hints: Default::default(),
span: DUMMY_SP,
},
r#"(_openBlock(),_createBlock(_resolveDynamicComponent("div"),null,{"default":_withCtx(()=>[_createTextVNode("foobar")]),_:1}))"#,
)
}
#[test]
fn it_generates_component_builtin_named_slot() {
test_out(
ElementNode {
kind: ElementKind::Builtin(BuiltinType::Component),
starting_tag: StartingTag {
tag_name: "component".into(),
attributes: vec![regular_attribute("is", "div")],
directives: None,
},
children: vec![Node::Element(ElementNode {
kind: ElementKind::Element,
starting_tag: StartingTag {
tag_name: "template".into(),
attributes: vec![],
directives: Some(Box::new(VueDirectives {
v_slot: Some(VSlotDirective {
slot_name: Some("named".into()),
value: None,
}),
..Default::default()
})),
},
children: vec![Node::Text("foobar".into(), DUMMY_SP)],
template_scope: 0,
patch_hints: Default::default(),
span: DUMMY_SP,
})],
template_scope: 0,
patch_hints: Default::default(),
span: DUMMY_SP,
},
r#"(_openBlock(),_createBlock(_resolveDynamicComponent("div"),null,{named:_withCtx(()=>[_createTextVNode("foobar")]),_:1}))"#,
)
}
#[test]
fn it_generates_full_component_builtin() {
test_out(
ElementNode {
kind: ElementKind::Builtin(BuiltinType::Component),
starting_tag: StartingTag {
tag_name: "component".into(),
attributes: vec![
regular_attribute("is", "div"),
regular_attribute("foo", "bar"),
v_bind_attribute("baz", "qux"),
],
directives: None,
},
children: vec![
Node::Text("foobar".into(), DUMMY_SP),
Node::Element(ElementNode {
kind: ElementKind::Element,
starting_tag: StartingTag {
tag_name: "template".into(),
attributes: vec![],
directives: Some(Box::new(VueDirectives {
v_slot: Some(VSlotDirective {
slot_name: Some("named".into()),
value: None,
}),
..Default::default()
})),
},
children: vec![Node::Text("bazqux".into(), DUMMY_SP)],
template_scope: 0,
patch_hints: Default::default(),
span: DUMMY_SP,
}),
],
template_scope: 0,
patch_hints: Default::default(),
span: DUMMY_SP,
},
r#"(_openBlock(),_createBlock(_resolveDynamicComponent("div"),{foo:"bar",baz:qux},{named:_withCtx(()=>[_createTextVNode("bazqux")]),"default":_withCtx(()=>[_createTextVNode("foobar")]),_:1}))"#,
)
}
fn test_out(input: ElementNode, expected: &str) {
let mut ctx = CodegenContext::default();
let out = ctx.generate_component_builtin(&input);
assert_eq!(crate::test_utils::to_str(out), expected)
}
fn test_panic<F: FnOnce() -> R + std::panic::UnwindSafe, R: Debug>(f: F, expected_err: &str) {
let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let err = std::panic::catch_unwind(f).unwrap_err();
std::panic::set_hook(prev_hook);
let panic_msg = panic_message::get_panic_message(&err);
assert!(panic_msg.is_some());
assert_eq!(expected_err, panic_msg.unwrap());
}
}