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