swc_ecma_transforms_react/jsx/
mod.rs

1#![allow(clippy::redundant_allocation)]
2
3use std::{
4    iter::{self, once},
5    sync::RwLock,
6};
7
8use bytes_str::BytesStr;
9use once_cell::sync::Lazy;
10use rustc_hash::FxHashMap;
11use serde::{Deserialize, Serialize};
12use string_enum::StringEnum;
13use swc_atoms::{atom, Atom};
14use swc_common::{
15    comments::{Comment, CommentKind, Comments},
16    errors::HANDLER,
17    sync::Lrc,
18    util::take::Take,
19    FileName, Mark, SourceMap, Span, Spanned, SyntaxContext, DUMMY_SP,
20};
21use swc_config::merge::Merge;
22use swc_ecma_ast::*;
23use swc_ecma_parser::{parse_file_as_expr, Syntax};
24use swc_ecma_utils::{
25    drop_span, prepend_stmt, private_ident, quote_ident, str::is_line_terminator, ExprFactory,
26    StmtLike,
27};
28use swc_ecma_visit::{noop_visit_mut_type, visit_mut_pass, VisitMut, VisitMutWith};
29
30use self::static_check::should_use_create_element;
31use crate::refresh::options::{deserialize_refresh, RefreshOptions};
32
33mod static_check;
34#[cfg(test)]
35mod tests;
36
37/// https://babeljs.io/docs/en/babel-plugin-transform-react-jsx#runtime
38#[derive(StringEnum, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
39pub enum Runtime {
40    /// `automatic`
41    Automatic,
42    /// `classic`
43    Classic,
44    /// `preserve`
45    Preserve,
46}
47
48/// Note: This will changed in v2
49impl Default for Runtime {
50    fn default() -> Self {
51        Runtime::Classic
52    }
53}
54
55#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq, Merge)]
56#[serde(rename_all = "camelCase")]
57#[serde(deny_unknown_fields)]
58pub struct Options {
59    /// If this is `true`, swc will behave just like babel 8 with
60    /// `BABEL_8_BREAKING: true`.
61    #[serde(skip, default)]
62    pub next: Option<bool>,
63
64    #[serde(default)]
65    pub runtime: Option<Runtime>,
66
67    /// For automatic runtime
68    #[serde(default)]
69    pub import_source: Option<Atom>,
70
71    #[serde(default)]
72    pub pragma: Option<BytesStr>,
73    #[serde(default)]
74    pub pragma_frag: Option<BytesStr>,
75
76    #[serde(default)]
77    pub throw_if_namespace: Option<bool>,
78
79    #[serde(default)]
80    pub development: Option<bool>,
81
82    // @babel/plugin-transform-react-jsx: Since "useBuiltIns" is removed in Babel 8, you can remove
83    // it from the config.
84    #[deprecated(
85        since = "0.167.4",
86        note = r#"Since `useBuiltIns` is removed in swc, you can remove it from the config."#
87    )]
88    #[serde(default, alias = "useBuiltIns")]
89    pub use_builtins: Option<bool>,
90
91    // '@babel/plugin-transform-react-jsx: Since Babel 8, an inline object with spread elements is
92    // always used, and the "useSpread" option is no longer available. Please remove it from your
93    // config.',
94    #[deprecated(
95        since = "0.167.4",
96        note = r#"An inline object with spread elements is always used, and the `useSpread` option is no longer available. Please remove it from your config."#
97    )]
98    #[serde(default)]
99    pub use_spread: Option<bool>,
100
101    #[serde(default, deserialize_with = "deserialize_refresh")]
102    // default to disabled since this is still considered as experimental by now
103    pub refresh: Option<RefreshOptions>,
104}
105
106#[cfg(feature = "concurrent")]
107macro_rules! static_str {
108    ($s:expr) => {{
109        static VAL: Lazy<BytesStr> = Lazy::new(|| $s.into());
110        VAL.clone()
111    }};
112}
113
114#[cfg(not(feature = "concurrent"))]
115macro_rules! static_str {
116    ($s:expr) => {
117        $s.into()
118    };
119}
120
121pub fn default_import_source() -> Atom {
122    atom!("react")
123}
124
125pub fn default_pragma() -> BytesStr {
126    static_str!("React.createElement")
127}
128
129pub fn default_pragma_frag() -> BytesStr {
130    static_str!("React.Fragment")
131}
132
133fn default_throw_if_namespace() -> bool {
134    true
135}
136
137/// Parse `src` to use as a `pragma` or `pragmaFrag` in jsx.
138pub fn parse_expr_for_jsx(
139    cm: &SourceMap,
140    name: &str,
141    src: BytesStr,
142    top_level_mark: Mark,
143) -> Box<Expr> {
144    let fm = cm.new_source_file(cache_filename(name), src);
145
146    parse_file_as_expr(
147        &fm,
148        Syntax::default(),
149        Default::default(),
150        None,
151        &mut Vec::new(),
152    )
153    .map_err(|e| {
154        if HANDLER.is_set() {
155            HANDLER.with(|h| {
156                e.into_diagnostic(h)
157                    .note("Failed to parse jsx pragma")
158                    .emit()
159            })
160        }
161    })
162    .map(drop_span)
163    .map(|mut expr| {
164        apply_mark(&mut expr, top_level_mark);
165        expr
166    })
167    .unwrap_or_else(|()| {
168        panic!(
169            "failed to parse jsx option {}: '{}' is not an expression",
170            name, fm.src,
171        )
172    })
173}
174
175fn apply_mark(e: &mut Expr, mark: Mark) {
176    match e {
177        Expr::Ident(i) => {
178            i.ctxt = i.ctxt.apply_mark(mark);
179        }
180        Expr::Member(MemberExpr { obj, .. }) => {
181            apply_mark(obj, mark);
182        }
183        _ => {}
184    }
185}
186
187/// `@babel/plugin-transform-react-jsx`
188///
189/// Turn JSX into React function calls
190///
191///
192/// `top_level_mark` should be [Mark] passed to
193/// [swc_ecma_transforms_base::resolver::resolver_with_mark].
194///
195///
196/// # Parameters
197///
198/// ## `top_level_ctxt`
199///
200/// This is used to reference `React` defined by the user.
201///
202/// e.g.
203///
204/// ```js
205/// import React from 'react';
206/// ```
207pub fn jsx<C>(
208    cm: Lrc<SourceMap>,
209    comments: Option<C>,
210    options: Options,
211    top_level_mark: Mark,
212    unresolved_mark: Mark,
213) -> impl Pass + VisitMut
214where
215    C: Comments,
216{
217    visit_mut_pass(Jsx {
218        cm: cm.clone(),
219        top_level_mark,
220        unresolved_mark,
221        runtime: options.runtime.unwrap_or_default(),
222        import_source: options.import_source.unwrap_or_else(default_import_source),
223        import_jsx: None,
224        import_jsxs: None,
225        import_fragment: None,
226        import_create_element: None,
227
228        pragma: Lrc::new(parse_expr_for_jsx(
229            &cm,
230            "pragma",
231            options.pragma.unwrap_or_else(default_pragma),
232            top_level_mark,
233        )),
234        comments,
235        pragma_frag: Lrc::new(parse_expr_for_jsx(
236            &cm,
237            "pragmaFrag",
238            options.pragma_frag.unwrap_or_else(default_pragma_frag),
239            top_level_mark,
240        )),
241        development: options.development.unwrap_or_default(),
242        throw_if_namespace: options
243            .throw_if_namespace
244            .unwrap_or_else(default_throw_if_namespace),
245        top_level_node: true,
246    })
247}
248
249struct Jsx<C>
250where
251    C: Comments,
252{
253    cm: Lrc<SourceMap>,
254
255    top_level_mark: Mark,
256    unresolved_mark: Mark,
257
258    runtime: Runtime,
259    /// For automatic runtime.
260    import_source: Atom,
261    /// For automatic runtime.
262    import_jsx: Option<Ident>,
263    /// For automatic runtime.
264    import_jsxs: Option<Ident>,
265    /// For automatic runtime.
266    import_create_element: Option<Ident>,
267    /// For automatic runtime.
268    import_fragment: Option<Ident>,
269    top_level_node: bool,
270
271    pragma: Lrc<Box<Expr>>,
272    comments: Option<C>,
273    pragma_frag: Lrc<Box<Expr>>,
274    development: bool,
275    throw_if_namespace: bool,
276}
277
278#[derive(Debug, Default, Clone, PartialEq, Eq)]
279pub struct JsxDirectives {
280    pub runtime: Option<Runtime>,
281
282    /// For automatic runtime.
283    pub import_source: Option<Atom>,
284
285    /// Parsed from `@jsx`
286    pub pragma: Option<Lrc<Box<Expr>>>,
287
288    /// Parsed from `@jsxFrag`
289    pub pragma_frag: Option<Lrc<Box<Expr>>>,
290}
291
292fn respan(e: &mut Expr, span: Span) {
293    match e {
294        Expr::Ident(i) => {
295            i.span.lo = span.lo;
296            i.span.hi = span.hi;
297        }
298        Expr::Member(e) => {
299            e.span = span;
300        }
301        _ => {}
302    }
303}
304
305impl JsxDirectives {
306    pub fn from_comments(
307        cm: &SourceMap,
308        _: Span,
309        comments: &[Comment],
310        top_level_mark: Mark,
311    ) -> Self {
312        let mut res = JsxDirectives::default();
313
314        for cmt in comments {
315            if cmt.kind != CommentKind::Block {
316                continue;
317            }
318
319            for line in cmt.text.lines() {
320                let mut line = line.trim();
321                if line.starts_with('*') {
322                    line = line[1..].trim();
323                }
324
325                if !line.starts_with("@jsx") {
326                    continue;
327                }
328
329                let mut words = line.split_whitespace();
330                loop {
331                    let pragma = words.next();
332                    if pragma.is_none() {
333                        break;
334                    }
335                    let val = words.next();
336
337                    match pragma {
338                        Some("@jsxRuntime") => match val {
339                            Some("classic") => res.runtime = Some(Runtime::Classic),
340                            Some("automatic") => res.runtime = Some(Runtime::Automatic),
341                            None => {}
342                            _ => {
343                                HANDLER.with(|handler| {
344                                    handler
345                                        .struct_span_err(
346                                            cmt.span,
347                                            "Runtime must be either `classic` or `automatic`.",
348                                        )
349                                        .emit()
350                                });
351                            }
352                        },
353                        Some("@jsxImportSource") => {
354                            if let Some(src) = val {
355                                res.runtime = Some(Runtime::Automatic);
356                                res.import_source = Some(Atom::new(src));
357                            }
358                        }
359                        Some("@jsxFrag") => {
360                            if let Some(src) = val {
361                                if is_valid_for_pragma(src) {
362                                    // TODO: Optimize
363                                    let mut e = parse_expr_for_jsx(
364                                        cm,
365                                        "module-jsx-pragma-frag",
366                                        cache_source(src),
367                                        top_level_mark,
368                                    );
369                                    respan(&mut e, cmt.span);
370                                    res.pragma_frag = Some(e.into())
371                                }
372                            }
373                        }
374                        Some("@jsx") => {
375                            if let Some(src) = val {
376                                if is_valid_for_pragma(src) {
377                                    // TODO: Optimize
378                                    let mut e = parse_expr_for_jsx(
379                                        cm,
380                                        "module-jsx-pragma",
381                                        cache_source(src),
382                                        top_level_mark,
383                                    );
384                                    respan(&mut e, cmt.span);
385                                    res.pragma = Some(e.into());
386                                }
387                            }
388                        }
389                        _ => {}
390                    }
391                }
392            }
393        }
394
395        res
396    }
397}
398
399#[cfg(feature = "concurrent")]
400fn cache_filename(name: &str) -> Lrc<FileName> {
401    static FILENAME_CACHE: Lazy<RwLock<FxHashMap<String, Lrc<FileName>>>> =
402        Lazy::new(|| RwLock::new(FxHashMap::default()));
403
404    {
405        let cache = FILENAME_CACHE
406            .read()
407            .expect("Failed to read FILENAME_CACHE");
408        if let Some(f) = cache.get(name) {
409            return f.clone();
410        }
411    }
412
413    let file = Lrc::new(FileName::Internal(format!("jsx-config-{name}.js")));
414
415    {
416        let mut cache = FILENAME_CACHE
417            .write()
418            .expect("Failed to write FILENAME_CACHE");
419        cache.insert(name.to_string(), file.clone());
420    }
421
422    file
423}
424
425#[cfg(not(feature = "concurrent"))]
426fn cache_filename(name: &str) -> Lrc<FileName> {
427    Lrc::new(FileName::Internal(format!("jsx-config-{name}.js")))
428}
429
430#[cfg(feature = "concurrent")]
431fn cache_source(src: &str) -> BytesStr {
432    use rustc_hash::FxHashSet;
433
434    static CACHE: Lazy<RwLock<FxHashSet<BytesStr>>> =
435        Lazy::new(|| RwLock::new(FxHashSet::default()));
436
437    {
438        let cache = CACHE.write().unwrap();
439
440        if let Some(cached) = cache.get(src) {
441            return cached.clone();
442        }
443    }
444
445    let cached: BytesStr = src.to_string().into();
446    {
447        let mut cache = CACHE.write().unwrap();
448        cache.insert(cached.clone());
449    }
450    cached
451}
452
453#[cfg(not(feature = "concurrent"))]
454fn cache_source(src: &str) -> BytesStr {
455    // We cannot cache because Rc does not implement Send.
456    src.to_string().into()
457}
458
459fn is_valid_for_pragma(s: &str) -> bool {
460    if s.is_empty() {
461        return false;
462    }
463
464    if !s.starts_with(|c: char| Ident::is_valid_start(c)) {
465        return false;
466    }
467
468    for c in s.chars() {
469        if !Ident::is_valid_continue(c) && c != '.' {
470            return false;
471        }
472    }
473
474    true
475}
476
477impl<C> Jsx<C>
478where
479    C: Comments,
480{
481    fn inject_runtime<T, F>(&mut self, body: &mut Vec<T>, inject: F)
482    where
483        T: StmtLike,
484        // Fn(Vec<(local, imported)>, src, body)
485        F: Fn(Vec<(Ident, IdentName)>, &str, &mut Vec<T>),
486    {
487        if self.runtime == Runtime::Automatic {
488            if let Some(local) = self.import_create_element.take() {
489                inject(
490                    vec![(local, quote_ident!("createElement"))],
491                    &self.import_source,
492                    body,
493                );
494            }
495
496            let imports = self.import_jsx.take();
497            let imports = if self.development {
498                imports
499                    .map(|local| (local, quote_ident!("jsxDEV")))
500                    .into_iter()
501                    .chain(
502                        self.import_fragment
503                            .take()
504                            .map(|local| (local, quote_ident!("Fragment"))),
505                    )
506                    .collect::<Vec<_>>()
507            } else {
508                imports
509                    .map(|local| (local, quote_ident!("jsx")))
510                    .into_iter()
511                    .chain(
512                        self.import_jsxs
513                            .take()
514                            .map(|local| (local, quote_ident!("jsxs"))),
515                    )
516                    .chain(
517                        self.import_fragment
518                            .take()
519                            .map(|local| (local, quote_ident!("Fragment"))),
520                    )
521                    .collect::<Vec<_>>()
522            };
523
524            if !imports.is_empty() {
525                let jsx_runtime = if self.development {
526                    "jsx-dev-runtime"
527                } else {
528                    "jsx-runtime"
529                };
530
531                let value = format!("{}/{}", self.import_source, jsx_runtime);
532                inject(imports, &value, body)
533            }
534        }
535    }
536
537    fn jsx_frag_to_expr(&mut self, el: JSXFragment) -> Expr {
538        let mut span = el.span();
539
540        if let Some(comments) = &self.comments {
541            if span.lo.is_dummy() {
542                span.lo = Span::dummy_with_cmt().lo;
543            }
544
545            comments.add_pure_comment(span.lo);
546        }
547
548        match self.runtime {
549            Runtime::Automatic => {
550                let fragment = self
551                    .import_fragment
552                    .get_or_insert_with(|| private_ident!("_Fragment"))
553                    .clone();
554
555                let mut props_obj = ObjectLit {
556                    span: DUMMY_SP,
557                    props: Vec::new(),
558                };
559
560                let children = el
561                    .children
562                    .into_iter()
563                    .filter_map(|child| self.jsx_elem_child_to_expr(child))
564                    .map(Some)
565                    .collect::<Vec<_>>();
566
567                let use_jsxs = match children.len() {
568                    0 => false,
569                    1 if matches!(children.first(), Some(Some(child)) if child.spread.is_none()) => {
570                        props_obj
571                            .props
572                            .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
573                                key: PropName::Ident(quote_ident!("children")),
574                                value: children.into_iter().next().flatten().unwrap().expr,
575                            }))));
576
577                        false
578                    }
579                    _ => {
580                        props_obj
581                            .props
582                            .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
583                                key: PropName::Ident(quote_ident!("children")),
584                                value: ArrayLit {
585                                    span: DUMMY_SP,
586                                    elems: children,
587                                }
588                                .into(),
589                            }))));
590                        true
591                    }
592                };
593
594                let jsx = if use_jsxs && !self.development {
595                    self.import_jsxs
596                        .get_or_insert_with(|| private_ident!("_jsxs"))
597                        .clone()
598                } else {
599                    let jsx = if self.development { "_jsxDEV" } else { "_jsx" };
600                    self.import_jsx
601                        .get_or_insert_with(|| private_ident!(jsx))
602                        .clone()
603                };
604
605                let args = once(fragment.as_arg()).chain(once(props_obj.as_arg()));
606
607                let args = if self.development {
608                    args.chain(once(Expr::undefined(DUMMY_SP).as_arg()))
609                        .chain(once(use_jsxs.as_arg()))
610                        .collect()
611                } else {
612                    args.collect()
613                };
614
615                CallExpr {
616                    span,
617                    callee: jsx.as_callee(),
618                    args,
619                    ..Default::default()
620                }
621                .into()
622            }
623            Runtime::Classic => {
624                CallExpr {
625                    span,
626                    callee: (*self.pragma).clone().as_callee(),
627                    args: iter::once((*self.pragma_frag).clone().as_arg())
628                        // attribute: null
629                        .chain(iter::once(Lit::Null(Null { span: DUMMY_SP }).as_arg()))
630                        .chain({
631                            // Children
632                            el.children
633                                .into_iter()
634                                .filter_map(|c| self.jsx_elem_child_to_expr(c))
635                        })
636                        .collect(),
637                    ..Default::default()
638                }
639                .into()
640            }
641            Runtime::Preserve => unreachable!(),
642        }
643    }
644
645    /// # Automatic
646    ///
647    /// <div></div> => jsx('div', null);
648    ///
649    /// # Classic
650    ///
651    /// <div></div> => React.createElement('div', null);
652    fn jsx_elem_to_expr(&mut self, el: JSXElement) -> Expr {
653        let top_level_node = self.top_level_node;
654        let mut span = el.span();
655        let use_create_element = should_use_create_element(&el.opening.attrs);
656        self.top_level_node = false;
657
658        let name = self.jsx_name(el.opening.name);
659
660        if let Some(comments) = &self.comments {
661            if span.lo.is_dummy() {
662                span.lo = Span::dummy_with_cmt().lo;
663            }
664
665            comments.add_pure_comment(span.lo);
666        }
667
668        match self.runtime {
669            Runtime::Automatic => {
670                // function jsx(tagName: string, props: { children: Node[], ... }, key: string)
671
672                let mut props_obj = ObjectLit {
673                    span: DUMMY_SP,
674                    props: Vec::new(),
675                };
676
677                let mut key = None;
678                let mut source_props = None;
679                let mut self_props = None;
680
681                for attr in el.opening.attrs {
682                    match attr {
683                        JSXAttrOrSpread::JSXAttr(attr) => {
684                            //
685                            match attr.name {
686                                JSXAttrName::Ident(i) => {
687                                    //
688                                    if !use_create_element && i.sym == "key" {
689                                        key = attr
690                                            .value
691                                            .and_then(jsx_attr_value_to_expr)
692                                            .map(|expr| expr.as_arg());
693
694                                        if key.is_none() {
695                                            HANDLER.with(|handler| {
696                                                handler
697                                                    .struct_span_err(
698                                                        i.span,
699                                                        "The value of property 'key' should not \
700                                                         be empty",
701                                                    )
702                                                    .emit();
703                                            });
704                                        }
705                                        continue;
706                                    }
707
708                                    if !use_create_element
709                                        && *i.sym == *"__source"
710                                        && self.development
711                                    {
712                                        if source_props.is_some() {
713                                            panic!("Duplicate __source is found");
714                                        }
715                                        source_props = attr
716                                            .value
717                                            .and_then(jsx_attr_value_to_expr)
718                                            .map(|expr| expr.as_arg());
719                                        assert_ne!(
720                                            source_props, None,
721                                            "value of property '__source' should not be empty"
722                                        );
723                                        continue;
724                                    }
725
726                                    if !use_create_element
727                                        && *i.sym == *"__self"
728                                        && self.development
729                                    {
730                                        if self_props.is_some() {
731                                            panic!("Duplicate __self is found");
732                                        }
733                                        self_props = attr
734                                            .value
735                                            .and_then(jsx_attr_value_to_expr)
736                                            .map(|expr| expr.as_arg());
737                                        assert_ne!(
738                                            self_props, None,
739                                            "value of property '__self' should not be empty"
740                                        );
741                                        continue;
742                                    }
743
744                                    let value = match attr.value {
745                                        Some(v) => jsx_attr_value_to_expr(v)
746                                            .expect("empty expression container?"),
747                                        None => true.into(),
748                                    };
749
750                                    // TODO: Check if `i` is a valid identifier.
751                                    let key = if i.sym.contains('-') {
752                                        PropName::Str(Str {
753                                            span: i.span,
754                                            raw: None,
755                                            value: i.sym,
756                                        })
757                                    } else {
758                                        PropName::Ident(i)
759                                    };
760                                    props_obj.props.push(PropOrSpread::Prop(Box::new(
761                                        Prop::KeyValue(KeyValueProp { key, value }),
762                                    )));
763                                }
764                                JSXAttrName::JSXNamespacedName(JSXNamespacedName {
765                                    ns,
766                                    name,
767                                    ..
768                                }) => {
769                                    if self.throw_if_namespace {
770                                        HANDLER.with(|handler| {
771                                            handler
772                                                .struct_span_err(
773                                                    span,
774                                                    "JSX Namespace is disabled by default because \
775                                                     react does not support it yet. You can \
776                                                     specify jsc.transform.react.throwIfNamespace \
777                                                     to false to override default behavior",
778                                                )
779                                                .emit()
780                                        });
781                                    }
782
783                                    let value = match attr.value {
784                                        Some(v) => jsx_attr_value_to_expr(v)
785                                            .expect("empty expression container?"),
786                                        None => true.into(),
787                                    };
788
789                                    let str_value = format!("{}:{}", ns.sym, name.sym);
790                                    let key = Str {
791                                        span,
792                                        raw: None,
793                                        value: str_value.into(),
794                                    };
795                                    let key = PropName::Str(key);
796
797                                    props_obj.props.push(PropOrSpread::Prop(Box::new(
798                                        Prop::KeyValue(KeyValueProp { key, value }),
799                                    )));
800                                }
801                            }
802                        }
803                        JSXAttrOrSpread::SpreadElement(attr) => match *attr.expr {
804                            Expr::Object(obj) => {
805                                props_obj.props.extend(obj.props);
806                            }
807                            _ => {
808                                props_obj.props.push(PropOrSpread::Spread(attr));
809                            }
810                        },
811                    }
812                }
813
814                let mut children = el
815                    .children
816                    .into_iter()
817                    .filter_map(|child| self.jsx_elem_child_to_expr(child))
818                    .map(Some)
819                    .collect::<Vec<_>>();
820
821                let use_jsxs = match children.len() {
822                    0 => false,
823                    1 if matches!(children.first(), Some(Some(child)) if child.spread.is_none()) => {
824                        if !use_create_element {
825                            props_obj
826                                .props
827                                .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
828                                    key: PropName::Ident(quote_ident!("children")),
829                                    value: children
830                                        .take()
831                                        .into_iter()
832                                        .next()
833                                        .flatten()
834                                        .unwrap()
835                                        .expr,
836                                }))));
837                        }
838
839                        false
840                    }
841                    _ => {
842                        props_obj
843                            .props
844                            .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
845                                key: PropName::Ident(quote_ident!("children")),
846                                value: ArrayLit {
847                                    span: DUMMY_SP,
848                                    elems: children.take(),
849                                }
850                                .into(),
851                            }))));
852                        true
853                    }
854                };
855
856                let jsx = if use_create_element {
857                    self.import_create_element
858                        .get_or_insert_with(|| private_ident!("_createElement"))
859                        .clone()
860                } else if use_jsxs && !self.development {
861                    self.import_jsxs
862                        .get_or_insert_with(|| private_ident!("_jsxs"))
863                        .clone()
864                } else {
865                    let jsx = if self.development { "_jsxDEV" } else { "_jsx" };
866                    self.import_jsx
867                        .get_or_insert_with(|| private_ident!(jsx))
868                        .clone()
869                };
870
871                self.top_level_node = top_level_node;
872
873                let args = once(name.as_arg()).chain(once(props_obj.as_arg()));
874                let args = if use_create_element {
875                    args.chain(children.into_iter().flatten()).collect()
876                } else if self.development {
877                    // set undefined literal to key if key is None
878                    let key = match key {
879                        Some(key) => key,
880                        None => Expr::undefined(DUMMY_SP).as_arg(),
881                    };
882
883                    // set undefined literal to __source if __source is None
884                    let source_props = match source_props {
885                        Some(source_props) => source_props,
886                        None => Expr::undefined(DUMMY_SP).as_arg(),
887                    };
888
889                    // set undefined literal to __self if __self is None
890                    let self_props = match self_props {
891                        Some(self_props) => self_props,
892                        None => Expr::undefined(DUMMY_SP).as_arg(),
893                    };
894                    args.chain(once(key))
895                        .chain(once(use_jsxs.as_arg()))
896                        .chain(once(source_props))
897                        .chain(once(self_props))
898                        .collect()
899                } else {
900                    args.chain(key).collect()
901                };
902                CallExpr {
903                    span,
904                    callee: jsx.as_callee(),
905                    args,
906                    ..Default::default()
907                }
908                .into()
909            }
910            Runtime::Classic => {
911                CallExpr {
912                    span,
913                    callee: (*self.pragma).clone().as_callee(),
914                    args: iter::once(name.as_arg())
915                        .chain(iter::once({
916                            // Attributes
917                            self.fold_attrs_for_classic(el.opening.attrs).as_arg()
918                        }))
919                        .chain({
920                            // Children
921                            el.children
922                                .into_iter()
923                                .filter_map(|c| self.jsx_elem_child_to_expr(c))
924                        })
925                        .collect(),
926                    ..Default::default()
927                }
928                .into()
929            }
930            Runtime::Preserve => unreachable!(),
931        }
932    }
933
934    fn jsx_elem_child_to_expr(&mut self, c: JSXElementChild) -> Option<ExprOrSpread> {
935        self.top_level_node = false;
936
937        Some(match c {
938            JSXElementChild::JSXText(text) => {
939                // TODO(kdy1): Optimize
940                let value = jsx_text_to_str(&text.value);
941                let s = Str {
942                    span: text.span,
943                    raw: None,
944                    value,
945                };
946
947                if s.value.is_empty() {
948                    return None;
949                }
950
951                Lit::Str(s).as_arg()
952            }
953            JSXElementChild::JSXExprContainer(JSXExprContainer {
954                expr: JSXExpr::Expr(e),
955                ..
956            }) => e.as_arg(),
957            JSXElementChild::JSXExprContainer(JSXExprContainer {
958                expr: JSXExpr::JSXEmptyExpr(..),
959                ..
960            }) => return None,
961            JSXElementChild::JSXElement(el) => self.jsx_elem_to_expr(*el).as_arg(),
962            JSXElementChild::JSXFragment(el) => self.jsx_frag_to_expr(el).as_arg(),
963            JSXElementChild::JSXSpreadChild(JSXSpreadChild { span, expr, .. }) => ExprOrSpread {
964                spread: Some(span),
965                expr,
966            },
967        })
968    }
969
970    fn fold_attrs_for_classic(&mut self, attrs: Vec<JSXAttrOrSpread>) -> Box<Expr> {
971        if attrs.is_empty() {
972            return Lit::Null(Null { span: DUMMY_SP }).into();
973        }
974        let attr_cnt = attrs.len();
975
976        let mut props = Vec::new();
977        for attr in attrs {
978            match attr {
979                JSXAttrOrSpread::JSXAttr(attr) => {
980                    props.push(PropOrSpread::Prop(Box::new(self.attr_to_prop(attr))))
981                }
982                JSXAttrOrSpread::SpreadElement(spread) => {
983                    if attr_cnt == 1 {
984                        return spread.expr;
985                    }
986                    // babel does some optimizations
987                    match *spread.expr {
988                        Expr::Object(obj) => props.extend(obj.props),
989                        _ => props.push(PropOrSpread::Spread(spread)),
990                    }
991                }
992            }
993        }
994
995        let obj = ObjectLit {
996            span: DUMMY_SP,
997            props,
998        };
999
1000        obj.into()
1001    }
1002
1003    fn attr_to_prop(&mut self, a: JSXAttr) -> Prop {
1004        let key = to_prop_name(a.name);
1005        let value = a
1006            .value
1007            .map(|v| match v {
1008                JSXAttrValue::Lit(Lit::Str(s)) => {
1009                    let value = transform_jsx_attr_str(&s.value);
1010
1011                    Lit::Str(Str {
1012                        span: s.span,
1013                        raw: None,
1014                        value: value.into(),
1015                    })
1016                    .into()
1017                }
1018                JSXAttrValue::JSXExprContainer(JSXExprContainer {
1019                    expr: JSXExpr::Expr(e),
1020                    ..
1021                }) => e,
1022                JSXAttrValue::JSXElement(element) => Box::new(self.jsx_elem_to_expr(*element)),
1023                JSXAttrValue::JSXFragment(fragment) => Box::new(self.jsx_frag_to_expr(fragment)),
1024                JSXAttrValue::Lit(lit) => Box::new(lit.into()),
1025                JSXAttrValue::JSXExprContainer(JSXExprContainer {
1026                    span: _,
1027                    expr: JSXExpr::JSXEmptyExpr(_),
1028                }) => unreachable!("attr_to_prop(JSXEmptyExpr)"),
1029            })
1030            .unwrap_or_else(|| {
1031                Lit::Bool(Bool {
1032                    span: key.span(),
1033                    value: true,
1034                })
1035                .into()
1036            });
1037        Prop::KeyValue(KeyValueProp { key, value })
1038    }
1039}
1040
1041impl<C> Jsx<C>
1042where
1043    C: Comments,
1044{
1045    /// If we found required jsx directives, we returns true.
1046    fn parse_directives(&mut self, span: Span) -> bool {
1047        let mut found = false;
1048
1049        let directives = self.comments.with_leading(span.lo, |comments| {
1050            JsxDirectives::from_comments(&self.cm, span, comments, self.top_level_mark)
1051        });
1052
1053        let JsxDirectives {
1054            runtime,
1055            import_source,
1056            pragma,
1057            pragma_frag,
1058        } = directives;
1059
1060        if let Some(runtime) = runtime {
1061            found = true;
1062            self.runtime = runtime;
1063        }
1064
1065        if let Some(import_source) = import_source {
1066            found = true;
1067            self.import_source = import_source;
1068        }
1069
1070        if let Some(pragma) = pragma {
1071            if let Runtime::Automatic = self.runtime {
1072                HANDLER.with(|handler| {
1073                    handler
1074                        .struct_span_err(
1075                            pragma.span(),
1076                            "pragma cannot be set when runtime is automatic",
1077                        )
1078                        .emit()
1079                });
1080            }
1081
1082            found = true;
1083            self.pragma = pragma;
1084        }
1085
1086        if let Some(pragma_frag) = pragma_frag {
1087            if let Runtime::Automatic = self.runtime {
1088                HANDLER.with(|handler| {
1089                    handler
1090                        .struct_span_err(
1091                            pragma_frag.span(),
1092                            "pragmaFrag cannot be set when runtime is automatic",
1093                        )
1094                        .emit()
1095                });
1096            }
1097
1098            found = true;
1099            self.pragma_frag = pragma_frag;
1100        }
1101
1102        found
1103    }
1104}
1105
1106impl<C> VisitMut for Jsx<C>
1107where
1108    C: Comments,
1109{
1110    noop_visit_mut_type!();
1111
1112    fn visit_mut_expr(&mut self, expr: &mut Expr) {
1113        let top_level_node = self.top_level_node;
1114        let mut did_work = false;
1115
1116        if let Expr::JSXElement(el) = expr {
1117            did_work = true;
1118            // <div></div> => React.createElement('div', null);
1119            *expr = self.jsx_elem_to_expr(*el.take());
1120        } else if let Expr::JSXFragment(frag) = expr {
1121            // <></> => React.createElement(React.Fragment, null);
1122            did_work = true;
1123            *expr = self.jsx_frag_to_expr(frag.take());
1124        } else if let Expr::Paren(ParenExpr {
1125            expr: inner_expr, ..
1126        }) = expr
1127        {
1128            if let Expr::JSXElement(el) = &mut **inner_expr {
1129                did_work = true;
1130                *expr = self.jsx_elem_to_expr(*el.take());
1131            } else if let Expr::JSXFragment(frag) = &mut **inner_expr {
1132                // <></> => React.createElement(React.Fragment, null);
1133                did_work = true;
1134                *expr = self.jsx_frag_to_expr(frag.take());
1135            }
1136        }
1137
1138        if did_work {
1139            self.top_level_node = false;
1140        }
1141
1142        expr.visit_mut_children_with(self);
1143
1144        self.top_level_node = top_level_node;
1145    }
1146
1147    fn visit_mut_module(&mut self, module: &mut Module) {
1148        self.parse_directives(module.span);
1149
1150        for item in &module.body {
1151            let span = item.span();
1152            if self.parse_directives(span) {
1153                break;
1154            }
1155        }
1156
1157        module.visit_mut_children_with(self);
1158
1159        if self.runtime == Runtime::Automatic {
1160            self.inject_runtime(&mut module.body, |imports, src, stmts| {
1161                let specifiers = imports
1162                    .into_iter()
1163                    .map(|(local, imported)| {
1164                        ImportSpecifier::Named(ImportNamedSpecifier {
1165                            span: DUMMY_SP,
1166                            local,
1167                            imported: Some(ModuleExportName::Ident(imported.into())),
1168                            is_type_only: false,
1169                        })
1170                    })
1171                    .collect();
1172
1173                prepend_stmt(
1174                    stmts,
1175                    ImportDecl {
1176                        span: DUMMY_SP,
1177                        specifiers,
1178                        src: Str {
1179                            span: DUMMY_SP,
1180                            raw: None,
1181                            value: src.into(),
1182                        }
1183                        .into(),
1184                        type_only: Default::default(),
1185                        with: Default::default(),
1186                        phase: Default::default(),
1187                    }
1188                    .into(),
1189                )
1190            });
1191        }
1192    }
1193
1194    fn visit_mut_script(&mut self, script: &mut Script) {
1195        self.parse_directives(script.span);
1196
1197        for item in &script.body {
1198            let span = item.span();
1199            if self.parse_directives(span) {
1200                break;
1201            }
1202        }
1203
1204        script.visit_mut_children_with(self);
1205
1206        if self.runtime == Runtime::Automatic {
1207            let mark = self.unresolved_mark;
1208            self.inject_runtime(&mut script.body, |imports, src, stmts| {
1209                prepend_stmt(stmts, add_require(imports, src, mark))
1210            });
1211        }
1212    }
1213}
1214
1215// const { createElement } = require('react')
1216// const { jsx: jsx } = require('react/jsx-runtime')
1217fn add_require(imports: Vec<(Ident, IdentName)>, src: &str, unresolved_mark: Mark) -> Stmt {
1218    VarDecl {
1219        span: DUMMY_SP,
1220        kind: VarDeclKind::Const,
1221        declare: false,
1222        decls: vec![VarDeclarator {
1223            span: DUMMY_SP,
1224            name: Pat::Object(ObjectPat {
1225                span: DUMMY_SP,
1226                props: imports
1227                    .into_iter()
1228                    .map(|(local, imported)| {
1229                        if imported.sym != local.sym {
1230                            ObjectPatProp::KeyValue(KeyValuePatProp {
1231                                key: PropName::Ident(imported),
1232                                value: Box::new(Pat::Ident(local.into())),
1233                            })
1234                        } else {
1235                            ObjectPatProp::Assign(AssignPatProp {
1236                                span: DUMMY_SP,
1237                                key: local.into(),
1238                                value: None,
1239                            })
1240                        }
1241                    })
1242                    .collect(),
1243                optional: false,
1244                type_ann: None,
1245            }),
1246            // require('react')
1247            init: Some(Box::new(Expr::Call(CallExpr {
1248                span: DUMMY_SP,
1249                callee: Callee::Expr(Box::new(Expr::Ident(Ident {
1250                    ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
1251                    sym: atom!("require"),
1252                    optional: false,
1253                    ..Default::default()
1254                }))),
1255                args: vec![ExprOrSpread {
1256                    spread: None,
1257                    expr: Box::new(Expr::Lit(Lit::Str(Str {
1258                        span: DUMMY_SP,
1259                        value: src.into(),
1260                        raw: None,
1261                    }))),
1262                }],
1263                ..Default::default()
1264            }))),
1265            definite: false,
1266        }],
1267        ..Default::default()
1268    }
1269    .into()
1270}
1271
1272impl<C> Jsx<C>
1273where
1274    C: Comments,
1275{
1276    fn jsx_name(&self, name: JSXElementName) -> Box<Expr> {
1277        let span = name.span();
1278        match name {
1279            JSXElementName::Ident(i) => {
1280                if i.sym == "this" {
1281                    return ThisExpr { span }.into();
1282                }
1283
1284                // If it starts with lowercase
1285                if i.as_ref().starts_with(|c: char| c.is_ascii_lowercase()) {
1286                    Lit::Str(Str {
1287                        span,
1288                        raw: None,
1289                        value: i.sym,
1290                    })
1291                    .into()
1292                } else {
1293                    i.into()
1294                }
1295            }
1296            JSXElementName::JSXNamespacedName(JSXNamespacedName {
1297                ref ns, ref name, ..
1298            }) => {
1299                if self.throw_if_namespace {
1300                    HANDLER.with(|handler| {
1301                        handler
1302                            .struct_span_err(
1303                                span,
1304                                "JSX Namespace is disabled by default because react does not \
1305                                 support it yet. You can specify \
1306                                 jsc.transform.react.throwIfNamespace to false to override \
1307                                 default behavior",
1308                            )
1309                            .emit()
1310                    });
1311                }
1312
1313                let value = format!("{}:{}", ns.sym, name.sym);
1314
1315                Lit::Str(Str {
1316                    span,
1317                    raw: None,
1318                    value: value.into(),
1319                })
1320                .into()
1321            }
1322            JSXElementName::JSXMemberExpr(JSXMemberExpr { obj, prop, .. }) => {
1323                fn convert_obj(obj: JSXObject) -> Box<Expr> {
1324                    let span = obj.span();
1325
1326                    (match obj {
1327                        JSXObject::Ident(i) => {
1328                            if i.sym == "this" {
1329                                Expr::This(ThisExpr { span })
1330                            } else {
1331                                i.into()
1332                            }
1333                        }
1334                        JSXObject::JSXMemberExpr(e) => MemberExpr {
1335                            span,
1336                            obj: convert_obj(e.obj),
1337                            prop: MemberProp::Ident(e.prop),
1338                        }
1339                        .into(),
1340                    })
1341                    .into()
1342                }
1343                MemberExpr {
1344                    span,
1345                    obj: convert_obj(obj),
1346                    prop: MemberProp::Ident(prop),
1347                }
1348                .into()
1349            }
1350        }
1351    }
1352}
1353
1354fn to_prop_name(n: JSXAttrName) -> PropName {
1355    let span = n.span();
1356
1357    match n {
1358        JSXAttrName::Ident(i) => {
1359            if i.sym.contains('-') {
1360                PropName::Str(Str {
1361                    span,
1362                    raw: None,
1363                    value: i.sym,
1364                })
1365            } else {
1366                PropName::Ident(i)
1367            }
1368        }
1369        JSXAttrName::JSXNamespacedName(JSXNamespacedName { ns, name, .. }) => {
1370            let value = format!("{}:{}", ns.sym, name.sym);
1371
1372            PropName::Str(Str {
1373                span,
1374                raw: None,
1375                value: value.into(),
1376            })
1377        }
1378    }
1379}
1380
1381/// https://github.com/microsoft/TypeScript/blob/9e20e032effad965567d4a1e1c30d5433b0a3332/src/compiler/transformers/jsx.ts#L572-L608
1382///
1383/// JSX trims whitespace at the end and beginning of lines, except that the
1384/// start/end of a tag is considered a start/end of a line only if that line is
1385/// on the same line as the closing tag. See examples in
1386/// tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx
1387/// See also https://www.w3.org/TR/html4/struct/text.html#h-9.1 and https://www.w3.org/TR/CSS2/text.html#white-space-model
1388///
1389/// An equivalent algorithm would be:
1390/// - If there is only one line, return it.
1391/// - If there is only whitespace (but multiple lines), return `undefined`.
1392/// - Split the text into lines.
1393/// - 'trimRight' the first line, 'trimLeft' the last line, 'trim' middle lines.
1394/// - Decode entities on each line (individually).
1395/// - Remove empty lines and join the rest with " ".
1396#[inline]
1397fn jsx_text_to_str(t: &str) -> Atom {
1398    let mut acc: Option<String> = None;
1399    let mut only_line: Option<&str> = None;
1400    let mut first_non_whitespace: Option<usize> = Some(0);
1401    let mut last_non_whitespace: Option<usize> = None;
1402
1403    for (index, c) in t.char_indices() {
1404        if is_line_terminator(c) {
1405            if let (Some(first), Some(last)) = (first_non_whitespace, last_non_whitespace) {
1406                let line_text = &t[first..last];
1407                add_line_of_jsx_text(line_text, &mut acc, &mut only_line);
1408            }
1409            first_non_whitespace = None;
1410        } else if !is_white_space_single_line(c) {
1411            last_non_whitespace = Some(index + c.len_utf8());
1412            if first_non_whitespace.is_none() {
1413                first_non_whitespace.replace(index);
1414            }
1415        }
1416    }
1417
1418    if let Some(first) = first_non_whitespace {
1419        let line_text = &t[first..];
1420        add_line_of_jsx_text(line_text, &mut acc, &mut only_line);
1421    }
1422
1423    if let Some(acc) = acc {
1424        acc.into()
1425    } else if let Some(only_line) = only_line {
1426        only_line.into()
1427    } else {
1428        "".into()
1429    }
1430}
1431
1432/// [TODO]: Re-validate this whitespace handling logic.
1433///
1434/// We cannot use [swc_ecma_utils::str::is_white_space_single_line] because
1435/// HTML entities (like `&nbsp;` → `\u{00a0}`) are pre-processed by the parser,
1436/// making it impossible to distinguish them from literal Unicode characters. We
1437/// should never trim HTML entities.
1438///
1439/// As a reference, Babel only trims regular spaces and tabs, so this is a
1440/// simplified implementation already in use.
1441/// https://github.com/babel/babel/blob/e5c8dc7330cb2f66c37637677609df90b31ff0de/packages/babel-types/src/utils/react/cleanJSXElementLiteralChild.ts#L28-L39
1442fn is_white_space_single_line(c: char) -> bool {
1443    matches!(c, ' ' | '\t')
1444}
1445
1446// less allocations trick from OXC
1447// https://github.com/oxc-project/oxc/blob/4c35f4abb6874bd741b84b34df7889637425e9ea/crates/oxc_transformer/src/jsx/jsx_impl.rs#L1061-L1091
1448fn add_line_of_jsx_text<'a>(
1449    trimmed_line: &'a str,
1450    acc: &mut Option<String>,
1451    only_line: &mut Option<&'a str>,
1452) {
1453    if let Some(buffer) = acc.as_mut() {
1454        // Already some text in accumulator. Push a space before this line is added to
1455        // `acc`.
1456        buffer.push(' ');
1457    } else if let Some(only_line_content) = only_line.take() {
1458        // This is the 2nd line containing text. Previous line did not contain any HTML
1459        // entities. Generate an accumulator containing previous line and a
1460        // trailing space. Current line will be added to the accumulator after
1461        // it.
1462        let mut buffer = String::with_capacity(trimmed_line.len() * 2); // rough estimate
1463        buffer.push_str(only_line_content);
1464        buffer.push(' ');
1465        *acc = Some(buffer);
1466    }
1467
1468    // [TODO]: Decode any HTML entities in this line
1469
1470    // For now, just use the trimmed line directly
1471    if let Some(buffer) = acc.as_mut() {
1472        buffer.push_str(trimmed_line);
1473    } else {
1474        // This is the first line containing text, and there are no HTML entities in
1475        // this line. Record this line in `only_line`.
1476        // If this turns out to be the only line, we won't need to construct a String,
1477        // so avoid all copying.
1478        *only_line = Some(trimmed_line);
1479    }
1480}
1481
1482fn jsx_attr_value_to_expr(v: JSXAttrValue) -> Option<Box<Expr>> {
1483    Some(match v {
1484        JSXAttrValue::Lit(Lit::Str(s)) => {
1485            let value = transform_jsx_attr_str(&s.value);
1486
1487            Lit::Str(Str {
1488                span: s.span,
1489                raw: None,
1490                value: value.into(),
1491            })
1492            .into()
1493        }
1494        JSXAttrValue::Lit(lit) => Box::new(lit.into()),
1495        JSXAttrValue::JSXExprContainer(e) => match e.expr {
1496            JSXExpr::JSXEmptyExpr(_) => None?,
1497            JSXExpr::Expr(e) => e,
1498        },
1499        JSXAttrValue::JSXElement(e) => e.into(),
1500        JSXAttrValue::JSXFragment(f) => f.into(),
1501    })
1502}
1503
1504fn transform_jsx_attr_str(v: &str) -> String {
1505    let single_quote = false;
1506    let mut buf = String::with_capacity(v.len());
1507    let mut iter = v.chars().peekable();
1508
1509    while let Some(c) = iter.next() {
1510        match c {
1511            '\u{0008}' => buf.push_str("\\b"),
1512            '\u{000c}' => buf.push_str("\\f"),
1513            ' ' => buf.push(' '),
1514
1515            '\n' | '\r' | '\t' => {
1516                buf.push(' ');
1517
1518                while let Some(' ') = iter.peek() {
1519                    iter.next();
1520                }
1521            }
1522            '\u{000b}' => buf.push_str("\\v"),
1523            '\0' => buf.push_str("\\x00"),
1524
1525            '\'' if single_quote => buf.push_str("\\'"),
1526            '"' if !single_quote => buf.push('\"'),
1527
1528            '\x01'..='\x0f' | '\x10'..='\x1f' => {
1529                buf.push(c);
1530            }
1531
1532            '\x20'..='\x7e' => {
1533                //
1534                buf.push(c);
1535            }
1536            '\u{7f}'..='\u{ff}' => {
1537                buf.push(c);
1538            }
1539
1540            _ => {
1541                buf.push(c);
1542            }
1543        }
1544    }
1545
1546    buf
1547}