use crate::swc::atoms::Atom;
use swc_atoms::Wtf8Atom;
use swc_common::DUMMY_SP;
use swc_common::SyntaxContext;
use swc_ecma_ast::*;
use swc_ecma_lexer::common::lexer::char::CharExt;
use swc_ecma_utils::prepend_stmt;
use swc_ecma_utils::quote_ident;
use swc_ecma_visit::VisitMut;
use swc_ecma_visit::VisitMutWith;
use swc_ecma_visit::noop_visit_mut_type;
#[derive(Debug, Default)]
pub struct JsxPrecompile {
import_source: Option<String>,
skip_serialize: Option<Vec<String>>,
skip_prop_serialize: Option<Vec<String>>,
next_index: usize,
templates: Vec<(usize, Vec<String>)>,
import_jsx: Option<Ident>,
import_jsx_ssr: Option<Ident>,
import_jsx_attr: Option<Ident>,
import_jsx_escape: Option<Ident>,
}
impl JsxPrecompile {
pub fn new(
import_source: Option<String>,
skip_serialize: Option<Vec<String>>,
skip_prop_serialize: Option<Vec<String>>,
) -> Self {
Self {
import_source,
skip_serialize,
skip_prop_serialize,
..JsxPrecompile::default()
}
}
}
fn create_tpl_binding_name(index: usize) -> String {
format!("$$_tpl_{index}")
}
fn normalize_dom_attr_name(name: &str) -> String {
match name {
"htmlFor" => "for".to_string(),
"className" => "class".to_string(),
"dangerouslySetInnerHTML" => name.to_string(),
"panose1" => "panose-1".to_string(),
"xlinkActuate" => "xlink:actuate".to_string(),
"xlinkArcrole" => "xlink:arcrole".to_string(),
"xlinkHref" => "href".to_string(),
"xlink:href" => "href".to_string(),
"xlinkRole" => "xlink:role".to_string(),
"xlinkShow" => "xlink:show".to_string(),
"xlinkTitle" => "xlink:title".to_string(),
"xlinkType" => "xlink:type".to_string(),
"xmlBase" => "xml:base".to_string(),
"xmlLang" => "xml:lang".to_string(),
"xmlSpace" => "xml:space".to_string(),
"accentHeight"
| "acceptCharset"
| "alignmentBaseline"
| "arabicForm"
| "baselineShift"
| "capHeight"
| "clipPath"
| "clipRule"
| "colorInterpolation"
| "colorInterpolationFilters"
| "colorProfile"
| "colorRendering"
| "contentScriptType"
| "contentStyleType"
| "dominantBaseline"
| "enableBackground"
| "fillOpacity"
| "fillRule"
| "floodColor"
| "floodOpacity"
| "fontFamily"
| "fontSize"
| "fontSizeAdjust"
| "fontStretch"
| "fontStyle"
| "fontVariant"
| "fontWeight"
| "glyphName"
| "glyphOrientationHorizontal"
| "glyphOrientationVertical"
| "horizAdvX"
| "horizOriginX"
| "horizOriginY"
| "httpEquiv"
| "imageRendering"
| "letterSpacing"
| "lightingColor"
| "markerEnd"
| "markerMid"
| "markerStart"
| "overlinePosition"
| "overlineThickness"
| "paintOrder"
| "pointerEvents"
| "renderingIntent"
| "shapeRendering"
| "stopColor"
| "stopOpacity"
| "strikethroughPosition"
| "strikethroughThickness"
| "strokeDasharray"
| "strokeDashoffset"
| "strokeLinecap"
| "strokeLinejoin"
| "strokeMiterlimit"
| "strokeOpacity"
| "strokeWidth"
| "textAnchor"
| "textDecoration"
| "textRendering"
| "transformOrigin"
| "underlinePosition"
| "underlineThickness"
| "unicodeBidi"
| "unicodeRange"
| "unitsPerEm"
| "vAlphabetic"
| "vectorEffect"
| "vertAdvY"
| "vertOriginX"
| "vertOriginY"
| "vHanging"
| "vMathematical"
| "wordSpacing"
| "writingMode"
| "xHeight" => name
.chars()
.map(|ch| match ch {
'A'..='Z' => format!("-{}", ch.to_lowercase()),
_ => ch.to_string(),
})
.collect(),
"allowReorder"
| "attributeName"
| "attributeType"
| "baseFrequency"
| "baseProfile"
| "calcMode"
| "clipPathUnits"
| "diffuseConstant"
| "edgeMode"
| "filterUnits"
| "glyphRef"
| "gradientTransform"
| "gradientUnits"
| "kernelMatrix"
| "kernelUnitLength"
| "keyPoints"
| "keySplines"
| "keyTimes"
| "lengthAdjust"
| "limitingConeAngle"
| "markerHeight"
| "markerUnits"
| "markerWidth"
| "maskContentUnits"
| "maskUnits"
| "numOctaves"
| "pathLength"
| "patternContentUnits"
| "patternTransform"
| "patternUnits"
| "pointsAtX"
| "pointsAtY"
| "pointsAtZ"
| "preserveAlpha"
| "preserveAspectRatio"
| "primitiveUnits"
| "referrerPolicy"
| "refX"
| "refY"
| "repeatCount"
| "repeatDur"
| "requiredExtensions"
| "requiredFeatures"
| "specularConstant"
| "specularExponent"
| "spreadMethod"
| "startOffset"
| "stdDeviation"
| "stitchTiles"
| "surfaceScale"
| "systemLanguage"
| "tableValues"
| "targetX"
| "targetY"
| "textLength"
| "viewBox"
| "xChannelSelector"
| "yChannelSelector"
| "zoomAndPan" => name.to_string(),
_ => {
name.to_lowercase()
}
}
}
fn is_void_element(name: &str) -> bool {
matches!(
name,
"area"
| "base"
| "br"
| "col"
| "embed"
| "hr"
| "img"
| "input"
| "link"
| "meta"
| "param"
| "source"
| "track"
| "wbr"
)
}
fn is_boolean_attr(name: &str) -> bool {
matches!(
name,
"allowfullscreen"
| "async"
| "autofocus"
| "autoplay"
| "checked"
| "controls"
| "default"
| "defer"
| "disabled"
| "formnovalidate"
| "inert"
| "ismap"
| "itemscope"
| "loop"
| "multiple"
| "muted"
| "nomodule"
| "novalidate"
| "open"
| "playsinline"
| "readonly"
| "required"
| "reversed"
| "selected"
)
}
fn null_arg() -> ExprOrSpread {
ExprOrSpread {
spread: None,
expr: Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))),
}
}
fn get_attr_name(jsx_attr: &JSXAttr, normalize: bool) -> String {
match &jsx_attr.name {
JSXAttrName::Ident(ident) => {
if !normalize {
ident.sym.to_string()
} else {
normalize_dom_attr_name(ident.sym.as_ref())
}
}
JSXAttrName::JSXNamespacedName(namespace_name) => {
let ns = namespace_name.ns.sym.to_string();
let name = namespace_name.name.sym.to_string();
let combined = format!("{}:{}", ns, name);
normalize_dom_attr_name(&combined)
}
}
}
fn normalize_lit_str(lit_str: &Str) -> Lit {
let value = lit_str.value.to_string_lossy();
let mut replaced = "".to_string();
for (i, line) in value.lines().enumerate() {
if i > 0 {
replaced.push(' ');
}
replaced.push_str(line.trim_start());
}
Lit::Str(Str {
span: lit_str.span,
value: replaced.into(),
raw: None,
})
}
fn jsx_text_to_str(
jsx_text: &JSXText,
escape: bool,
trim_last_child: bool,
) -> String {
let mut text = String::new();
let mut lines = jsx_text.value.lines().enumerate().peekable();
while let Some((i, line)) = lines.next() {
let mut line = if i != 0 { line.trim_start() } else { line };
if lines.peek().is_some() || trim_last_child {
line = line.trim_end();
}
if line.is_empty() {
continue;
}
if i > 0 && !text.is_empty() {
text.push(' ')
}
text.push_str(line);
}
if escape { escape_html(&text) } else { text }
}
fn jsx_member_expr_to_normal(jsx_member_expr: &JSXMemberExpr) -> MemberExpr {
MemberExpr {
span: DUMMY_SP,
obj: match jsx_member_expr.obj.clone() {
JSXObject::Ident(ident) => Box::new(Expr::Ident(ident.clone())),
JSXObject::JSXMemberExpr(member_expr) => {
Box::new(Expr::Member(jsx_member_expr_to_normal(&member_expr)))
}
},
prop: MemberProp::Ident(jsx_member_expr.prop.clone()),
}
}
fn is_serializable(
opening: &JSXOpeningElement,
skip_serialize: &Option<Vec<String>>,
) -> bool {
match opening.name.clone() {
JSXElementName::Ident(ident) => {
let name = ident.sym.to_string();
if name.chars().next().unwrap().is_ascii_uppercase() {
return false;
}
if let Some(skip_elements) = skip_serialize
&& skip_elements.contains(&name)
{
return false;
}
if opening.attrs.is_empty() {
return true;
}
!opening.attrs.iter().any(|attr| match attr {
JSXAttrOrSpread::SpreadElement(_) => true,
JSXAttrOrSpread::JSXAttr(attr) => {
let name = get_attr_name(attr, false);
matches!(name.as_str(), "dangerouslySetInnerHTML")
}
})
}
_ => false,
}
}
fn is_text_valid_identifier(string_value: &str) -> bool {
if string_value.is_empty() {
return false;
}
for (i, c) in string_value.chars().enumerate() {
if (i == 0 && !c.is_ident_start()) || !c.is_ident_part() {
return false;
}
}
true
}
fn string_lit_expr(value: Wtf8Atom) -> Expr {
Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
value,
raw: None,
}))
}
fn escape_html(str: &str) -> String {
str
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('\'', "'")
.replace('"', """)
}
fn serialize_attr(attr_name: &str, value: &str) -> String {
format!(" {}=\"{}\"", escape_html(attr_name), escape_html(value))
}
fn merge_serializable_children(
children: &[JSXElementChild],
skip_serialize: &Option<Vec<String>>,
escape_text_children: bool,
is_parent_serializable: bool,
) -> (Vec<JSXElementChild>, usize, usize) {
let mut text_count = 0;
let mut serializable_count = 0;
let mut buf = String::new();
let mut normalized_children: Vec<JSXElementChild> = vec![];
let mut child_iter = children.iter().peekable();
while let Some(child) = child_iter.next() {
match child {
JSXElementChild::JSXText(jsx_text) => {
let text = jsx_text_to_str(
jsx_text,
escape_text_children,
is_parent_serializable && child_iter.peek().is_none(),
);
if text.is_empty() {
continue;
}
text_count += 1;
buf.push_str(&text);
}
JSXElementChild::JSXExprContainer(jsx_expr_container) => {
match &jsx_expr_container.expr {
JSXExpr::JSXEmptyExpr(_) => {}
JSXExpr::Expr(expr) => {
if let Expr::Lit(lit) = &**expr {
match lit {
Lit::Bool(_) => continue,
Lit::Num(num) => {
let text = num.to_string();
buf.push_str(&text);
continue;
}
Lit::Str(str_lit) => {
buf.push_str(&escape_html(&str_lit.value.to_string_lossy()));
continue;
}
_ => {}
}
}
if !buf.is_empty() {
let atom = Atom::new(buf);
normalized_children.push(JSXElementChild::JSXText(JSXText {
span: DUMMY_SP,
value: atom.clone(),
raw: atom,
}));
buf = String::new()
}
normalized_children.push(child.clone());
}
}
}
JSXElementChild::JSXElement(jsx_el) => {
if !buf.is_empty() {
let atom = Atom::new(buf);
normalized_children.push(JSXElementChild::JSXText(JSXText {
span: DUMMY_SP,
value: atom.clone(),
raw: atom,
}));
buf = String::new()
}
if is_serializable(&jsx_el.opening, skip_serialize) {
serializable_count += 1;
}
normalized_children.push(JSXElementChild::JSXElement(jsx_el.clone()));
}
JSXElementChild::JSXSpreadChild(_) => {}
child => {
if !buf.is_empty() {
let atom = Atom::new(buf);
normalized_children.push(JSXElementChild::JSXText(JSXText {
span: DUMMY_SP,
value: atom.clone(),
raw: atom,
}));
buf = String::new()
}
normalized_children.push(child.clone());
}
}
}
if !buf.is_empty() {
let atom = Atom::new(buf);
normalized_children.push(JSXElementChild::JSXText(JSXText {
span: DUMMY_SP,
value: atom.clone(),
raw: atom,
}));
}
(normalized_children, text_count, serializable_count)
}
impl JsxPrecompile {
fn get_jsx_identifier(&mut self) -> Ident {
match &self.import_jsx {
Some(ident) => ident.clone(),
None => {
let ident = new_ident("_jsx".into());
self.import_jsx = Some(ident.clone());
ident
}
}
}
fn get_jsx_ssr_identifier(&mut self) -> Ident {
match &self.import_jsx_ssr {
Some(ident) => ident.clone(),
None => {
let ident = new_ident("_jsxTemplate".into());
self.import_jsx_ssr = Some(ident.clone());
ident
}
}
}
fn get_jsx_attr_identifier(&mut self) -> Ident {
match &self.import_jsx_attr {
Some(ident) => ident.clone(),
None => {
let ident = new_ident("_jsxAttr".into());
self.import_jsx_attr = Some(ident.clone());
ident
}
}
}
fn get_jsx_escape_fn_identifier(&mut self) -> Ident {
match &self.import_jsx_escape {
Some(ident) => ident.clone(),
None => {
let ident = new_ident("_jsxEscape".into());
self.import_jsx_escape = Some(ident.clone());
ident
}
}
}
fn wrap_with_jsx_escape_call(&mut self, expr: Expr) -> Expr {
let args: Vec<ExprOrSpread> = vec![ExprOrSpread {
spread: None,
expr: Box::new(expr),
}];
Expr::Call(CallExpr {
span: DUMMY_SP,
ctxt: Default::default(),
callee: Callee::Expr(Box::new(Expr::Ident(
self.get_jsx_escape_fn_identifier(),
))),
args,
type_args: None,
})
}
fn maybe_wrap_with_jsx_escape_call(&mut self, expr: Expr) -> Expr {
match &expr {
Expr::Lit(lit_expr) => match lit_expr {
Lit::Num(_) => expr,
Lit::Bool(_) => expr,
Lit::Null(_) => expr,
Lit::Str(string_lit) => {
let escaped_value = escape_html(&string_lit.value.to_string_lossy());
if string_lit.value.to_string_lossy() != escaped_value {
self.wrap_with_jsx_escape_call(expr)
} else {
expr
}
}
_ => expr,
},
_ => self.wrap_with_jsx_escape_call(expr),
}
}
fn serialize_jsx_children_to_expr(
&mut self,
children: &[JSXElementChild],
) -> Option<Expr> {
match children.len() {
0 => None,
1 => {
let child = &children[0];
match child {
JSXElementChild::JSXText(jsx_text) => {
let text = jsx_text_to_str(jsx_text, false, true);
Some(string_lit_expr(text.into()))
}
JSXElementChild::JSXExprContainer(jsx_expr_container) => {
match &jsx_expr_container.expr {
JSXExpr::JSXEmptyExpr(_) => None,
JSXExpr::Expr(expr) => Some(*expr.clone()),
}
}
JSXElementChild::JSXElement(jsx_element) => {
Some(self.serialize_jsx(jsx_element))
}
JSXElementChild::JSXFragment(jsx_frag) => {
self.serialize_jsx_children_to_expr(&jsx_frag.children)
}
JSXElementChild::JSXSpreadChild(_) => None,
}
}
_ => {
let (normalized_children, text_count, serializable_count) =
merge_serializable_children(
children,
&self.skip_serialize,
false,
false,
);
let mut elems: Vec<Option<ExprOrSpread>> = vec![];
if serializable_count > 1 || serializable_count == 1 && text_count > 0 {
self.next_index += 1;
let index = self.next_index;
let mut strings: Vec<String> = vec![];
let mut dynamic_exprs: Vec<Expr> = vec![];
strings.push("".to_string());
self.serialize_jsx_children_to_string(
&normalized_children,
&mut strings,
&mut dynamic_exprs,
false,
);
let expr = self.gen_template(index, strings, dynamic_exprs);
elems.push(Some(ExprOrSpread {
spread: None,
expr: Box::new(expr.clone()),
}));
} else {
for child in normalized_children {
match child {
JSXElementChild::JSXText(jsx_text) => {
elems.push(Some(ExprOrSpread {
spread: None,
expr: Box::new(string_lit_expr(jsx_text.value.into())),
}));
}
JSXElementChild::JSXExprContainer(jsx_expr_container) => {
match jsx_expr_container.expr {
JSXExpr::JSXEmptyExpr(_) => continue,
JSXExpr::Expr(expr) => {
elems.push(Some(ExprOrSpread { spread: None, expr }));
}
}
}
JSXElementChild::JSXElement(jsx_el) => {
let expr = self.serialize_jsx(&jsx_el);
elems.push(Some(ExprOrSpread {
spread: None,
expr: Box::new(expr.clone()),
}));
}
JSXElementChild::JSXFragment(jsx_frag) => {
if let Some(child_expr) =
self.serialize_jsx_children_to_expr(&jsx_frag.children)
{
match child_expr {
Expr::Array(array_lit) => {
elems.extend(array_lit.elems);
}
_ => {
elems.push(Some(ExprOrSpread {
spread: None,
expr: Box::new(child_expr.clone()),
}));
}
};
}
}
JSXElementChild::JSXSpreadChild(_) => {}
}
}
}
if elems.len() == 1
&& let Some(first) = &elems[0]
{
let expr = &*first.expr;
return Some(expr.clone());
}
Some(Expr::Array(ArrayLit {
span: DUMMY_SP,
elems,
}))
}
}
}
fn serialize_jsx_to_call_expr(&mut self, el: &JSXElement) -> CallExpr {
let mut is_component = false;
let name_expr = match &el.opening.name {
JSXElementName::Ident(ident) => {
let name = &ident.sym;
if name.chars().next().unwrap().is_ascii_uppercase() {
is_component = true;
Expr::Ident(ident.clone())
} else {
string_lit_expr(name.clone().into())
}
}
JSXElementName::JSXMemberExpr(jsx_member_expr) => {
is_component = true;
Expr::Member(jsx_member_expr_to_normal(jsx_member_expr))
}
JSXElementName::JSXNamespacedName(namespace_name) => {
let ns = namespace_name.ns.sym.to_string();
let name = namespace_name.name.sym.to_string();
let combined = format!("{}:{}", ns, name);
string_lit_expr(combined.into())
}
};
let mut args: Vec<ExprOrSpread> = vec![];
args.push(ExprOrSpread {
spread: None,
expr: Box::new(name_expr),
});
let mut key_value: Option<Expr> = None;
if el.opening.attrs.is_empty() && el.children.is_empty() {
args.push(null_arg())
} else {
let mut props: Vec<PropOrSpread> = vec![];
for attr in el.opening.attrs.iter() {
match attr {
JSXAttrOrSpread::JSXAttr(jsx_attr) => {
let attr_name = get_attr_name(jsx_attr, !is_component);
let prop_name = if !is_text_valid_identifier(&attr_name) {
PropName::Str(Str {
span: DUMMY_SP,
raw: None,
value: attr_name.clone().into(),
})
} else {
PropName::Ident(quote_ident!(attr_name.clone()))
};
let Some(attr_value) = &jsx_attr.value else {
props.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(
KeyValueProp {
key: prop_name,
value: Box::new(Expr::Lit(Lit::Bool(Bool {
span: DUMMY_SP,
value: true,
}))),
},
))));
continue;
};
if attr_name == "key" {
key_value = match attr_value {
JSXAttrValue::Str(str) => {
let normalized_lit = normalize_lit_str(str);
Some(Expr::Lit(normalized_lit))
}
JSXAttrValue::JSXExprContainer(jsx_expr_container) => {
match &jsx_expr_container.expr {
JSXExpr::JSXEmptyExpr(_) => None,
JSXExpr::Expr(expr) => Some(*expr.clone()),
}
}
_ => None,
};
continue;
}
match attr_value {
JSXAttrValue::Str(str) => {
let normalized_lit = normalize_lit_str(str);
props.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(
KeyValueProp {
key: prop_name,
value: Box::new(Expr::Lit(normalized_lit)),
},
))));
}
JSXAttrValue::JSXExprContainer(jsx_expr_container) => {
match &jsx_expr_container.expr {
JSXExpr::JSXEmptyExpr(_) => continue,
JSXExpr::Expr(expr) => {
props.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(
KeyValueProp {
key: prop_name,
value: expr.clone(),
},
))));
}
}
}
JSXAttrValue::JSXElement(_) => {}
JSXAttrValue::JSXFragment(_) => {}
}
}
JSXAttrOrSpread::SpreadElement(spread_el) => {
props.push(PropOrSpread::Spread(spread_el.clone()));
}
}
}
let child_expr = self.serialize_jsx_children_to_expr(&el.children);
if let Some(expr) = child_expr {
let children_name = PropName::Ident(quote_ident!("children"));
props.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(
KeyValueProp {
key: children_name.clone(),
value: Box::new(expr),
},
))));
}
if props.is_empty() {
args.push(null_arg())
} else {
let obj_expr = Box::new(Expr::Object(ObjectLit {
span: DUMMY_SP,
props,
}));
args.push(ExprOrSpread {
spread: None,
expr: obj_expr,
});
}
}
if let Some(key_expr) = key_value {
args.push(ExprOrSpread {
spread: None,
expr: Box::new(key_expr),
});
}
CallExpr {
span: DUMMY_SP,
ctxt: Default::default(),
callee: Callee::Expr(Box::new(Expr::Ident(self.get_jsx_identifier()))),
args,
type_args: None,
}
}
fn convert_to_jsx_attr_call(
&mut self,
name: Wtf8Atom,
expr: Expr,
) -> CallExpr {
let args = vec![
ExprOrSpread {
spread: None,
expr: Box::new(string_lit_expr(name)),
},
ExprOrSpread {
spread: None,
expr: Box::new(expr),
},
];
CallExpr {
span: DUMMY_SP,
ctxt: Default::default(),
callee: Callee::Expr(Box::new(Expr::Ident(
self.get_jsx_attr_identifier(),
))),
args,
type_args: None,
}
}
fn serialize_jsx_children_to_string(
&mut self,
children: &[JSXElementChild],
strings: &mut Vec<String>,
dynamic_exprs: &mut Vec<Expr>,
is_parent_serializable: bool,
) {
let (normalized_children, _text_count, _serializable_count) =
merge_serializable_children(
children,
&self.skip_serialize,
true,
is_parent_serializable,
);
for child in normalized_children {
match child {
JSXElementChild::JSXText(jsx_text) => {
strings
.last_mut()
.unwrap()
.push_str(jsx_text.value.as_ref());
}
JSXElementChild::JSXExprContainer(jsx_expr_container) => {
match jsx_expr_container.expr {
JSXExpr::JSXEmptyExpr(_) => continue,
JSXExpr::Expr(expr) => {
strings.push("".to_string());
let escaped_expr = self.maybe_wrap_with_jsx_escape_call(*expr);
dynamic_exprs.push(escaped_expr);
}
}
}
JSXElementChild::JSXElement(jsx_element) => self
.serialize_jsx_element_to_string_vec(
&jsx_element,
strings,
dynamic_exprs,
),
JSXElementChild::JSXFragment(jsx_frag) => self
.serialize_jsx_children_to_string(
&jsx_frag.children,
strings,
dynamic_exprs,
false,
),
JSXElementChild::JSXSpreadChild(_) => {}
}
}
}
fn serialize_jsx_element_to_string_vec(
&mut self,
el: &JSXElement,
strings: &mut Vec<String>,
dynamic_exprs: &mut Vec<Expr>,
) {
if !is_serializable(&el.opening, &self.skip_serialize) {
let expr = Expr::Call(self.serialize_jsx_to_call_expr(el));
strings.push("".to_string());
dynamic_exprs.push(expr);
return;
} else if strings.is_empty() {
strings.push("".to_string());
}
let name: &str = match &el.opening.name {
JSXElementName::Ident(ident) => &ident.sym,
_ => {
unreachable!("serialize_jsx_element_to_string_vec(JSXNamespacedName)")
}
};
strings.last_mut().unwrap().push('<');
let escaped_name = escape_html(name);
strings.last_mut().unwrap().push_str(escaped_name.as_str());
for attr in &el.opening.attrs {
match attr {
JSXAttrOrSpread::JSXAttr(jsx_attr) => {
if let Some(skip_prop_serialize) = &self.skip_prop_serialize {
let attr_name = get_attr_name(jsx_attr, false);
if skip_prop_serialize.contains(&attr_name) {
strings.last_mut().unwrap().push(' ');
strings.push("".to_string());
let value = match &jsx_attr.value {
Some(attr_value) => match attr_value.clone() {
JSXAttrValue::Str(lit) => Expr::Lit(Lit::Str(lit)),
JSXAttrValue::JSXExprContainer(_) => todo!(),
JSXAttrValue::JSXElement(jsx_element) => {
Expr::JSXElement(jsx_element)
}
JSXAttrValue::JSXFragment(jsx_frag) => {
Expr::JSXFragment(jsx_frag)
}
},
None => Expr::Lit(Lit::Bool(Bool {
span: DUMMY_SP,
value: true,
})),
};
let expr = self.convert_to_jsx_attr_call(attr_name.into(), value);
dynamic_exprs.push(Expr::Call(expr));
continue;
}
}
let attr_name = get_attr_name(jsx_attr, true);
let Some(attr_value) = &jsx_attr.value else {
strings.last_mut().unwrap().push(' ');
let escaped_attr_name = escape_html(&attr_name);
if is_boolean_attr(&attr_name) {
strings
.last_mut()
.unwrap()
.push_str(escaped_attr_name.as_str());
} else {
strings.push("".to_string());
let expr = self.convert_to_jsx_attr_call(
attr_name.into(),
Expr::Lit(Lit::Bool(Bool {
span: DUMMY_SP,
value: true,
})),
);
dynamic_exprs.push(Expr::Call(expr));
}
continue;
};
match attr_value {
JSXAttrValue::Str(string_lit) => {
if attr_name == "key" || attr_name == "ref" {
strings.last_mut().unwrap().push(' ');
strings.push("".to_string());
let expr = self.convert_to_jsx_attr_call(
attr_name.into(),
string_lit_expr(string_lit.value.clone()),
);
dynamic_exprs.push(Expr::Call(expr));
continue;
}
let serialized_attr =
serialize_attr(&attr_name, &string_lit.value.to_string_lossy());
strings
.last_mut()
.unwrap()
.push_str(serialized_attr.as_str());
}
JSXAttrValue::JSXExprContainer(jsx_expr_container) => {
match &jsx_expr_container.expr {
JSXExpr::JSXEmptyExpr(_) => {}
JSXExpr::Expr(jsx_expr) => {
let expr = *jsx_expr.clone();
match &expr {
Expr::Lit(lit) => match lit {
Lit::Bool(lit_bool) => {
if is_boolean_attr(&attr_name) {
if !lit_bool.value {
continue;
}
strings.last_mut().unwrap().push(' ');
strings.last_mut().unwrap().push_str(&attr_name);
continue;
}
}
Lit::Num(num) => {
let serialized_attr =
serialize_attr(&attr_name, &num.value.to_string());
strings
.last_mut()
.unwrap()
.push_str(serialized_attr.as_str());
continue;
}
Lit::Str(str_lit) => {
let serialized_attr = serialize_attr(
&attr_name,
&str_lit.value.to_string_lossy(),
);
strings
.last_mut()
.unwrap()
.push_str(serialized_attr.as_str());
continue;
}
_ => {}
},
Expr::Unary(unary_expr) => {
if unary_expr.op == UnaryOp::Minus
&& let Expr::Lit(Lit::Num(num_lit)) = &*unary_expr.arg
{
let value = format!("-{}", &num_lit.value);
let serialized_attr =
serialize_attr(&attr_name, &value);
strings
.last_mut()
.unwrap()
.push_str(serialized_attr.as_str());
continue;
};
}
_ => {}
}
strings.last_mut().unwrap().push(' ');
strings.push("".to_string());
if is_boolean_attr(&attr_name) {
let cond_expr = Expr::Cond(CondExpr {
span: DUMMY_SP,
test: Box::new(expr),
cons: Box::new(string_lit_expr(attr_name.into())),
alt: Box::new(string_lit_expr("".into())),
});
dynamic_exprs.push(cond_expr)
} else {
let call_expr =
self.convert_to_jsx_attr_call(attr_name.into(), expr);
dynamic_exprs.push(Expr::Call(call_expr));
}
}
}
}
JSXAttrValue::JSXElement(_) => {}
JSXAttrValue::JSXFragment(_) => {}
}
}
JSXAttrOrSpread::SpreadElement(_) => {}
};
}
strings.last_mut().unwrap().push('>');
if is_void_element(name) {
return;
}
self.serialize_jsx_children_to_string(
&el.children,
strings,
dynamic_exprs,
true,
);
let closing_tag = format!("</{}>", name);
strings.last_mut().unwrap().push_str(closing_tag.as_str());
}
fn gen_template(
&mut self,
template_index: usize,
static_strs: Vec<String>,
dynamic_exprs: Vec<Expr>,
) -> Expr {
let name = create_tpl_binding_name(template_index);
self.templates.push((template_index, static_strs));
let mut args: Vec<ExprOrSpread> =
Vec::with_capacity(1 + dynamic_exprs.len());
args.push(ExprOrSpread {
spread: None,
expr: Box::new(Expr::Ident(new_ident(name.into()))),
});
for dynamic_expr in dynamic_exprs.into_iter() {
args.push(ExprOrSpread {
spread: None,
expr: Box::new(dynamic_expr),
});
}
let jsx_ident = self.get_jsx_ssr_identifier();
Expr::Call(CallExpr {
span: DUMMY_SP,
ctxt: Default::default(),
callee: Callee::Expr(Box::new(Expr::Ident(jsx_ident))),
args,
type_args: Default::default(),
})
}
fn serialize_jsx(&mut self, el: &JSXElement) -> Expr {
if is_serializable(&el.opening, &self.skip_serialize) {
self.next_index += 1;
let index = self.next_index;
let mut static_strs: Vec<String> = vec![];
let mut dynamic_exprs: Vec<Expr> = vec![];
self.serialize_jsx_element_to_string_vec(
el,
&mut static_strs,
&mut dynamic_exprs,
);
self.gen_template(index, static_strs, dynamic_exprs)
} else {
Expr::Call(self.serialize_jsx_to_call_expr(el))
}
}
fn inject_runtime(&mut self, stmts: &mut Vec<ModuleItem>) {
let mut imports: Vec<(Ident, Ident)> = vec![];
if let Some(jsx_ident) = &self.import_jsx {
imports.push((jsx_ident.clone(), new_ident("jsx".into())))
}
if let Some(jsx_ssr_ident) = &self.import_jsx_ssr {
imports.push((jsx_ssr_ident.clone(), new_ident("jsxTemplate".into())))
}
if let Some(jsx_attr_ident) = &self.import_jsx_attr {
imports.push((jsx_attr_ident.clone(), new_ident("jsxAttr".into())))
}
if let Some(espace_ident) = self.import_jsx_escape.take() {
imports.push((espace_ident, new_ident("jsxEscape".into())))
}
if !imports.is_empty() {
let src = format!(
"{}/jsx-runtime",
self.import_source.as_deref().unwrap_or("react")
);
let specifiers = imports
.into_iter()
.map(|(local, imported)| {
ImportSpecifier::Named(ImportNamedSpecifier {
span: DUMMY_SP,
local,
imported: Some(ModuleExportName::Ident(imported)),
is_type_only: false,
})
})
.collect();
prepend_stmt(
stmts,
ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
span: DUMMY_SP,
specifiers,
src: Str {
span: DUMMY_SP,
raw: None,
value: src.into(),
}
.into(),
type_only: Default::default(),
with: Default::default(),
phase: Default::default(),
})),
);
}
}
}
impl VisitMut for JsxPrecompile {
noop_visit_mut_type!();
fn visit_mut_module(&mut self, module: &mut Module) {
module.visit_mut_children_with(self);
let non_mod_stmt_idx = module
.body
.iter()
.position(|stmt| match stmt {
ModuleItem::ModuleDecl(mod_dec) => {
matches!(
mod_dec,
ModuleDecl::ExportDecl(_) | ModuleDecl::ExportDefaultDecl(_)
)
}
ModuleItem::Stmt(stmt) => !matches!(stmt, Stmt::Empty(_)),
})
.unwrap_or(0);
for (idx, strings) in self.templates.iter().rev() {
let elems: Vec<Option<ExprOrSpread>> = strings
.iter()
.map(|el| {
Some(ExprOrSpread {
spread: None,
expr: Box::new(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
value: el.as_str().into(),
raw: None,
}))),
})
})
.collect();
module.body.insert(
non_mod_stmt_idx,
ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl {
span: DUMMY_SP,
ctxt: SyntaxContext::default(),
kind: VarDeclKind::Const,
declare: false,
decls: vec![VarDeclarator {
span: DUMMY_SP,
name: Pat::Ident(BindingIdent {
id: new_ident(create_tpl_binding_name(*idx).into()),
type_ann: None,
}),
init: Some(Box::new(Expr::Array(ArrayLit {
span: DUMMY_SP,
elems,
}))),
definite: false,
}],
})))),
)
}
self.inject_runtime(&mut module.body);
}
fn visit_mut_expr(&mut self, expr: &mut Expr) {
if let Expr::JSXElement(el) = expr {
*expr = self.serialize_jsx(el);
} else if let Expr::JSXFragment(frag) = expr {
match frag.children.len() {
0 => {
*expr = Expr::Lit(Lit::Null(Null { span: frag.span }));
}
1 => {
let child = &frag.children[0];
match child {
JSXElementChild::JSXText(_)
| JSXElementChild::JSXExprContainer(_) => {
self.next_index += 1;
let index = self.next_index;
let mut strings: Vec<String> = vec![];
let mut dynamic_exprs: Vec<Expr> = vec![];
strings.push("".to_string());
self.serialize_jsx_children_to_string(
&frag.children,
&mut strings,
&mut dynamic_exprs,
false,
);
*expr = self.gen_template(index, strings, dynamic_exprs)
}
JSXElementChild::JSXElement(jsx_element) => {
*expr = self.serialize_jsx(jsx_element);
}
JSXElementChild::JSXFragment(jsx_frag) => {
let serialized =
self.serialize_jsx_children_to_expr(&jsx_frag.children);
if let Some(serialized_expr) = serialized {
*expr = serialized_expr
}
}
JSXElementChild::JSXSpreadChild(_) => {}
}
}
_ => {
self.next_index += 1;
let index = self.next_index;
let mut strings: Vec<String> = vec![];
let mut dynamic_exprs: Vec<Expr> = vec![];
strings.push("".to_string());
self.serialize_jsx_children_to_string(
&frag.children,
&mut strings,
&mut dynamic_exprs,
false,
);
*expr = self.gen_template(index, strings, dynamic_exprs)
}
}
}
expr.visit_mut_children_with(self);
}
}
fn new_ident(name: Atom) -> Ident {
Ident::new(name, DUMMY_SP, SyntaxContext::default())
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::EmitOptions;
use crate::ModuleSpecifier;
use crate::SourceMap;
use crate::swc::parser::Parser;
use crate::swc::parser::StringInput;
use crate::swc::parser::Syntax;
use crate::swc::parser::TsSyntax;
use pretty_assertions::assert_eq;
use swc_common::comments::SingleThreadedComments;
use swc_ecma_visit::visit_mut_pass;
use super::*;
#[test]
fn basic_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <div>Hello!</div>;
const b = <div>Hello {name}!</div>;
const c = <button class="btn" onClick={onClick}>Hello {name}!</button>;
"#,
r#"import { jsxTemplate as _jsxTemplate, jsxAttr as _jsxAttr, jsxEscape as _jsxEscape } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div>Hello!</div>"
];
const $$_tpl_2 = [
"<div>Hello ",
"!</div>"
];
const $$_tpl_3 = [
'<button class="btn" ',
">Hello ",
"!</button>"
];
const a = _jsxTemplate($$_tpl_1);
const b = _jsxTemplate($$_tpl_2, _jsxEscape(name));
const c = _jsxTemplate($$_tpl_3, _jsxAttr("onclick", onClick), _jsxEscape(name));"#,
);
}
#[test]
fn convert_self_closing_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <div />;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div></div>"
];
const a = _jsxTemplate($$_tpl_1);"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <br></br>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<br>"
];
const a = _jsxTemplate($$_tpl_1);"#,
);
}
#[test]
fn normalize_attr_name_test() {
let mappings: Vec<(String, String)> = vec![
("htmlFor".to_string(), "for".to_string()),
("className".to_string(), "class".to_string()),
("xlinkRole".to_string(), "xlink:role".to_string()),
("acceptCharset".to_string(), "accept-charset".to_string()),
("onFoo".to_string(), "onfoo".to_string()),
];
for mapping in mappings.iter() {
test_transform(
JsxPrecompile::default(),
format!("const a = <label {}=\"foo\">label</label>", &mapping.0)
.as_str(),
format!(
"{}\nconst $$_tpl_1 = [\n '<label {}=\"foo\">label</label>'\n];\nconst a = _jsxTemplate($$_tpl_1);",
"import { jsxTemplate as _jsxTemplate } from \"react/jsx-runtime\";",
&mapping.1
)
.as_str(),
);
let quoted = if mapping.1.contains('-') || mapping.1.contains(':') {
format!("\"{}\"", &mapping.1)
} else {
mapping.1.clone()
};
test_transform(
JsxPrecompile::default(),
format!("const a = <label {}=\"foo\" {{...foo}} />", &mapping.0)
.as_str(),
format!(
"{}\nconst a = _jsx(\"label\", {{\n {}: \"foo\",\n ...foo\n}});",
"import { jsx as _jsx } from \"react/jsx-runtime\";", quoted
)
.as_str(),
);
}
for mapping in mappings.iter() {
test_transform(
JsxPrecompile::default(),
format!("const a = <Foo {}=\"foo\">foo</Foo>", &mapping.0).as_str(),
format!(
"{}\nconst a = _jsx(Foo, {{\n {}: \"foo\",\n children: \"foo\"\n}});",
"import { jsx as _jsx } from \"react/jsx-runtime\";",
&mapping.0
)
.as_str(),
);
}
}
#[test]
fn boolean_attr_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <input type="checkbox" checked />;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
'<input type="checkbox" checked>'
];
const a = _jsxTemplate($$_tpl_1);"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <input type="checkbox" checked={false} required={true} selected={foo} />;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
'<input type="checkbox" required ',
">"
];
const a = _jsxTemplate($$_tpl_1, foo ? "selected" : "");"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <div f-client-nav />;"#,
r#"import { jsxTemplate as _jsxTemplate, jsxAttr as _jsxAttr } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div ",
"></div>"
];
const a = _jsxTemplate($$_tpl_1, _jsxAttr("f-client-nav", true));"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <div f-client-nav={false} />;"#,
r#"import { jsxTemplate as _jsxTemplate, jsxAttr as _jsxAttr } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div ",
"></div>"
];
const a = _jsxTemplate($$_tpl_1, _jsxAttr("f-client-nav", false));"#,
);
}
#[test]
fn dynamic_attr_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <div class="foo" bar={2 + 2}></div>;"#,
r#"import { jsxTemplate as _jsxTemplate, jsxAttr as _jsxAttr } from "react/jsx-runtime";
const $$_tpl_1 = [
'<div class="foo" ',
"></div>"
];
const a = _jsxTemplate($$_tpl_1, _jsxAttr("bar", 2 + 2));"#,
);
}
#[test]
fn namespace_attr_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <a xlink:href="foo">foo</a>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
'<a href="foo">foo</a>'
];
const a = _jsxTemplate($$_tpl_1);"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <a foo:bar="foo">foo</a>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
'<a foo:bar="foo">foo</a>'
];
const a = _jsxTemplate($$_tpl_1);"#,
);
}
#[test]
fn mixed_static_dynamic_props_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <div foo="1" {...props} bar="2">foo</div>;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx("div", {
foo: "1",
...props,
bar: "2",
children: "foo"
});"#,
);
}
#[test]
fn non_identiifer_attr_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo aria-label="bar" {...props} />;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(Foo, {
"aria-label": "bar",
...props
});"#,
);
}
#[test]
fn dangerously_html_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <div dangerouslySetInnerHTML={{__html: "foo"}}>foo</div>;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx("div", {
dangerouslySetInnerHTML: {
__html: "foo"
},
children: "foo"
});"#,
);
}
#[test]
fn key_attr_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <div key="foo">foo</div>;"#,
r#"import { jsxTemplate as _jsxTemplate, jsxAttr as _jsxAttr } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div ",
">foo</div>"
];
const a = _jsxTemplate($$_tpl_1, _jsxAttr("key", "foo"));"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <div key={foo}>foo</div>;"#,
r#"import { jsxTemplate as _jsxTemplate, jsxAttr as _jsxAttr } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div ",
">foo</div>"
];
const a = _jsxTemplate($$_tpl_1, _jsxAttr("key", foo));"#,
);
}
#[test]
fn key_attr_comp_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo key="foo" />;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(Foo, null, "foo");"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo key={2} />;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(Foo, null, 2);"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo key={2}>foo</Foo>;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(Foo, {
children: "foo"
}, 2);"#,
);
}
#[test]
fn ref_attr_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <div ref="foo">foo</div>;"#,
r#"import { jsxTemplate as _jsxTemplate, jsxAttr as _jsxAttr } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div ",
">foo</div>"
];
const a = _jsxTemplate($$_tpl_1, _jsxAttr("ref", "foo"));"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <div ref={bar}>foo</div>;"#,
r#"import { jsxTemplate as _jsxTemplate, jsxAttr as _jsxAttr } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div ",
">foo</div>"
];
const a = _jsxTemplate($$_tpl_1, _jsxAttr("ref", bar));"#,
);
}
#[test]
fn serialize_lit_attr_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <img width={100} />;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
'<img width="100">'
];
const a = _jsxTemplate($$_tpl_1);"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <div tabIndex={-1} />;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
'<div tabindex="-1"></div>'
];
const a = _jsxTemplate($$_tpl_1);"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <div foo={"b&>'\"ar"} bar={'baz'} />;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
'<div foo="b&>'"ar" bar="baz"></div>'
];
const a = _jsxTemplate($$_tpl_1);"#,
);
}
#[test]
fn escape_attr_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <div class="a&<>'">foo</div>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
'<div class="a&<>'">foo</div>'
];
const a = _jsxTemplate($$_tpl_1);"#,
);
}
#[test]
fn escape_children_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <div>"a&>'</div>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div>"a&>'</div>"
];
const a = _jsxTemplate($$_tpl_1);"#,
);
test_transform(
JsxPrecompile::default(),
r#"const child = [`"a&>'`].join("");
const a = <div>{child}</div>;"#,
r#"import { jsxTemplate as _jsxTemplate, jsxEscape as _jsxEscape } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div>",
"</div>"
];
const child = [
`"a&>'`
].join("");
const a = _jsxTemplate($$_tpl_1, _jsxEscape(child));"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <div>{foo}{bar}</div>;"#,
r#"import { jsxTemplate as _jsxTemplate, jsxEscape as _jsxEscape } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div>",
"",
"</div>"
];
const a = _jsxTemplate($$_tpl_1, _jsxEscape(foo), _jsxEscape(bar));"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <div>{"\"a&>'"}</div>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div>"a&>'</div>"
];
const a = _jsxTemplate($$_tpl_1);"#,
);
}
#[test]
fn namespace_name_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <a:b>foo</a:b>;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx("a:b", {
children: "foo"
});"#,
);
}
#[test]
fn empty_jsx_child_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <p>{}</p>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<p></p>"
];
const a = _jsxTemplate($$_tpl_1);"#,
);
}
#[test]
fn empty_jsx_text_children_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <p>
foo
</p>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<p>foo</p>"
];
const a = _jsxTemplate($$_tpl_1);"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <p>
foo
bar
</p>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<p>foo bar</p>"
];
const a = _jsxTemplate($$_tpl_1);"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <p>
<span />
</p>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<p><span></span></p>"
];
const a = _jsxTemplate($$_tpl_1);"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo>
<span />
</Foo>;"#,
r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<span></span>"
];
const a = _jsx(Foo, {
children: _jsxTemplate($$_tpl_1)
});"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo>
foo
</Foo>;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(Foo, {
children: "foo"
});"#,
);
}
#[test]
fn child_expr_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <p>{2 + 2}</p>;"#,
r#"import { jsxTemplate as _jsxTemplate, jsxEscape as _jsxEscape } from "react/jsx-runtime";
const $$_tpl_1 = [
"<p>",
"</p>"
];
const a = _jsxTemplate($$_tpl_1, _jsxEscape(2 + 2));"#,
);
}
#[test]
fn empty_fragment_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <></>;"#,
r#"const a = null;"#,
);
}
#[test]
fn fragment_expr_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <>{"foo"}</>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"foo"
];
const a = _jsxTemplate($$_tpl_1);"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <>&'"</>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"&'""
];
const a = _jsxTemplate($$_tpl_1);"#,
);
}
#[test]
fn fragment_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <>foo</>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"foo"
];
const a = _jsxTemplate($$_tpl_1);"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <>&'"</>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"&'""
];
const a = _jsxTemplate($$_tpl_1);"#,
);
}
#[test]
fn fragment_nested_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <><>foo</></>;"#,
r#"const a = "foo";"#,
);
}
#[test]
fn text_indent_test() {
test_transform(
JsxPrecompile::default(),
r#"const result = <div>
foo
bar
</div>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div>foo bar</div>"
];
const result = _jsxTemplate($$_tpl_1);"#,
);
test_transform(
JsxPrecompile::default(),
r#"const result = <div>
foo
bar
</div>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div>foo bar</div>"
];
const result = _jsxTemplate($$_tpl_1);"#,
);
}
#[test]
fn fragment_mulitple_children_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <>foo<div /><Foo /></>;"#,
r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"foo<div></div>",
""
];
const a = _jsxTemplate($$_tpl_1, _jsx(Foo, null));"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <>{foo}<Foo /></>;"#,
r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate, jsxEscape as _jsxEscape } from "react/jsx-runtime";
const $$_tpl_1 = [
"",
"",
""
];
const a = _jsxTemplate($$_tpl_1, _jsxEscape(foo), _jsx(Foo, null));"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <>{foo}</>;"#,
r#"import { jsxTemplate as _jsxTemplate, jsxEscape as _jsxEscape } from "react/jsx-runtime";
const $$_tpl_1 = [
"",
""
];
const a = _jsxTemplate($$_tpl_1, _jsxEscape(foo));"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <>{foo}{bar}</>;"#,
r#"import { jsxTemplate as _jsxTemplate, jsxEscape as _jsxEscape } from "react/jsx-runtime";
const $$_tpl_1 = [
"",
"",
""
];
const a = _jsxTemplate($$_tpl_1, _jsxEscape(foo), _jsxEscape(bar));"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo><div /><><>foo</><span /></></Foo>;"#,
r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div></div>"
];
const $$_tpl_2 = [
"<span></span>"
];
const a = _jsx(Foo, {
children: [
_jsxTemplate($$_tpl_1),
"foo",
_jsxTemplate($$_tpl_2)
]
});"#,
);
}
#[test]
fn fragment_escape_test() {
test_transform(
JsxPrecompile::default(),
r#"const Component = (props: any) => <>{props.children}</>;
const jsx1 = (
<Component>
"test"
<span>test</span>
</Component>
);
const jsx2 = (
<Component>
"test"
</Component>
);"#,
r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate, jsxEscape as _jsxEscape } from "react/jsx-runtime";
const $$_tpl_1 = [
"",
""
];
const $$_tpl_2 = [
""test"<span>test</span>"
];
const Component = (props: any)=>_jsxTemplate($$_tpl_1, _jsxEscape(props.children));
const jsx1 = (_jsx(Component, {
children: _jsxTemplate($$_tpl_2)
}));
const jsx2 = (_jsx(Component, {
children: '"test"'
}));"#,
)
}
#[test]
fn nested_elements_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <div>foo<p>bar</p></div>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div>foo<p>bar</p></div>"
];
const a = _jsxTemplate($$_tpl_1);"#,
);
}
#[test]
fn prop_spread_without_children_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <div {...props} />;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx("div", {
...props
});"#,
);
}
#[test]
fn prop_spread_with_children_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <div {...props}>hello</div>;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx("div", {
...props,
children: "hello"
});"#,
);
}
#[test]
fn prop_spread_with_other_attrs_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <div foo="1" {...props} bar="2">hello</div>;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx("div", {
foo: "1",
...props,
bar: "2",
children: "hello"
});"#,
);
}
#[test]
fn component_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <div><Foo /></div>;"#,
r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div>",
"</div>"
];
const a = _jsxTemplate($$_tpl_1, _jsx(Foo, null));"#,
);
}
#[test]
fn component_outer_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo />;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(Foo, null);"#,
);
}
#[test]
fn component_with_props_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo required foo="1" bar={2} />;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(Foo, {
required: true,
foo: "1",
bar: 2
});"#,
);
}
#[test]
fn component_with_spread_props_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo {...props} foo="1" />;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(Foo, {
...props,
foo: "1"
});"#,
);
}
#[test]
fn component_with_children_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo>bar</Foo>;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(Foo, {
children: "bar"
});"#,
);
}
#[test]
fn component_with_children_jsx_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo><span>hello</span></Foo>;"#,
r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<span>hello</span>"
];
const a = _jsx(Foo, {
children: _jsxTemplate($$_tpl_1)
});"#,
);
}
#[test]
fn component_with_multiple_children_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo><span>hello</span>foo<Bar />asdf</Foo>;"#,
r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<span>hello</span>foo",
"asdf"
];
const a = _jsx(Foo, {
children: _jsxTemplate($$_tpl_1, _jsx(Bar, null))
});"#,
);
}
#[test]
fn component_with_multiple_children_2_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo><span>hello</span>foo<Bar><p>asdf</p></Bar></Foo>;"#,
r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_2 = [
"<p>asdf</p>"
];
const $$_tpl_1 = [
"<span>hello</span>foo",
""
];
const a = _jsx(Foo, {
children: _jsxTemplate($$_tpl_1, _jsx(Bar, {
children: _jsxTemplate($$_tpl_2)
}))
});"#,
);
}
#[test]
fn component_child_expr_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo>{2 + 2}</Foo>;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(Foo, {
children: 2 + 2
});"#,
);
}
#[test]
fn component_with_jsx_attr_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo bar={<div>hello</div>} />;"#,
r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div>hello</div>"
];
const a = _jsx(Foo, {
bar: _jsxTemplate($$_tpl_1)
});"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo bar={<Bar>hello</Bar>} />;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(Foo, {
bar: _jsx(Bar, {
children: "hello"
})
});"#,
);
}
#[test]
fn component_with_jsx_frag_attr_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo bar={<>foo</>} />;"#,
r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"foo"
];
const a = _jsx(Foo, {
bar: _jsxTemplate($$_tpl_1)
});"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo bar={<>foo<Foo/>bar</>} />;"#,
r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"foo",
"bar"
];
const a = _jsx(Foo, {
bar: _jsxTemplate($$_tpl_1, _jsx(Foo, null))
});"#,
);
}
#[test]
fn component_with_nested_frag_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo><>foo<Bar><></></Bar></></Foo>;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(Foo, {
children: [
"foo",
_jsx(Bar, null)
]
});"#,
);
}
#[test]
fn component_with_jsx_member_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <ctx.Provider value={null} />;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(ctx.Provider, {
value: null
});"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <a.b.c.d value={null} />;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(a.b.c.d, {
value: null
});"#,
);
}
#[test]
fn component_prop_casing_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo someCasing={2} />;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(Foo, {
someCasing: 2
});"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <MyIsland.Foo someCasing={2} />;"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(MyIsland.Foo, {
someCasing: 2
});"#,
);
}
#[test]
fn import_source_option_test() {
test_transform(
JsxPrecompile::new(Some("foobar".to_string()), None, None),
r#"const a = <div>foo</div>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "foobar/jsx-runtime";
const $$_tpl_1 = [
"<div>foo</div>"
];
const a = _jsxTemplate($$_tpl_1);"#,
);
}
#[test]
fn template_index_test() {
test_transform(
JsxPrecompile::default(),
r#"<div><Foo><span /></Foo></div>;"#,
r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_2 = [
"<span></span>"
];
const $$_tpl_1 = [
"<div>",
"</div>"
];
_jsxTemplate($$_tpl_1, _jsx(Foo, {
children: _jsxTemplate($$_tpl_2)
}));"#,
);
}
#[test]
fn multi_jsx_string_line_to_jsx_call_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo
key="Register a module with the third party
registry."
description="Register a module with the third party
registry."
/>
"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(Foo, {
description: "Register a module with the third party registry."
}, "Register a module with the third party registry.");"#,
);
}
#[test]
fn insert_tpl_after_imports_test() {
test_transform(
JsxPrecompile::default(),
r#"import Foo from "./foo.ts";
import Bar from "./bar.ts";
const a = <div />"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
import Foo from "./foo.ts";
import Bar from "./bar.ts";
const $$_tpl_1 = [
"<div></div>"
];
const a = _jsxTemplate($$_tpl_1);"#,
);
test_transform(
JsxPrecompile::default(),
r#"import Foo from "./foo.ts";
export function foo() {
return <div />
}"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
import Foo from "./foo.ts";
const $$_tpl_1 = [
"<div></div>"
];
export function foo() {
return _jsxTemplate($$_tpl_1);
}"#,
);
test_transform(
JsxPrecompile::default(),
r#"import Foo from "./foo.ts";
export default function foo() {
return <div />
}"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
import Foo from "./foo.ts";
const $$_tpl_1 = [
"<div></div>"
];
export default function foo() {
return _jsxTemplate($$_tpl_1);
}"#,
);
}
#[test]
fn merge_component_text_children_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo>foo{" "}bar{' '}</Foo>"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(Foo, {
children: "foo bar "
});"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo>foo{2}bar{true}{false}baz</Foo>"#,
r#"import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx(Foo, {
children: "foo2barbaz"
});"#,
);
test_transform(
JsxPrecompile::default(),
r#"const a = <Foo>foo<div />bar{" "}</Foo>"#,
r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"foo<div></div>bar "
];
const a = _jsx(Foo, {
children: _jsxTemplate($$_tpl_1)
});"#,
);
}
#[test]
fn merge_element_text_children_test() {
test_transform(
JsxPrecompile::default(),
r#"const a = <div>foo{" "}bar{' '}</div>"#,
r#"import { jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div>foo bar </div>"
];
const a = _jsxTemplate($$_tpl_1);"#,
);
}
#[test]
fn skip_serialization_test() {
test_transform(
JsxPrecompile::new(
None,
Some(vec!["a".to_string(), "img".to_string()]),
None,
),
r#"const a = <div><img src="foo.jpg"/><a href="\#">foo</a></div>"#,
r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div>",
"",
"</div>"
];
const a = _jsxTemplate($$_tpl_1, _jsx("img", {
src: "foo.jpg"
}), _jsx("a", {
href: "\\#",
children: "foo"
}));"#,
);
}
#[test]
fn skip_prop_serialization_test() {
test_transform(
JsxPrecompile::new(
None,
None,
Some(vec!["class".to_string(), "className".to_string()]),
),
r#"const a = <div class="foo"><img id="foo" className="foo" /></div>"#,
r#"import { jsxTemplate as _jsxTemplate, jsxAttr as _jsxAttr } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div ",
'><img id="foo" ',
"></div>"
];
const a = _jsxTemplate($$_tpl_1, _jsxAttr("class", "foo"), _jsxAttr("className", "foo"));"#,
);
}
#[test]
fn attr_casing_test() {
let values = HashMap::from([
("accentHeight", "accent-height"),
("acceptCharset", "accept-charset"),
("alignmentBaseline", "alignment-baseline"),
("allowReorder", "allowReorder"),
("arabicForm", "arabic-form"),
("attributeName", "attributeName"),
("attributeType", "attributeType"),
("baseFrequency", "baseFrequency"),
("baselineShift", "baseline-shift"),
("baseProfile", "baseProfile"),
("calcMode", "calcMode"),
("capHeight", "cap-height"),
("className", "class"),
("clipPath", "clip-path"),
("clipPathUnits", "clipPathUnits"),
("clipRule", "clip-rule"),
("colorInterpolation", "color-interpolation"),
("colorInterpolationFilters", "color-interpolation-filters"),
("colorProfile", "color-profile"),
("colorRendering", "color-rendering"),
("contentScriptType", "content-script-type"),
("contentStyleType", "content-style-type"),
("diffuseConstant", "diffuseConstant"),
("dominantBaseline", "dominant-baseline"),
("edgeMode", "edgeMode"),
("enableBackground", "enable-background"),
("fillOpacity", "fill-opacity"),
("fillRule", "fill-rule"),
("filterUnits", "filterUnits"),
("floodColor", "flood-color"),
("floodOpacity", "flood-opacity"),
("fontFamily", "font-family"),
("fontSize", "font-size"),
("fontSizeAdjust", "font-size-adjust"),
("fontStretch", "font-stretch"),
("fontStyle", "font-style"),
("fontVariant", "font-variant"),
("fontWeight", "font-weight"),
("glyphName", "glyph-name"),
("glyphOrientationHorizontal", "glyph-orientation-horizontal"),
("glyphOrientationVertical", "glyph-orientation-vertical"),
("glyphRef", "glyphRef"),
("gradientTransform", "gradientTransform"),
("gradientUnits", "gradientUnits"),
("horizAdvX", "horiz-adv-x"),
("horizOriginX", "horiz-origin-x"),
("horizOriginY", "horiz-origin-y"),
("htmlFor", "for"),
("httpEquiv", "http-equiv"),
("imageRendering", "image-rendering"),
("kernelMatrix", "kernelMatrix"),
("kernelUnitLength", "kernelUnitLength"),
("keyPoints", "keyPoints"),
("keySplines", "keySplines"),
("keyTimes", "keyTimes"),
("lengthAdjust", "lengthAdjust"),
("letterSpacing", "letter-spacing"),
("lightingColor", "lighting-color"),
("limitingConeAngle", "limitingConeAngle"),
("markerEnd", "marker-end"),
("markerHeight", "markerHeight"),
("markerMid", "marker-mid"),
("markerStart", "marker-start"),
("markerUnits", "markerUnits"),
("markerWidth", "markerWidth"),
("maskContentUnits", "maskContentUnits"),
("maskUnits", "maskUnits"),
("numOctaves", "numOctaves"),
("overlinePosition", "overline-position"),
("overlineThickness", "overline-thickness"),
("paintOrder", "paint-order"),
("panose1", "panose-1"),
("pathLength", "pathLength"),
("patternContentUnits", "patternContentUnits"),
("patternTransform", "patternTransform"),
("patternUnits", "patternUnits"),
("pointsAtX", "pointsAtX"),
("pointsAtY", "pointsAtY"),
("pointsAtZ", "pointsAtZ"),
("pointerEvents", "pointer-events"),
("preserveAlpha", "preserveAlpha"),
("preserveAspectRatio", "preserveAspectRatio"),
("primitiveUnits", "primitiveUnits"),
("referrerPolicy", "referrerPolicy"),
("refX", "refX"),
("refY", "refY"),
("renderingIntent", "rendering-intent"),
("repeatCount", "repeatCount"),
("repeatDur", "repeatDur"),
("requiredExtensions", "requiredExtensions"),
("requiredFeatures", "requiredFeatures"),
("shapeRendering", "shape-rendering"),
("specularConstant", "specularConstant"),
("specularExponent", "specularExponent"),
("spreadMethod", "spreadMethod"),
("startOffset", "startOffset"),
("stdDeviation", "stdDeviation"),
("stitchTiles", "stitchTiles"),
("stopColor", "stop-color"),
("stopOpacity", "stop-opacity"),
("strikethroughPosition", "strikethrough-position"),
("strikethroughThickness", "strikethrough-thickness"),
("strokeDasharray", "stroke-dasharray"),
("strokeDashoffset", "stroke-dashoffset"),
("strokeLinecap", "stroke-linecap"),
("strokeLinejoin", "stroke-linejoin"),
("strokeMiterlimit", "stroke-miterlimit"),
("strokeOpacity", "stroke-opacity"),
("strokeWidth", "stroke-width"),
("surfaceScale", "surfaceScale"),
("systemLanguage", "systemLanguage"),
("tableValues", "tableValues"),
("targetX", "targetX"),
("targetY", "targetY"),
("textAnchor", "text-anchor"),
("textDecoration", "text-decoration"),
("textLength", "textLength"),
("textRendering", "text-rendering"),
("transformOrigin", "transform-origin"),
("underlinePosition", "underline-position"),
("underlineThickness", "underline-thickness"),
("unicodeBidi", "unicode-bidi"),
("unicodeRange", "unicode-range"),
("unitsPerEm", "units-per-em"),
("vAlphabetic", "v-alphabetic"),
("viewBox", "viewBox"),
("vectorEffect", "vector-effect"),
("vertAdvY", "vert-adv-y"),
("vertOriginX", "vert-origin-x"),
("vertOriginY", "vert-origin-y"),
("vHanging", "v-hanging"),
("vMathematical", "v-mathematical"),
("wordSpacing", "word-spacing"),
("writingMode", "writing-mode"),
("xChannelSelector", "xChannelSelector"),
("xHeight", "x-height"),
("xlinkActuate", "xlink:actuate"),
("xlinkArcrole", "xlink:arcrole"),
("xlinkHref", "href"),
("xlink:href", "href"),
("xlinkRole", "xlink:role"),
("xlinkShow", "xlink:show"),
("xlinkTitle", "xlink:title"),
("xlinkType", "xlink:type"),
("xmlBase", "xml:base"),
("xmlLang", "xml:lang"),
("xmlSpace", "xml:space"),
("yChannelSelector", "yChannelSelector"),
("zoomAndPan", "zoomAndPan"),
]);
for (key, value) in values.into_iter() {
let input = format!("const a = <div {}=\"foo\" />", key);
let expected = [
"import { jsxTemplate as _jsxTemplate } from \"react/jsx-runtime\";",
"const $$_tpl_1 = [",
&format!(" '<div {}=\"foo\"></div>'", value),
"];",
"const a = _jsxTemplate($$_tpl_1);",
]
.join("\n");
test_transform(JsxPrecompile::new(None, None, None), &input, &expected);
}
}
#[track_caller]
fn test_transform(
transform: impl VisitMut,
src: &str,
expected_output: &str,
) {
let (source_map, program) = parse(src);
let mut transform_folder = visit_mut_pass(transform);
let output = print(&source_map, &program.apply(&mut transform_folder));
assert_eq!(output, format!("{}\n", expected_output));
}
fn parse(src: &str) -> (SourceMap, Program) {
let source_map = SourceMap::default();
let source_file = source_map.new_source_file(
ModuleSpecifier::parse("file:///test.ts").unwrap(),
src.to_string(),
);
let input = StringInput::from(&*source_file);
let syntax = Syntax::Typescript(TsSyntax {
tsx: true,
..Default::default()
});
let mut parser = Parser::new(syntax, input, None);
(source_map, Program::Module(parser.parse_module().unwrap()))
}
fn print(source_map: &SourceMap, program: &Program) -> String {
crate::emit::emit(
program.into(),
&SingleThreadedComments::default(),
source_map,
&EmitOptions {
source_map: crate::SourceMapOption::None,
..Default::default()
},
)
.unwrap()
.text
}
}