swc_plugin_inferno/jsx/
mod.rs

1#![allow(clippy::redundant_allocation)]
2
3use serde::{Deserialize, Serialize};
4use std::{borrow::Cow, sync::Arc};
5use swc_config::merge::Merge;
6use swc_core::common::comments::Comments;
7use swc_core::common::iter::IdentifyLast;
8use swc_core::common::util::take::Take;
9use swc_core::common::{FileName, Mark, SourceMap, Span, Spanned, DUMMY_SP, SyntaxContext};
10use swc_core::ecma::ast::*;
11use swc_core::ecma::atoms::{Atom};
12use swc_core::ecma::utils::{drop_span, prepend_stmt, quote_ident, ExprFactory, StmtLike};
13use swc_core::ecma::visit::{noop_visit_mut_type, visit_mut_pass, VisitMut, VisitMutWith};
14use swc_core::plugin::errors::HANDLER;
15use swc_ecma_parser::{parse_file_as_expr, Syntax};
16use crate::VNodeType::Component;
17use crate::{
18    inferno_flags::{ChildFlags, VNodeFlags},
19    refresh::options::{deserialize_refresh, RefreshOptions},
20};
21use crate::transformations::transform_attribute::transform_attribute;
22use crate::transformations::lowercase_attrs::requires_lowercasing;
23use crate::transformations::parse_vnode_flag::parse_vnode_flag;
24
25#[cfg(test)]
26mod tests;
27
28#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq, Merge)]
29#[serde(rename_all = "camelCase")]
30#[serde(deny_unknown_fields)]
31pub struct Options {
32    /// If this is `true`, swc will behave just like babel 8 with
33    /// `BABEL_8_BREAKING: true`.
34    #[serde(skip, default)]
35    pub next: Option<bool>,
36
37    #[serde(default)]
38    pub import_source: Option<String>,
39
40    #[serde(default)]
41    pub development: Option<bool>,
42
43    #[serde(default, deserialize_with = "deserialize_refresh")]
44    // default to disabled since this is still considered as experimental by now
45    pub refresh: Option<RefreshOptions>,
46}
47
48pub fn default_import_source() -> String {
49    "inferno".into()
50}
51
52pub fn parse_expr_for_jsx(
53    cm: &SourceMap,
54    name: &str,
55    src: String,
56    top_level_mark: Mark,
57) -> Arc<Box<Expr>> {
58    let fm = cm.new_source_file(
59        FileName::Custom(format!("<jsx-config-{}.js>", name)).into(),
60        src
61    );
62
63    parse_file_as_expr(
64        &fm,
65        Syntax::default(),
66        Default::default(),
67        None,
68        &mut vec![],
69    )
70    .map_err(|e| {
71        HANDLER.with(|h| {
72            e.into_diagnostic(h)
73                .note("error detected while parsing option for classic jsx transform")
74                .emit()
75        })
76    })
77    .map(drop_span)
78    .map(|mut expr| {
79        apply_mark(&mut expr, top_level_mark);
80        expr
81    })
82    .map(Arc::new)
83    .unwrap_or_else(|()| {
84        panic!(
85            "failed to parse jsx option {}: '{}' is not an expression",
86            name, fm.src,
87        )
88    })
89}
90
91fn apply_mark(e: &mut Expr, mark: Mark) {
92    match e {
93        Expr::Ident(i) => {
94            i.ctxt = i.ctxt.apply_mark(mark);
95        }
96        Expr::Member(MemberExpr { obj, .. }) => {
97            apply_mark(obj, mark);
98        }
99        _ => {}
100    }
101}
102
103fn named_import_exists(import_name: &str, import: &ImportDecl) -> bool {
104    for specifier in &import.specifiers {
105        match specifier {
106            ImportSpecifier::Named(named) => {
107                if import_name == named.local.sym.as_ref() {
108                    return true;
109                }
110            }
111            _ => {
112                continue;
113            }
114        }
115    }
116
117    false
118}
119
120fn merge_imports(
121    imports: &Vec<&str>,
122    default_import_src: &str,
123    stmts: &mut Vec<ModuleItem>,
124) -> bool {
125    for stmt in stmts {
126        if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = stmt {
127            if import.src.value == default_import_src {
128                for specifier in &import.specifiers {
129                    if let ImportSpecifier::Namespace(_) = specifier {
130                        // Do not try to merge with * As FooBar import statements
131                        return false;
132                    }
133                }
134
135                for import_to_add in imports {
136                    let import_exists = named_import_exists(import_to_add, import);
137
138                    if !import_exists {
139                        import
140                            .specifiers
141                            .push(ImportSpecifier::Named(ImportNamedSpecifier {
142                                span: DUMMY_SP,
143                                local: quote_ident!(*import_to_add).into(),
144                                imported: None,
145                                is_type_only: false,
146                            }))
147                    }
148                }
149
150                return true;
151            }
152        }
153    }
154
155    false
156}
157
158#[derive(PartialEq)]
159pub enum VNodeType {
160    Element = 0,
161    Component = 1,
162    Fragment = 2,
163}
164
165///
166/// Turn JSX into Inferno function calls
167///
168///
169/// `top_level_mark` should be [Mark] passed to
170/// [swc_ecma_transforms_base::resolver::resolver_with_mark].
171pub fn jsx<C>(comments: Option<C>, options: Options, unresolved_mark: Mark) -> impl Pass
172where
173    C: Comments,
174{
175    visit_mut_pass(Jsx {
176        unresolved_mark,
177        import_source: options
178            .import_source
179            .unwrap_or_else(default_import_source)
180            .into(),
181        import_create_vnode: None,
182        import_create_component: None,
183        import_create_text_vnode: None,
184        import_create_fragment: None,
185        import_normalize_props: None,
186
187        comments,
188        top_level_node: true,
189    })
190}
191
192struct Jsx<C>
193where
194    C: Comments,
195{
196    unresolved_mark: Mark,
197
198    import_source: Atom,
199
200    import_create_vnode: Option<Ident>,
201    import_create_component: Option<Ident>,
202    import_create_text_vnode: Option<Ident>,
203    import_create_fragment: Option<Ident>,
204    import_normalize_props: Option<Ident>,
205    top_level_node: bool,
206
207    comments: Option<C>,
208}
209
210#[derive(Debug, Default, Clone, PartialEq, Eq)]
211pub struct JsxDirectives {
212    pub import_source: Option<Atom>,
213}
214
215impl<C> Jsx<C>
216where
217    C: Comments,
218{
219    fn inject_runtime<T, F>(&mut self, body: &mut Vec<T>, inject: F)
220    where
221        T: StmtLike,
222        F: Fn(Vec<&str>, &str, &mut Vec<T>),
223    {
224        let mut import_specifiers = vec![];
225
226        if let Some(_local) = self.import_create_vnode.take() {
227            import_specifiers.push("createVNode")
228        }
229        if let Some(_local) = self.import_create_component.take() {
230            import_specifiers.push("createComponentVNode")
231        }
232        if let Some(_local) = self.import_create_text_vnode.take() {
233            import_specifiers.push("createTextVNode")
234        }
235        if let Some(_local) = self.import_normalize_props.take() {
236            import_specifiers.push("normalizeProps")
237        }
238        if let Some(_local) = self.import_create_fragment.take() {
239            import_specifiers.push("createFragment")
240        }
241
242        if !import_specifiers.is_empty() {
243            inject(import_specifiers, &self.import_source, body);
244        }
245    }
246
247    fn set_local_import_refs(&mut self, stmts: &mut Vec<ModuleItem>) {
248        for stmt in stmts {
249            if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = stmt {
250                if import.src.value == self.import_source {
251                    for specifier in import.specifiers.iter_mut() {
252                        match specifier {
253                            ImportSpecifier::Named(named_import) => {
254                                if named_import.local.sym == "createVNode" {
255                                    self.import_create_vnode
256                                        .get_or_insert(named_import.local.clone());
257                                } else if named_import.local.sym == "createComponentVNode" {
258                                    self.import_create_component
259                                        .get_or_insert(named_import.local.clone());
260                                } else if named_import.local.sym == "createTextVNode" {
261                                    self.import_create_text_vnode
262                                        .get_or_insert(named_import.local.clone());
263                                } else if named_import.local.sym == "createFragment" {
264                                    self.import_create_fragment
265                                        .get_or_insert(named_import.local.clone());
266                                } else if named_import.local.sym == "normalizeProps" {
267                                    self.import_normalize_props
268                                        .get_or_insert(named_import.local.clone());
269                                }
270                            }
271                            _ => continue,
272                        }
273                    }
274
275                    return;
276                }
277            }
278        }
279    }
280
281    fn jsx_frag_to_expr(&mut self, el: JSXFragment) -> Expr {
282        let span = el.span();
283
284        if let Some(comments) = &self.comments {
285            comments.add_pure_comment(span.lo);
286        }
287
288        let fragment = self
289            .import_create_fragment
290            .get_or_insert_with(|| quote_ident!("createFragment").into())
291            .clone();
292
293        let mut children_requires_normalization: bool = false;
294        let mut parent_can_be_keyed: bool = false;
295        let mut children_count: u16 = 0;
296
297        let mut children = vec![];
298        for child in el.children {
299            let child_expr = Some(match child {
300                JSXElementChild::JSXText(text) => {
301                    // TODO(kdy1): Optimize
302                    let value = jsx_text_to_str(text.value);
303                    let s = Str {
304                        span: text.span,
305                        raw: None,
306                        value,
307                    };
308
309                    if s.value.is_empty() {
310                        continue;
311                    }
312
313                    ExprOrSpread {
314                        spread: None,
315                        expr: Box::new(Expr::Call(CallExpr {
316                            span: DUMMY_SP,
317                            callee: self
318                                .import_create_text_vnode
319                                .get_or_insert_with(|| quote_ident!("createTextVNode").into())
320                                .clone()
321                                .as_callee(),
322                            args: vec![s.as_arg()],
323                            ..Default::default()
324                        })),
325                    }
326                }
327                JSXElementChild::JSXExprContainer(JSXExprContainer {
328                    expr: JSXExpr::Expr(e),
329                    ..
330                }) => {
331                    children_requires_normalization = true;
332                    parent_can_be_keyed = false;
333                    e.as_arg()
334                }
335                JSXElementChild::JSXExprContainer(JSXExprContainer {
336                    expr: JSXExpr::JSXEmptyExpr(..),
337                    ..
338                }) => continue,
339                JSXElementChild::JSXElement(el) => {
340                    if !parent_can_be_keyed && !children_requires_normalization {
341                        // Loop direct children to check if they have key property set
342                        parent_can_be_keyed = Self::does_children_have_key_defined(&el);
343                    }
344                    self.jsx_elem_to_expr(*el).as_arg()
345                }
346                JSXElementChild::JSXFragment(el) => self.jsx_frag_to_expr(el).as_arg(),
347                JSXElementChild::JSXSpreadChild(JSXSpreadChild { span, expr, .. }) => {
348                    ExprOrSpread {
349                        spread: Some(span),
350                        expr,
351                    }
352                }
353            });
354
355            children_count += 1;
356
357            children.push(child_expr)
358        }
359
360        let child_flags;
361
362        if !children_requires_normalization {
363            if children_count >= 1 {
364                if parent_can_be_keyed {
365                    child_flags = ChildFlags::HasKeyedChildren;
366                } else {
367                    child_flags = ChildFlags::HasNonKeyedChildren;
368                }
369            } else {
370                child_flags = ChildFlags::HasInvalidChildren;
371            }
372        } else {
373            child_flags = ChildFlags::UnknownChildren;
374        };
375
376        Expr::Call(CallExpr {
377            span,
378            callee: fragment.as_callee(),
379            args: create_fragment_vnode_args(children, false, child_flags as u16, None, None),
380            type_args: None,
381            ..Default::default()
382        })
383    }
384
385    fn jsx_elem_to_expr(&mut self, el: JSXElement) -> Expr {
386        let top_level_node = self.top_level_node;
387        let span = el.span();
388        self.top_level_node = false;
389
390        let name_span: Span = el.opening.name.span();
391        let name_expr;
392        let mut mut_flags: u16;
393        let vnode_kind: VNodeType;
394
395        match el.opening.name {
396            JSXElementName::Ident(ident) => {
397                if ident.sym == "this" {
398                    vnode_kind = Component;
399                    mut_flags = VNodeFlags::ComponentUnknown as u16;
400                    name_expr = Expr::This(ThisExpr { span: name_span });
401                } else if is_component_vnode(&ident) {
402                    if ident.sym == "Fragment" {
403                        vnode_kind = VNodeType::Fragment;
404                        mut_flags = VNodeFlags::ComponentUnknown as u16;
405                        name_expr = Expr::Ident(Ident::new("createFragment".into(), ident.span, Default::default()));
406                    } else {
407                        vnode_kind = Component;
408                        mut_flags = VNodeFlags::ComponentUnknown as u16;
409                        name_expr = Expr::Ident(ident)
410                    }
411                } else {
412                    vnode_kind = VNodeType::Element;
413                    mut_flags = parse_vnode_flag(&ident.sym);
414                    name_expr = Expr::Lit(Lit::Str(Str {
415                        span: name_span,
416                        raw: None,
417                        value: ident.sym,
418                    }))
419                }
420            }
421            JSXElementName::JSXNamespacedName(_) => {
422                HANDLER.with(|handler| {
423                    handler
424                        .struct_span_err(name_span, "JSX Namespace is disabled")
425                        .emit()
426                });
427
428                return Expr::Invalid(Invalid { span: DUMMY_SP });
429            }
430            JSXElementName::JSXMemberExpr(JSXMemberExpr { obj, prop, .. }) => {
431                vnode_kind = Component;
432                mut_flags = VNodeFlags::ComponentUnknown as u16;
433
434                fn convert_obj(obj: JSXObject) -> Box<Expr> {
435                    let span = obj.span();
436
437                    (match obj {
438                        JSXObject::Ident(i) => {
439                            if i.sym == "this" {
440                                Expr::This(ThisExpr { span })
441                            } else {
442                                Expr::Ident(i)
443                            }
444                        }
445                        JSXObject::JSXMemberExpr(e) => Expr::Member(MemberExpr {
446                            span,
447                            obj: convert_obj(e.obj),
448                            prop: MemberProp::Ident(e.prop),
449                        }),
450                    })
451                    .into()
452                }
453                name_expr = Expr::Member(MemberExpr {
454                    span: name_span,
455                    obj: convert_obj(obj),
456                    prop: MemberProp::Ident(prop.clone()),
457                })
458            }
459        }
460
461        if let Some(comments) = &self.comments {
462            comments.add_pure_comment(span.lo);
463        }
464
465        let mut props_obj = ObjectLit {
466            span: DUMMY_SP,
467            props: vec![],
468        };
469
470        let mut key_prop = None;
471        let mut ref_prop = None;
472        let mut component_refs: Option<ObjectLit> = None;
473
474        let mut class_name_param: Option<Box<Expr>> = None;
475        let mut has_text_children: bool = false;
476        let mut has_keyed_children: bool = false;
477        let mut has_non_keyed_children: bool = false;
478        let mut children_known: bool = false;
479        let mut needs_normalization: bool = false;
480        let mut has_re_create_flag: bool = false;
481        let mut child_flags_override_param = None;
482        let mut flags_override_param = None;
483        let mut content_editable_props: bool = false;
484        let mut prop_children: Option<Box<Expr>> = None;
485
486        for attr in el.opening.attrs {
487            match attr {
488                JSXAttrOrSpread::JSXAttr(attr) => {
489                    //
490                    match attr.name {
491                        JSXAttrName::Ident(i) => {
492                            //
493                            if i.sym == "class" || i.sym == "className" {
494                                if vnode_kind == VNodeType::Element {
495                                    if let Some(v) = attr.value {
496                                        class_name_param = jsx_attr_value_to_expr(v)
497                                    }
498
499                                    continue;
500                                }
501                            } else if i.sym == "onDoubleClick" {
502                                props_obj
503                                    .props
504                                    .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(
505                                        KeyValueProp {
506                                            key: PropName::Ident(IdentName::new(
507                                                "onDblClick".into(),
508                                                span
509                                            )),
510                                            value: match attr.value {
511                                                Some(v) => jsx_attr_value_to_expr(v)
512                                                    .expect("empty expression?"),
513                                                None => true.into(),
514                                            },
515                                        },
516                                    ))));
517                                continue;
518                            } else if i.sym == "key" {
519                                key_prop = attr
520                                    .value
521                                    .and_then(jsx_attr_value_to_expr)
522                                    .map(|expr| expr.as_arg());
523
524                                if key_prop.is_none() {
525                                    HANDLER.with(|handler| {
526                                        handler
527                                            .struct_span_err(
528                                                i.span,
529                                                "The value of property 'key' should not be \
530                                                     empty",
531                                            )
532                                            .emit();
533                                    });
534                                }
535
536                                continue;
537                            } else if i.sym == "ref" {
538                                ref_prop = attr
539                                    .value
540                                    .and_then(jsx_attr_value_to_expr)
541                                    .map(|expr| expr.as_arg());
542
543                                if ref_prop.is_none() {
544                                    HANDLER.with(|handler| {
545                                        handler
546                                            .struct_span_err(
547                                                i.span,
548                                                "The value of property 'ref' should not be \
549                                                     empty",
550                                            )
551                                            .emit();
552                                    });
553                                }
554
555                                continue;
556                            } else if i.sym == "$ChildFlag" {
557                                child_flags_override_param = attr
558                                    .value
559                                    .and_then(jsx_attr_value_to_expr)
560                                    .map(|expr| expr.as_arg());
561
562                                if child_flags_override_param.is_none() {
563                                    HANDLER.with(|handler| {
564                                        handler
565                                            .struct_span_err(
566                                                i.span,
567                                                "The value of property '$ChildFlag' should \
568                                                     not be empty",
569                                            )
570                                            .emit();
571                                    });
572                                }
573
574                                children_known = true;
575                                continue;
576                            } else if i.sym == "$HasVNodeChildren" {
577                                children_known = true;
578                                continue;
579                            } else if i.sym == "$Flags" {
580                                flags_override_param = attr
581                                    .value
582                                    .and_then(jsx_attr_value_to_expr)
583                                    .map(|expr| expr.as_arg());
584
585                                if flags_override_param.is_none() {
586                                    HANDLER.with(|handler| {
587                                        handler
588                                            .struct_span_err(
589                                                i.span,
590                                                "The value of property '$Flags' should not be \
591                                                     empty",
592                                            )
593                                            .emit();
594                                    });
595                                }
596
597                                continue;
598                            } else if i.sym == "$HasTextChildren" {
599                                children_known = true;
600                                has_text_children = true;
601                                continue;
602                            } else if i.sym == "$HasNonKeyedChildren" {
603                                children_known = true;
604                                has_non_keyed_children = true;
605                                continue;
606                            } else if i.sym == "$HasKeyedChildren" {
607                                children_known = true;
608                                has_keyed_children = true;
609                                continue;
610                            } else if i.sym == "$ReCreate" {
611                                has_re_create_flag = true;
612                                continue;
613                            }
614
615                            if i.sym.to_ascii_lowercase() == "contenteditable" {
616                                content_editable_props = true;
617                            } else if i.sym == "children" {
618                                if !el.children.is_empty() {
619                                    // prop children is ignored if there are any nested children
620                                    continue;
621                                }
622
623                                prop_children = match attr.value {
624                                    Some(v) => jsx_attr_value_to_expr(v),
625                                    None => continue,
626                                };
627
628                                continue;
629                            } else if vnode_kind == Component
630                                && i.sym.as_ref().starts_with("onComponent")
631                            {
632                                if let Some(v) = attr.value {
633                                    if component_refs.is_none() {
634                                        component_refs = Some(ObjectLit {
635                                            span: DUMMY_SP,
636                                            props: vec![],
637                                        })
638                                    };
639
640                                    if let Some(some_component_refs) = component_refs.as_mut() {
641                                        some_component_refs.props.push(PropOrSpread::Prop(
642                                            Box::new(Prop::KeyValue(KeyValueProp {
643                                                key: PropName::Ident(i),
644                                                value: jsx_attr_value_to_expr(v)
645                                                    .expect("empty expression container?"),
646                                            })),
647                                        ));
648                                    }
649                                };
650
651                                continue;
652                            }
653
654                            let value = match attr.value {
655                                Some(v) => {
656                                    jsx_attr_value_to_expr(v).expect("empty expression container?")
657                                }
658                                None => true.into(),
659                            };
660
661                            let converted_prop_name = if requires_lowercasing(&i.sym) {
662                                PropName::Ident(IdentName {
663                                    span: i.span,
664                                    sym: i.sym.to_lowercase().into()
665                                })
666                            } else {
667                                let converted_sym = transform_attribute(&i.sym);
668
669                                if converted_sym.contains('-') || converted_sym.contains(':') {
670                                    PropName::Str(Str {
671                                        span: i.span,
672                                        raw: None,
673                                        value: converted_sym.into(),
674                                    })
675                                } else {
676                                    PropName::Ident(IdentName {
677                                        span: i.span,
678                                        sym: converted_sym.into()
679                                    })
680                                }
681                            };
682
683                            props_obj
684                                .props
685                                .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
686                                    key: converted_prop_name,
687                                    value,
688                                }))));
689                        }
690                        JSXAttrName::JSXNamespacedName(JSXNamespacedName { ns, name, .. }) => {
691                            let value = match attr.value {
692                                Some(v) => {
693                                    jsx_attr_value_to_expr(v).expect("empty expression container?")
694                                }
695                                None => true.into(),
696                            };
697
698                            let str_value = format!("{}:{}", ns.sym, name.sym);
699                            let key = Str {
700                                span,
701                                raw: None,
702                                value: str_value.into(),
703                            };
704                            let key = PropName::Str(key);
705
706                            props_obj
707                                .props
708                                .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
709                                    key,
710                                    value,
711                                }))));
712                        }
713                    }
714                }
715                JSXAttrOrSpread::SpreadElement(attr) => match *attr.expr {
716                    Expr::Object(obj) => {
717                        needs_normalization = true;
718                        props_obj.props.extend(obj.props);
719                    }
720                    _ => {
721                        needs_normalization = true;
722                        props_obj.props.push(PropOrSpread::Spread(attr));
723                    }
724                },
725            }
726        }
727
728        let mut children_requires_normalization: bool = false;
729        let mut children_found_text: bool = false;
730        let mut parent_can_be_keyed: bool = false;
731        let mut children_count: u16 = 0;
732
733        let mut children = vec![];
734
735        for child in el.children {
736            let child_expr = Some(match child {
737                JSXElementChild::JSXText(text) => {
738                    // TODO(kdy1): Optimize
739                    let value = jsx_text_to_str(text.value);
740                    let s = Str {
741                        span: text.span,
742                        raw: None,
743                        value,
744                    };
745
746                    if s.value.is_empty() {
747                        continue;
748                    }
749
750                    if vnode_kind == VNodeType::Fragment {
751                        ExprOrSpread {
752                            spread: None,
753                            expr: Box::new(Expr::Call(CallExpr {
754                                span: DUMMY_SP,
755                                ctxt: SyntaxContext::empty().apply_mark(self.unresolved_mark),
756                                callee: self
757                                    .import_create_text_vnode
758                                    .get_or_insert_with(|| quote_ident!("createTextVNode").into())
759                                    .clone()
760                                    .as_callee(),
761                                args: vec![s.as_arg()],
762                                type_args: Default::default(),
763                            })),
764                        }
765                    } else {
766                        children_found_text = true;
767                        Lit::Str(s).as_arg()
768                    }
769                }
770                JSXElementChild::JSXExprContainer(JSXExprContainer {
771                    expr: JSXExpr::Expr(e),
772                    ..
773                }) => {
774                    children_requires_normalization = true;
775                    parent_can_be_keyed = false;
776                    e.as_arg()
777                }
778                JSXElementChild::JSXExprContainer(JSXExprContainer {
779                    expr: JSXExpr::JSXEmptyExpr(..),
780                    ..
781                }) => continue,
782                JSXElementChild::JSXElement(el) => {
783                    if vnode_kind != Component
784                        && !parent_can_be_keyed
785                        && !children_known
786                        && !children_requires_normalization
787                    {
788                        // Loop direct children to check if they have key property set
789                        parent_can_be_keyed = Self::does_children_have_key_defined(&el);
790                    }
791                    self.jsx_elem_to_expr(*el).as_arg()
792                }
793                JSXElementChild::JSXFragment(el) => self.jsx_frag_to_expr(el).as_arg(),
794                JSXElementChild::JSXSpreadChild(JSXSpreadChild { span, expr, .. }) => {
795                    ExprOrSpread {
796                        spread: Some(span),
797                        expr,
798                    }
799                }
800            });
801
802            children_count += 1;
803
804            children.push(child_expr)
805        }
806
807        if children_found_text {
808            match children_count {
809                1 => has_text_children = true,
810                _ => {
811                    for i in 0..children.len() {
812                        let child = &children[i];
813
814                        match child {
815                            Some(v) => {
816                                if let Expr::Lit(Lit::Str(text)) = &*v.expr {
817                                    children[i] = Some(ExprOrSpread {
818                                        spread: None,
819                                        expr: Box::new(Expr::Call(CallExpr {
820                                            span: DUMMY_SP,
821                                            ctxt: SyntaxContext::empty().apply_mark(self.unresolved_mark),
822                                            callee: self
823                                                .import_create_text_vnode
824                                                .get_or_insert_with(|| {
825                                                    quote_ident!("createTextVNode").into()
826                                                })
827                                                .clone()
828                                                .as_callee(),
829                                            args: vec![text.clone().as_arg()],
830                                            type_args: Default::default(),
831                                        })),
832                                    })
833                                }
834                            }
835                            _ => continue,
836                        }
837                    }
838                }
839            }
840        }
841
842        parent_can_be_keyed =
843            children_count > 1 && parent_can_be_keyed && !children_requires_normalization;
844        let parent_can_be_non_keyed =
845            children_count > 1 && !parent_can_be_keyed && !children_requires_normalization;
846
847        let child_flags: ChildFlags;
848
849        if !children_requires_normalization || children_known {
850            if has_keyed_children || parent_can_be_keyed {
851                child_flags = ChildFlags::HasKeyedChildren;
852            } else if has_non_keyed_children || parent_can_be_non_keyed {
853                child_flags = ChildFlags::HasNonKeyedChildren;
854            } else if children_count == 1 {
855                if has_text_children {
856                    child_flags = ChildFlags::HasTextChildren;
857                } else if vnode_kind == VNodeType::Fragment {
858                    child_flags = ChildFlags::HasNonKeyedChildren;
859                } else {
860                    child_flags = ChildFlags::HasVNodeChildren;
861                }
862            } else {
863                child_flags = ChildFlags::HasInvalidChildren
864            }
865        } else if has_keyed_children {
866            child_flags = ChildFlags::HasKeyedChildren;
867        } else if has_non_keyed_children {
868            child_flags = ChildFlags::HasNonKeyedChildren;
869        } else if has_text_children {
870            child_flags = ChildFlags::HasTextChildren;
871        } else {
872            child_flags = ChildFlags::UnknownChildren;
873        }
874
875        if vnode_kind == Component {
876            match children.len() {
877                0 => {
878                    match prop_children {
879                        Some(some_prop_children) => {
880                            props_obj
881                                .props
882                                .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
883                                    key: PropName::Ident(quote_ident!("children")),
884                                    value: some_prop_children,
885                                }))));
886                        }
887                        None => {
888                            // noop
889                        }
890                    }
891                }
892                1 if children[0].as_ref().unwrap().spread.is_none() => {
893                    props_obj
894                        .props
895                        .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
896                            key: PropName::Ident(quote_ident!("children")),
897                            value: children.take().into_iter().next().flatten().unwrap().expr,
898                        }))));
899                }
900                _ => {
901                    props_obj
902                        .props
903                        .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
904                            key: PropName::Ident(quote_ident!("children")),
905                            value: Box::new(Expr::Array(ArrayLit {
906                                span: DUMMY_SP,
907                                elems: children.take(),
908                            })),
909                        }))));
910                }
911            }
912        } else {
913            // Backwards compatibility...
914            // Set prop children as children if no nested children were set
915            if children.is_empty() {
916                match prop_children {
917                    Some(some_prop_children) => children.push(Some(ExprOrSpread {
918                        spread: None,
919                        expr: some_prop_children,
920                    })),
921                    None => {
922                        // noop
923                    }
924                }
925            }
926        }
927
928        self.top_level_node = top_level_node;
929
930        if has_re_create_flag {
931            mut_flags |= VNodeFlags::ReCreate as u16;
932        }
933        if content_editable_props {
934            mut_flags |= VNodeFlags::ContentEditable as u16;
935        }
936
937        let flags_expr = match flags_override_param {
938            None => Box::new(Expr::Lit(Lit::Num(Number {
939                span: DUMMY_SP,
940                raw: None,
941                value: mut_flags as f64,
942            })))
943            .as_arg(),
944            Some(v) => v,
945        };
946
947        let create_method = if vnode_kind == Component {
948            self.import_create_component
949                .get_or_insert_with(|| quote_ident!("createComponentVNode").into())
950                .clone()
951        } else if vnode_kind == VNodeType::Element {
952            self.import_create_vnode
953                .get_or_insert_with(|| quote_ident!("createVNode").into())
954                .clone()
955        } else {
956            self.import_create_fragment
957                .get_or_insert_with(|| quote_ident!("createFragment").into())
958                .clone()
959        };
960
961        let create_method_args = if vnode_kind == Component {
962            // Functional component cannot have basic ref so when component refs is set use it
963            // If we can ever detect Functional component from Class component compile time
964            // We could add some validations
965            if let Some(some_refs) = component_refs {
966                create_component_vnode_args(
967                    flags_expr,
968                    name_expr,
969                    props_obj,
970                    key_prop,
971                    Some(some_refs.as_arg()),
972                )
973            } else {
974                create_component_vnode_args(flags_expr, name_expr, props_obj, key_prop, ref_prop)
975            }
976        } else if vnode_kind == VNodeType::Element {
977            create_vnode_args(
978                flags_expr,
979                name_expr,
980                class_name_param,
981                children,
982                child_flags as u16,
983                child_flags_override_param,
984                props_obj,
985                key_prop,
986                ref_prop,
987            )
988        } else {
989            create_fragment_vnode_args(
990                children,
991                has_non_keyed_children
992                    || has_keyed_children
993                    || child_flags_override_param.is_some(),
994                child_flags as u16,
995                child_flags_override_param,
996                key_prop,
997            )
998        };
999
1000        let create_expr = Expr::Call(CallExpr {
1001            span,
1002            ctxt: SyntaxContext::empty().apply_mark(self.unresolved_mark),
1003            callee: create_method.as_callee(),
1004            args: create_method_args,
1005            type_args: Default::default(),
1006        });
1007
1008        if needs_normalization {
1009            return Expr::Call(CallExpr {
1010                span,
1011                ctxt: SyntaxContext::empty().apply_mark(self.unresolved_mark),
1012                callee: self
1013                    .import_normalize_props
1014                    .get_or_insert_with(|| quote_ident!("normalizeProps").into())
1015                    .clone()
1016                    .as_callee(),
1017                args: vec![create_expr.as_arg()],
1018                type_args: Default::default(),
1019            });
1020        }
1021
1022        create_expr
1023    }
1024
1025    fn does_children_have_key_defined(el: &JSXElement) -> bool {
1026        for attr in &el.opening.attrs {
1027            match attr {
1028                JSXAttrOrSpread::JSXAttr(attr) => {
1029                    //
1030                    match &attr.name {
1031                        JSXAttrName::Ident(i) => {
1032                            if i.sym == "key" {
1033                                return true;
1034                            }
1035                        }
1036                        JSXAttrName::JSXNamespacedName(_) => {
1037                            continue;
1038                        }
1039                    }
1040                }
1041                JSXAttrOrSpread::SpreadElement(_attr) => {
1042                    continue;
1043                }
1044            }
1045        }
1046
1047        false
1048    }
1049}
1050
1051#[inline(always)]
1052fn create_vnode_args(
1053    flags: ExprOrSpread,
1054    name: Expr,
1055    class_name: Option<Box<Expr>>,
1056    mut children: Vec<Option<ExprOrSpread>>,
1057    child_flags: u16,
1058    child_flags_override_param: Option<ExprOrSpread>,
1059    props: ObjectLit,
1060    key: Option<ExprOrSpread>,
1061    refs: Option<ExprOrSpread>,
1062) -> Vec<ExprOrSpread> {
1063    let mut args: Vec<ExprOrSpread> = vec![flags, name.as_arg()];
1064
1065    let has_children = !children.is_empty();
1066    let has_child_flags = child_flags_override_param.is_some()
1067        || child_flags != (ChildFlags::HasInvalidChildren as u16);
1068    let has_props = !props.props.is_empty();
1069    let has_key = key.is_some();
1070    let has_ref = refs.is_some();
1071
1072    match class_name {
1073        None => {
1074            if has_children || has_child_flags || has_props || has_key || has_ref {
1075                args.push(Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))).as_arg());
1076            }
1077        }
1078        Some(some_class_name) => {
1079            args.push(some_class_name.as_arg());
1080        }
1081    }
1082
1083    match children.len() {
1084        0 => {
1085            if has_child_flags || has_props || has_key || has_ref {
1086                args.push(Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))).as_arg());
1087            }
1088        }
1089        1 => args.push(
1090            children
1091                .take()
1092                .into_iter()
1093                .next()
1094                .flatten()
1095                .unwrap()
1096                .expr
1097                .as_arg(),
1098        ),
1099        _ => args.push(
1100            Box::new(Expr::Array(ArrayLit {
1101                span: DUMMY_SP,
1102                elems: children.take(),
1103            }))
1104            .as_arg(),
1105        ),
1106    }
1107
1108    if has_child_flags {
1109        match child_flags_override_param {
1110            Some(some_child_flags_override_param) => {
1111                args.push(some_child_flags_override_param);
1112            }
1113            None => args.push(
1114                Box::new(Expr::Lit(Lit::Num(Number {
1115                    span: DUMMY_SP,
1116                    raw: None,
1117                    value: child_flags as f64,
1118                })))
1119                .as_arg(),
1120            ),
1121        }
1122    } else if has_props || has_key || has_ref {
1123        args.push(
1124            Box::new(Expr::Lit(Lit::Num(Number {
1125                span: DUMMY_SP,
1126                raw: None,
1127                value: (ChildFlags::HasInvalidChildren as u16) as f64,
1128            })))
1129            .as_arg(),
1130        );
1131    }
1132
1133    if has_props {
1134        args.push(props.as_arg());
1135    } else if has_key || has_ref {
1136        args.push(Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))).as_arg());
1137    }
1138
1139    match key {
1140        None => {
1141            if has_ref {
1142                args.push(Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))).as_arg());
1143            }
1144        }
1145        Some(some_key) => {
1146            args.push(some_key);
1147        }
1148    }
1149
1150    match refs {
1151        None => {}
1152        Some(some_refs) => {
1153            args.push(some_refs);
1154        }
1155    }
1156
1157    args
1158}
1159
1160#[inline(always)]
1161fn create_component_vnode_args(
1162    flags: ExprOrSpread,
1163    name: Expr,
1164    props_literal: ObjectLit,
1165    key: Option<ExprOrSpread>,
1166    refs: Option<ExprOrSpread>,
1167) -> Vec<ExprOrSpread> {
1168    let mut args: Vec<ExprOrSpread> = vec![flags, name.as_arg()];
1169
1170    if props_literal.props.is_empty() {
1171        if key.is_some() || refs.is_some() {
1172            args.push(Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))).as_arg());
1173        }
1174    } else {
1175        args.push(props_literal.as_arg());
1176    }
1177
1178    match key {
1179        None => {
1180            if refs.is_some() {
1181                args.push(Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))).as_arg());
1182            }
1183        }
1184        Some(some_key) => {
1185            args.push(some_key);
1186        }
1187    }
1188
1189    match refs {
1190        None => {}
1191        Some(some_ref) => {
1192            args.push(some_ref);
1193        }
1194    }
1195
1196    args
1197}
1198
1199#[inline(always)]
1200fn create_fragment_vnode_args(
1201    mut children: Vec<Option<ExprOrSpread>>,
1202    children_shape_is_user_defined: bool,
1203    child_flags: u16,
1204    child_flags_override_param: Option<ExprOrSpread>,
1205    key: Option<ExprOrSpread>,
1206) -> Vec<ExprOrSpread> {
1207    let mut args: Vec<ExprOrSpread> = vec![];
1208    let has_child_flags = child_flags_override_param.is_some()
1209        || child_flags != (ChildFlags::HasInvalidChildren as u16);
1210    let has_key = key.is_some();
1211
1212    match children.len() {
1213        0 => {
1214            if has_child_flags || has_key {
1215                args.push(Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))).as_arg());
1216            }
1217        }
1218        1 => {
1219            if children_shape_is_user_defined || child_flags == ChildFlags::UnknownChildren as u16 {
1220                args.push(
1221                    children
1222                        .take()
1223                        .into_iter()
1224                        .next()
1225                        .flatten()
1226                        .unwrap()
1227                        .expr
1228                        .as_arg(),
1229                );
1230            } else {
1231                args.push(
1232                    Box::new(Expr::Array(ArrayLit {
1233                        span: DUMMY_SP,
1234                        elems: children.take(),
1235                    }))
1236                    .as_arg(),
1237                );
1238            }
1239        }
1240        _ => args.push(
1241            Box::new(Expr::Array(ArrayLit {
1242                span: DUMMY_SP,
1243                elems: children.take(),
1244            }))
1245            .as_arg(),
1246        ),
1247    }
1248
1249    if has_child_flags {
1250        match child_flags_override_param {
1251            Some(some_child_flags_override_param) => {
1252                args.push(some_child_flags_override_param);
1253            }
1254            None => args.push(
1255                Box::new(Expr::Lit(Lit::Num(Number {
1256                    span: DUMMY_SP,
1257                    raw: None,
1258                    value: child_flags as f64,
1259                })))
1260                .as_arg(),
1261            ),
1262        }
1263    } else if has_key {
1264        args.push(
1265            Box::new(Expr::Lit(Lit::Num(Number {
1266                span: DUMMY_SP,
1267                raw: None,
1268                value: (ChildFlags::HasInvalidChildren as u16) as f64,
1269            })))
1270            .as_arg(),
1271        );
1272    }
1273
1274    match key {
1275        None => {}
1276        Some(some_key) => {
1277            args.push(some_key);
1278        }
1279    }
1280
1281    args
1282}
1283
1284impl<C> VisitMut for Jsx<C>
1285where
1286    C: Comments,
1287{
1288    noop_visit_mut_type!();
1289
1290    fn visit_mut_expr(&mut self, expr: &mut Expr) {
1291        let top_level_node = self.top_level_node;
1292        let mut did_work = false;
1293
1294        if let Expr::JSXElement(el) = expr {
1295            did_work = true;
1296            // <div></div> => Inferno.createVNode(...);
1297            *expr = self.jsx_elem_to_expr(*el.take());
1298        } else if let Expr::JSXFragment(frag) = expr {
1299            // <></> => Inferno.createFragment(...);
1300            did_work = true;
1301            *expr = self.jsx_frag_to_expr(frag.take());
1302        } else if let Expr::Paren(ParenExpr {
1303            expr: inner_expr, ..
1304        }) = expr
1305        {
1306            if let Expr::JSXElement(el) = &mut **inner_expr {
1307                did_work = true;
1308                *expr = self.jsx_elem_to_expr(*el.take());
1309            } else if let Expr::JSXFragment(frag) = &mut **inner_expr {
1310                // <></> => Inferno.createFragment(...);
1311                did_work = true;
1312                *expr = self.jsx_frag_to_expr(frag.take());
1313            }
1314        }
1315
1316        if did_work {
1317            self.top_level_node = false;
1318        }
1319
1320        expr.visit_mut_children_with(self);
1321
1322        self.top_level_node = top_level_node;
1323    }
1324
1325    fn visit_mut_module(&mut self, module: &mut Module) {
1326        self.set_local_import_refs(&mut module.body);
1327
1328        module.visit_mut_children_with(self);
1329
1330        self.inject_runtime(&mut module.body, |imports, default_import_src, stmts| {
1331            // Merge new imports to existing import
1332            if merge_imports(&imports, default_import_src, stmts) {
1333                return;
1334            }
1335
1336            // Existing inferno import was not found, add new
1337            let specifiers: Vec<ImportSpecifier> = imports
1338                .into_iter()
1339                .map(|imported| {
1340                    ImportSpecifier::Named(ImportNamedSpecifier {
1341                        span: DUMMY_SP,
1342                        local: quote_ident!(imported).into(),
1343                        imported: None,
1344                        is_type_only: false,
1345                    })
1346                })
1347                .collect();
1348
1349            prepend_stmt(
1350                stmts,
1351                ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
1352                    span: DUMMY_SP,
1353                    specifiers,
1354                    src: Str {
1355                        span: DUMMY_SP,
1356                        raw: None,
1357                        value: default_import_src.into(),
1358                    }
1359                    .into(),
1360                    type_only: Default::default(),
1361                    with: Default::default(),
1362                    phase: Default::default(),
1363                })),
1364            )
1365        });
1366    }
1367
1368    fn visit_mut_script(&mut self, script: &mut Script) {
1369        script.visit_mut_children_with(self);
1370
1371        let mark = self.unresolved_mark;
1372        self.inject_runtime(&mut script.body, |imports, src, stmts| {
1373            prepend_stmt(stmts, add_require(imports, src, mark))
1374        });
1375    }
1376}
1377
1378fn add_require(imports: Vec<&str>, src: &str, unresolved_mark: Mark) -> Stmt {
1379    Stmt::Decl(Decl::Var(Box::new(VarDecl {
1380        span: DUMMY_SP,
1381        ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
1382        kind: VarDeclKind::Const,
1383        declare: false,
1384        decls: vec![VarDeclarator {
1385            span: DUMMY_SP,
1386            name: Pat::Object(ObjectPat {
1387                span: DUMMY_SP,
1388                props: imports
1389                    .into_iter()
1390                    .map(|imported| {
1391                        ObjectPatProp::Assign(AssignPatProp {
1392                            span: DUMMY_SP,
1393                            key: BindingIdent {
1394                                id: Ident {
1395                                    span: DUMMY_SP,
1396                                    ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
1397                                    sym: imported.into(),
1398                                    optional: false,
1399                                },
1400                                type_ann: None,
1401                            },
1402                            value: None,
1403                        })
1404                    })
1405                    .collect(),
1406                optional: false,
1407                type_ann: None,
1408            }),
1409            // require('inferno')
1410            init: Some(Box::new(Expr::Call(CallExpr {
1411                span: DUMMY_SP,
1412                ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
1413                callee: Callee::Expr(Box::new(Expr::Ident(Ident {
1414                    ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
1415                    sym: "require".into(),
1416                    optional: false,
1417                    ..Default::default()
1418                }))),
1419                args: vec![ExprOrSpread {
1420                    spread: None,
1421                    expr: Box::new(Expr::Lit(Lit::Str(Str {
1422                        span: DUMMY_SP,
1423                        value: src.into(),
1424                        raw: None,
1425                    }))),
1426                }],
1427                type_args: None,
1428            }))),
1429            definite: false,
1430        }],
1431    })))
1432}
1433
1434#[inline]
1435fn is_component_vnode(i: &Ident) -> bool {
1436    // If it starts with uppercase
1437    i.as_ref().starts_with(|c: char| c.is_ascii_uppercase())
1438}
1439
1440#[inline]
1441fn jsx_text_to_str(t: Atom) -> Atom {
1442    let mut buf = String::new();
1443    let replaced = t.replace('\t', " ");
1444
1445    for (is_last, (i, line)) in replaced.lines().enumerate().identify_last() {
1446        if line.is_empty() {
1447            continue;
1448        }
1449        let line = Cow::from(line);
1450        let line = if i != 0 {
1451            Cow::Borrowed(line.trim_start_matches(' '))
1452        } else {
1453            line
1454        };
1455        let line = if is_last {
1456            line
1457        } else {
1458            Cow::Borrowed(line.trim_end_matches(' '))
1459        };
1460        if line.len() == 0 {
1461            continue;
1462        }
1463        if i != 0 && !buf.is_empty() {
1464            buf.push(' ')
1465        }
1466        buf.push_str(&line);
1467    }
1468    buf.into()
1469}
1470
1471fn jsx_attr_value_to_expr(v: JSXAttrValue) -> Option<Box<Expr>> {
1472    Some(match v {
1473        JSXAttrValue::Lit(Lit::Str(s)) => {
1474            let value = transform_jsx_attr_str(&s.value);
1475
1476            Box::new(Expr::Lit(Lit::Str(Str {
1477                span: s.span,
1478                raw: None,
1479                value: value.into(),
1480            })))
1481        }
1482        JSXAttrValue::Lit(lit) => Box::new(lit.into()),
1483        JSXAttrValue::JSXExprContainer(e) => match e.expr {
1484            JSXExpr::JSXEmptyExpr(_) => None?,
1485            JSXExpr::Expr(e) => e,
1486        },
1487        JSXAttrValue::JSXElement(e) => Box::new(Expr::JSXElement(e)),
1488        JSXAttrValue::JSXFragment(f) => Box::new(Expr::JSXFragment(f)),
1489    })
1490}
1491
1492fn transform_jsx_attr_str(v: &str) -> String {
1493    let single_quote = false;
1494    let mut buf = String::with_capacity(v.len());
1495    let mut iter = v.chars().peekable();
1496
1497    while let Some(c) = iter.next() {
1498        match c {
1499            '\u{0008}' => buf.push_str("\\b"),
1500            '\u{000c}' => buf.push_str("\\f"),
1501            ' ' => buf.push(' '),
1502
1503            '\n' | '\r' | '\t' => {
1504                buf.push(' ');
1505
1506                while let Some(' ') = iter.peek() {
1507                    iter.next();
1508                }
1509            }
1510            '\u{000b}' => buf.push_str("\\v"),
1511            '\0' => buf.push_str("\\x00"),
1512
1513            '\'' if single_quote => buf.push_str("\\'"),
1514            '"' if !single_quote => buf.push('\"'),
1515
1516            '\x01'..='\x0f' | '\x10'..='\x1f' => {
1517                buf.push(c);
1518            }
1519
1520            '\x20'..='\x7e' => {
1521                //
1522                buf.push(c);
1523            }
1524            '\u{7f}'..='\u{ff}' => {
1525                buf.push(c);
1526            }
1527
1528            _ => {
1529                buf.push(c);
1530            }
1531        }
1532    }
1533
1534    buf
1535}