From 3bc05faf276f033a86f949973a57348f11354023 Mon Sep 17 00:00:00 2001 From: Boshen <1430279+Boshen@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:20:54 +0000 Subject: [PATCH] feat(transformer): implement jsx spread child (#8763) closes #8690 --- crates/oxc_transformer/src/jsx/diagnostics.rs | 4 -- crates/oxc_transformer/src/jsx/jsx_impl.rs | 46 +++++++++++++++---- .../snapshots/semantic_typescript.snap | 15 ++++-- .../snapshots/oxc.snap.md | 4 +- .../spread-children-automatic/input.jsx | 1 + .../spread-children-automatic/options.json | 10 ++++ .../spread-children-automatic/output.js | 2 + .../spread-children-classic/input.jsx | 1 + .../spread-children-classic/options.json | 10 ++++ .../spread-children-classic/output.js | 1 + 10 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-automatic/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-automatic/options.json create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-automatic/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-classic/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-classic/options.json create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-classic/output.js diff --git a/crates/oxc_transformer/src/jsx/diagnostics.rs b/crates/oxc_transformer/src/jsx/diagnostics.rs index 0847aabaa39c3..cfc896ca8d3ce 100644 --- a/crates/oxc_transformer/src/jsx/diagnostics.rs +++ b/crates/oxc_transformer/src/jsx/diagnostics.rs @@ -30,7 +30,3 @@ pub fn valueless_key(span: Span) -> OxcDiagnostic { OxcDiagnostic::warn("Please provide an explicit key value. Using \"key\" as a shorthand for \"key={true}\" is not allowed.") .with_label(span) } - -pub fn spread_children_are_not_supported(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Spread children are not supported in React.").with_label(span) -} diff --git a/crates/oxc_transformer/src/jsx/jsx_impl.rs b/crates/oxc_transformer/src/jsx/jsx_impl.rs index d50e3a901bf83..0377f22db8460 100644 --- a/crates/oxc_transformer/src/jsx/jsx_impl.rs +++ b/crates/oxc_transformer/src/jsx/jsx_impl.rs @@ -625,7 +625,7 @@ impl<'a> JsxImpl<'a, '_> { // Append children to object properties in automatic mode if is_automatic { let mut children = ctx.ast.vec_from_iter( - children.iter().filter_map(|child| self.transform_jsx_child(child, ctx)), + children.iter().filter_map(|child| self.transform_jsx_child_automatic(child, ctx)), ); children_len = children.len(); if children_len != 0 { @@ -750,10 +750,7 @@ impl<'a> JsxImpl<'a, '_> { // React.createElement(type, arguments, ...children) // ^^^^^^^^^^^ arguments.extend( - children - .iter() - .filter_map(|child| self.transform_jsx_child(child, ctx)) - .map(Argument::from), + children.iter().filter_map(|child| self.transform_jsx_child_classic(child, ctx)), ); } @@ -883,6 +880,40 @@ impl<'a> JsxImpl<'a, '_> { } } + fn transform_jsx_child_automatic( + &mut self, + child: &JSXChild<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + // Align spread child behavior with esbuild. + // Instead of Babel throwing `Spread children are not supported in React.` + // `<>{...foo}` -> `jsxs(Fragment, { children: [ ...foo ] })` + if let JSXChild::Spread(e) = child { + // SAFETY: `ast.copy` is unsound! We need to fix. + let argument = unsafe { ctx.ast.copy(&e.expression) }; + let spread_element = ctx.ast.array_expression_element_spread_element(e.span, argument); + let elements = ctx.ast.vec1(spread_element); + return Some(ctx.ast.expression_array(e.span, elements, None)); + } + self.transform_jsx_child(child, ctx) + } + + fn transform_jsx_child_classic( + &mut self, + child: &JSXChild<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + // Align spread child behavior with esbuild. + // Instead of Babel throwing `Spread children are not supported in React.` + // `<>{...foo}` -> `React.createElement(React.Fragment, null, ...foo)` + if let JSXChild::Spread(e) = child { + // SAFETY: `ast.copy` is unsound! We need to fix. + let argument = unsafe { ctx.ast.copy(&e.expression) }; + return Some(ctx.ast.argument_spread_element(e.span, argument)); + } + self.transform_jsx_child(child, ctx).map(Argument::from) + } + fn transform_jsx_child( &mut self, child: &JSXChild<'a>, @@ -903,10 +934,7 @@ impl<'a> JsxImpl<'a, '_> { JSXChild::Fragment(e) => { Some(self.transform_jsx(&JSXElementOrFragment::Fragment(e), ctx)) } - JSXChild::Spread(e) => { - self.ctx.error(diagnostics::spread_children_are_not_supported(e.span)); - None - } + JSXChild::Spread(_) => unreachable!(), } } diff --git a/tasks/coverage/snapshots/semantic_typescript.snap b/tasks/coverage/snapshots/semantic_typescript.snap index 76cf4220b371a..98029a1d85e3d 100644 --- a/tasks/coverage/snapshots/semantic_typescript.snap +++ b/tasks/coverage/snapshots/semantic_typescript.snap @@ -39578,11 +39578,20 @@ after transform: ScopeId(0): [ScopeId(1), ScopeId(2)] rebuilt : ScopeId(0): [ScopeId(1)] tasks/coverage/typescript/tests/cases/conformance/jsx/tsxSpreadChildren.tsx -semantic error: Spread children are not supported in React. +semantic error: Bindings mismatch: +after transform: ScopeId(0): ["JSX", "React", "Todo", "TodoList", "_jsxFileName", "_objectSpread", "_reactJsxRuntime", "x"] +rebuilt : ScopeId(0): ["Todo", "TodoList", "_jsxFileName", "_objectSpread", "_reactJsxRuntime", "x"] +Scope children mismatch: +after transform: ScopeId(0): [ScopeId(1), ScopeId(4), ScopeId(5), ScopeId(6), ScopeId(7)] +rebuilt : ScopeId(0): [ScopeId(1), ScopeId(2)] tasks/coverage/typescript/tests/cases/conformance/jsx/tsxSpreadChildrenInvalidType.tsx -semantic error: Spread children are not supported in React. -Spread children are not supported in React. +semantic error: Bindings mismatch: +after transform: ScopeId(0): ["JSX", "React", "Todo", "TodoList", "TodoListNoError", "_jsxFileName", "_objectSpread", "_reactJsxRuntime", "x"] +rebuilt : ScopeId(0): ["Todo", "TodoList", "TodoListNoError", "_jsxFileName", "_objectSpread", "_reactJsxRuntime", "x"] +Scope children mismatch: +after transform: ScopeId(0): [ScopeId(1), ScopeId(4), ScopeId(5), ScopeId(6), ScopeId(7), ScopeId(8)] +rebuilt : ScopeId(0): [ScopeId(1), ScopeId(2), ScopeId(3)] tasks/coverage/typescript/tests/cases/conformance/jsx/tsxStatelessFunctionComponentOverload2.tsx semantic error: Scope children mismatch: diff --git a/tasks/transform_conformance/snapshots/oxc.snap.md b/tasks/transform_conformance/snapshots/oxc.snap.md index 3a89a04b6cf0b..f0b7f8e49c0eb 100644 --- a/tasks/transform_conformance/snapshots/oxc.snap.md +++ b/tasks/transform_conformance/snapshots/oxc.snap.md @@ -1,6 +1,6 @@ commit: acbc09a8 -Passed: 133/155 +Passed: 135/157 # All Passed: * babel-plugin-transform-class-static-block @@ -308,7 +308,7 @@ rebuilt : SymbolId(2): [] x Output mismatch -# babel-plugin-transform-react-jsx (35/38) +# babel-plugin-transform-react-jsx (37/40) * refresh/does-not-transform-it-because-it-is-not-used-in-the-AST/input.jsx x Output mismatch diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-automatic/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-automatic/input.jsx new file mode 100644 index 0000000000000..525a5dcf847de --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-automatic/input.jsx @@ -0,0 +1 @@ +<>{...foo} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-automatic/options.json b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-automatic/options.json new file mode 100644 index 0000000000000..e963e89a9c2c5 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-automatic/options.json @@ -0,0 +1,10 @@ +{ + "plugins": [ + [ + "transform-react-jsx", + { + "runtime": "automatic" + } + ] + ] +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-automatic/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-automatic/output.js new file mode 100644 index 0000000000000..3a111028d55c0 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-automatic/output.js @@ -0,0 +1,2 @@ +var _reactJsxRuntime = require("react/jsx-runtime"); +_reactJsxRuntime.jsx(_reactJsxRuntime.Fragment, { children: [...foo] }); diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-classic/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-classic/input.jsx new file mode 100644 index 0000000000000..525a5dcf847de --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-classic/input.jsx @@ -0,0 +1 @@ +<>{...foo} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-classic/options.json b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-classic/options.json new file mode 100644 index 0000000000000..3669bfdb6602b --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-classic/options.json @@ -0,0 +1,10 @@ +{ + "plugins": [ + [ + "transform-react-jsx", + { + "runtime": "classic" + } + ] + ] +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-classic/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-classic/output.js new file mode 100644 index 0000000000000..8112daadceb0b --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/spread-children-classic/output.js @@ -0,0 +1 @@ +React.createElement(React.Fragment, null, ...foo);