diff --git a/crates/swc_ecma_transforms_react/src/jsx/mod.rs b/crates/swc_ecma_transforms_react/src/jsx/mod.rs index 23bc1cc254ea..5a785d47f683 100644 --- a/crates/swc_ecma_transforms_react/src/jsx/mod.rs +++ b/crates/swc_ecma_transforms_react/src/jsx/mod.rs @@ -18,7 +18,9 @@ use swc_common::{ use swc_ecma_ast::*; use swc_ecma_parser::{Parser, StringInput, Syntax}; use swc_ecma_transforms_base::helper; -use swc_ecma_utils::{drop_span, member_expr, prepend, private_ident, quote_ident, ExprFactory}; +use swc_ecma_utils::{ + drop_span, member_expr, prepend, private_ident, quote_ident, undefined, ExprFactory, +}; use swc_ecma_visit::{as_folder, noop_visit_mut_type, Fold, VisitMut, VisitMutWith}; mod static_check; @@ -205,6 +207,7 @@ where pragma_frag: parse_expr_for_jsx(&cm, "pragmaFrag", options.pragma_frag, top_level_mark), use_builtins: options.use_builtins, use_spread: options.use_spread, + development: options.development, throw_if_namespace: options.throw_if_namespace, top_level_node: true, }) @@ -237,6 +240,7 @@ where pragma_frag: Arc>, use_builtins: bool, use_spread: bool, + development: bool, throw_if_namespace: bool, } @@ -346,13 +350,14 @@ where match self.runtime { Runtime::Automatic => { - let jsx = if use_jsxs { + let jsx = if use_jsxs && !self.development { self.import_jsxs .get_or_insert_with(|| private_ident!("_jsxs")) .clone() } else { + let jsx = if self.development { "_jsxDEV" } else { "_jsx" }; self.import_jsx - .get_or_insert_with(|| private_ident!("_jsx")) + .get_or_insert_with(|| private_ident!(jsx)) .clone() }; @@ -396,10 +401,26 @@ where } } + let args = once(fragment.as_arg()).chain(once(props_obj.as_arg())); + + let args = if self.development { + args.chain(once(undefined(DUMMY_SP).as_arg())) + .chain(once( + Lit::Bool(Bool { + span: DUMMY_SP, + value: use_jsxs, + }) + .as_arg(), + )) + .collect() + } else { + args.collect() + }; + Expr::Call(CallExpr { span, callee: jsx.as_callee(), - args: vec![fragment.as_arg(), props_obj.as_arg()], + args, type_args: None, }) } @@ -452,13 +473,14 @@ where self.import_create_element .get_or_insert_with(|| private_ident!("_createElement")) .clone() - } else if use_jsxs { + } else if use_jsxs && !self.development { self.import_jsxs .get_or_insert_with(|| private_ident!("_jsxs")) .clone() } else { + let jsx = if self.development { "_jsxDEV" } else { "_jsx" }; self.import_jsx - .get_or_insert_with(|| private_ident!("_jsx")) + .get_or_insert_with(|| private_ident!(jsx)) .clone() }; @@ -468,6 +490,8 @@ where }; let mut key = None; + let mut source_props = None; + let mut self_props = None; for attr in el.opening.attrs { match attr { @@ -489,6 +513,44 @@ where continue; } + if !use_create_element + && *i.sym == *"__source" + && self.development + { + if source_props.is_some() { + panic!("Duplicate __source is found"); + } + source_props = attr + .value + .map(jsx_attr_value_to_expr) + .flatten() + .map(|expr| ExprOrSpread { expr, spread: None }); + assert_ne!( + source_props, None, + "value of property '__source' should not be empty" + ); + continue; + } + + if !use_create_element + && *i.sym == *"__self" + && self.development + { + if self_props.is_some() { + panic!("Duplicate __self is found"); + } + self_props = attr + .value + .map(jsx_attr_value_to_expr) + .flatten() + .map(|expr| ExprOrSpread { expr, spread: None }); + assert_ne!( + self_props, None, + "value of property '__self' should not be empty" + ); + continue; + } + let value = match attr.value { Some(v) => jsx_attr_value_to_expr(v) .expect("empty expression container?"), @@ -596,13 +658,52 @@ where self.top_level_node = top_level_node; + let args = once(name.as_arg()).chain(once(props_obj.as_arg())); + let args = if self.development { + // set undefined literal to key if key is None + let key = match key { + Some(key) => key, + None => ExprOrSpread { + spread: None, + expr: undefined(DUMMY_SP), + }, + }; + + // set undefined literal to __source if __source is None + let source_props = match source_props { + Some(source_props) => source_props, + None => ExprOrSpread { + spread: None, + expr: undefined(DUMMY_SP), + }, + }; + + // set undefined literal to __self if __self is None + let self_props = match self_props { + Some(self_props) => self_props, + None => ExprOrSpread { + spread: None, + expr: undefined(DUMMY_SP), + }, + }; + args.chain(once(key)) + .chain(once( + Lit::Bool(Bool { + span: DUMMY_SP, + value: use_jsxs, + }) + .as_arg(), + )) + .chain(once(source_props)) + .chain(once(self_props)) + .collect() + } else { + args.chain(key).collect() + }; Expr::Call(CallExpr { span, callee: jsx.as_callee(), - args: once(name.as_arg()) - .chain(once(props_obj.as_arg())) - .chain(key) - .collect(), + args, type_args: Default::default(), }) } @@ -970,36 +1071,63 @@ where ); } - let imports = self - .import_jsx - .take() - .map(|local| ImportNamedSpecifier { - span: DUMMY_SP, - local, - imported: Some(quote_ident!("jsx")), - is_type_only: false, - }) - .into_iter() - .chain(self.import_jsxs.take().map(|local| ImportNamedSpecifier { - span: DUMMY_SP, - local, - imported: Some(quote_ident!("jsxs")), - is_type_only: false, - })) - .chain( - self.import_fragment - .take() - .map(|local| ImportNamedSpecifier { - span: DUMMY_SP, - local, - imported: Some(quote_ident!("Fragment")), - is_type_only: false, - }), - ) - .map(ImportSpecifier::Named) - .collect::>(); + let imports = self.import_jsx.take(); + let imports = if self.development { + imports + .map(|local| ImportNamedSpecifier { + span: DUMMY_SP, + local, + imported: Some(quote_ident!("jsxDEV")), + is_type_only: false, + }) + .into_iter() + .chain( + self.import_fragment + .take() + .map(|local| ImportNamedSpecifier { + span: DUMMY_SP, + local, + imported: Some(quote_ident!("Fragment")), + is_type_only: false, + }), + ) + .map(ImportSpecifier::Named) + .collect::>() + } else { + imports + .map(|local| ImportNamedSpecifier { + span: DUMMY_SP, + local, + imported: Some(quote_ident!("jsx")), + is_type_only: false, + }) + .into_iter() + .chain(self.import_jsxs.take().map(|local| ImportNamedSpecifier { + span: DUMMY_SP, + local, + imported: Some(quote_ident!("jsxs")), + is_type_only: false, + })) + .chain( + self.import_fragment + .take() + .map(|local| ImportNamedSpecifier { + span: DUMMY_SP, + local, + imported: Some(quote_ident!("Fragment")), + is_type_only: false, + }), + ) + .map(ImportSpecifier::Named) + .collect::>() + }; if !imports.is_empty() { + let jsx_runtime = if self.development { + "jsx-dev-runtime" + } else { + "jsx-runtime" + }; prepend( &mut module.body, ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { @@ -1007,7 +1135,7 @@ where specifiers: imports, src: Str { span: DUMMY_SP, - value: format!("{}/jsx-runtime", self.import_source).into(), + value: format!("{}/{}", self.import_source, jsx_runtime).into(), has_escape: false, kind: Default::default(), }, diff --git a/crates/swc_ecma_transforms_react/src/jsx/tests.rs b/crates/swc_ecma_transforms_react/src/jsx/tests.rs index ed7d4ade4988..8c26c058070b 100644 --- a/crates/swc_ecma_transforms_react/src/jsx/tests.rs +++ b/crates/swc_ecma_transforms_react/src/jsx/tests.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use super::*; -use crate::display_name; +use crate::{display_name, react}; use std::path::PathBuf; use swc_common::{chain, Mark}; use swc_ecma_parser::EsConfig; @@ -72,6 +72,28 @@ fn fixture_tr(t: &mut Tester, mut options: FixtureOptions) -> impl Fold { display_name(), ) } + +fn integration_tr(t: &mut Tester, mut options: FixtureOptions) -> impl Fold { + let top_level_mark = Mark::fresh(Mark::root()); + + options.options.next = options.babel_8_breaking || options.options.runtime.is_some(); + + if !options.babel_8_breaking && options.options.runtime.is_none() { + options.options.runtime = Some(Runtime::Classic); + } + + options.options.use_builtins |= options.use_builtins; + chain!( + resolver_with_mark(top_level_mark), + react( + t.cm.clone(), + Some(t.comments.clone()), + options.options, + top_level_mark + ), + display_name(), + ) +} test!( ::swc_ecma_parser::Syntax::Es(::swc_ecma_parser::EsConfig { jsx: true, @@ -1385,3 +1407,24 @@ fn fixture(input: PathBuf) { &output, ); } + +#[testing::fixture("tests/integration/fixture/**/input.js")] +fn integration(input: PathBuf) { + let mut output = input.with_file_name("output.js"); + if !output.exists() { + output = input.with_file_name("output.mjs"); + } + + test_fixture_allowing_error( + Syntax::Es(EsConfig { + jsx: true, + ..Default::default() + }), + &|t| { + let options = parse_options(input.parent().unwrap()); + integration_tr(t, options) + }, + &input, + &output, + ); +} diff --git a/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-dev-transform/input.js b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-dev-transform/input.js new file mode 100644 index 000000000000..5374eb97cbcb --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-dev-transform/input.js @@ -0,0 +1,8 @@ +const App = ( +
+
+ <> +
hoge
+ +
+); diff --git a/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-dev-transform/options.json b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-dev-transform/options.json new file mode 100644 index 000000000000..e7394a9a83f4 --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-dev-transform/options.json @@ -0,0 +1 @@ +{ "runtime": "automatic", "development": true } diff --git a/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-dev-transform/output.mjs b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-dev-transform/output.mjs new file mode 100644 index 000000000000..437967648c36 --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-dev-transform/output.mjs @@ -0,0 +1,24 @@ +import { jsxDEV as _jsxDEV, Fragment as _Fragment } from "react/jsx-dev-runtime"; +const App = _jsxDEV("div", { + children: [ + _jsxDEV("div", { + }, void 0, false, { + fileName: "input.js", + lineNumber: 3, + columnNumber: 9 + }, this), + _jsxDEV(_Fragment, { + children: _jsxDEV("div", { + children: "hoge" + }, 1, false, { + fileName: "input.js", + lineNumber: 5, + columnNumber: 13 + }, this) + }, void 0, false) + ] +}, void 0, true, { + fileName: "input.js", + lineNumber: 2, + columnNumber: 5 +}, this); \ No newline at end of file diff --git a/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-transform/input.js b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-transform/input.js new file mode 100644 index 000000000000..5374eb97cbcb --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-transform/input.js @@ -0,0 +1,8 @@ +const App = ( +
+
+ <> +
hoge
+ +
+); diff --git a/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-transform/options.json b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-transform/options.json new file mode 100644 index 000000000000..8c29c48c6f58 --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-transform/options.json @@ -0,0 +1 @@ +{ "runtime": "automatic" } diff --git a/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-transform/output.mjs b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-transform/output.mjs new file mode 100644 index 000000000000..131d1761b91b --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsx-transform/output.mjs @@ -0,0 +1,12 @@ +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +const App = _jsxs("div", { + children: [ + _jsx("div", { + }), + _jsx(_Fragment, { + children: _jsx("div", { + children: "hoge" + }, 1) + }) + ] +}); \ No newline at end of file diff --git a/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-args-with-fragment/input.js b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-args-with-fragment/input.js new file mode 100644 index 000000000000..1cbfdc88d670 --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-args-with-fragment/input.js @@ -0,0 +1,6 @@ +var x = ( + <> +
hoge
+
fuga
+ +); diff --git a/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-args-with-fragment/options.json b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-args-with-fragment/options.json new file mode 100644 index 000000000000..e7394a9a83f4 --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-args-with-fragment/options.json @@ -0,0 +1 @@ +{ "runtime": "automatic", "development": true } diff --git a/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-args-with-fragment/output.js b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-args-with-fragment/output.js new file mode 100644 index 000000000000..b331e93a032a --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-args-with-fragment/output.js @@ -0,0 +1,17 @@ +import { jsxDEV as _jsxDEV, Fragment as _Fragment } from "react/jsx-dev-runtime"; + +var x = /*#__PURE__*/_jsxDEV(_Fragment, { + children: [/*#__PURE__*/_jsxDEV("div", { + children: "hoge" + }, void 0, false, { + fileName: "input.js", + lineNumber: 3, + columnNumber: 9 + }, this), /*#__PURE__*/_jsxDEV("div", { + children: "fuga" + }, void 0, false, { + fileName: "input.js", + lineNumber: 4, + columnNumber: 9 + }, this)] +}, void 0, true); \ No newline at end of file diff --git a/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-fragment/input.js b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-fragment/input.js new file mode 100644 index 000000000000..8998b645e119 --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-fragment/input.js @@ -0,0 +1,6 @@ +const App = ( + <> +
hoge
+
fuga
+ +); diff --git a/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-fragment/options.json b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-fragment/options.json new file mode 100644 index 000000000000..e7394a9a83f4 --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-fragment/options.json @@ -0,0 +1 @@ +{ "runtime": "automatic", "development": true } diff --git a/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-fragment/output.mjs b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-fragment/output.mjs new file mode 100644 index 000000000000..2bc6cb3da902 --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/integration/fixture/jsxdev-fragment/output.mjs @@ -0,0 +1,17 @@ +import { jsxDEV as _jsxDEV, Fragment as _Fragment } from "react/jsx-dev-runtime"; + +const App = /*#__PURE__*/_jsxDEV(_Fragment, { + children: [/*#__PURE__*/_jsxDEV("div", { + children: "hoge" + }, void 0, false, { + fileName: "input.js", + lineNumber: 3, + columnNumber: 9 + }, this), /*#__PURE__*/_jsxDEV("div", { + children: "fuga" + }, void 0, false, { + fileName: "input.js", + lineNumber: 4, + columnNumber: 9 + }, this)] +}, void 0, true); \ No newline at end of file diff --git a/crates/swc_ecma_transforms_react/tests/integration/fixture/with-pragma/input.js b/crates/swc_ecma_transforms_react/tests/integration/fixture/with-pragma/input.js new file mode 100644 index 000000000000..92dc99d3ebf2 --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/integration/fixture/with-pragma/input.js @@ -0,0 +1,9 @@ +/**@jsxRuntime automatic */ +const App = ( +
+
+ <> +
hoge
+ +
+); diff --git a/crates/swc_ecma_transforms_react/tests/integration/fixture/with-pragma/options.json b/crates/swc_ecma_transforms_react/tests/integration/fixture/with-pragma/options.json new file mode 100644 index 000000000000..b9c76dfe86e8 --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/integration/fixture/with-pragma/options.json @@ -0,0 +1 @@ +{ "development": true } diff --git a/crates/swc_ecma_transforms_react/tests/integration/fixture/with-pragma/output.mjs b/crates/swc_ecma_transforms_react/tests/integration/fixture/with-pragma/output.mjs new file mode 100644 index 000000000000..70a7b0630a21 --- /dev/null +++ b/crates/swc_ecma_transforms_react/tests/integration/fixture/with-pragma/output.mjs @@ -0,0 +1,24 @@ +import { jsxDEV as _jsxDEV, Fragment as _Fragment } from "react/jsx-dev-runtime"; +const App = _jsxDEV("div", { + children: [ + _jsxDEV("div", { + }, void 0, false, { + fileName: "input.js", + lineNumber: 4, + columnNumber: 9 + }, this), + _jsxDEV(_Fragment, { + children: _jsxDEV("div", { + children: "hoge" + }, void 0, false, { + fileName: "input.js", + lineNumber: 6, + columnNumber: 13 + }, this) + }, void 0, false) + ] +}, void 0, true, { + fileName: "input.js", + lineNumber: 3, + columnNumber: 5 +}, this); \ No newline at end of file