From f60fd65f653737fecbcb2115d88e29f9e016ac95 Mon Sep 17 00:00:00 2001 From: Boshen Date: Sat, 28 Oct 2023 16:17:11 +0800 Subject: [PATCH] feat(transfomer): implement react has_key_after_props_spread (#1075) --- crates/oxc_transformer/src/react_jsx/mod.rs | 88 ++++++++++++++++----- tasks/transform_conformance/babel.snap.md | 6 +- 2 files changed, 71 insertions(+), 23 deletions(-) diff --git a/crates/oxc_transformer/src/react_jsx/mod.rs b/crates/oxc_transformer/src/react_jsx/mod.rs index 54dd13173..adb799b70 100644 --- a/crates/oxc_transformer/src/react_jsx/mod.rs +++ b/crates/oxc_transformer/src/react_jsx/mod.rs @@ -20,6 +20,7 @@ pub struct ReactJsx<'a> { imports: Vec<'a, Statement<'a>>, import_jsx: bool, import_fragment: bool, + import_create_element: bool, } enum JSXElementOrFragment<'a, 'b> { @@ -43,12 +44,40 @@ impl<'a, 'b> JSXElementOrFragment<'a, 'b> { Self::Fragment(e) => &e.children, } } + + /// The react jsx/jsxs transform falls back to `createElement` when an explicit `key` argument comes after a spread + /// + fn has_key_after_props_spread(&self) -> bool { + let Self::Element(e) = self else { return false }; + let mut spread = false; + for attr in &e.opening_element.attributes { + if matches!(attr, JSXAttributeItem::SpreadAttribute(_)) { + spread = true; + } else if spread { + if let JSXAttributeItem::Attribute(a) = attr { + if let JSXAttributeName::Identifier(ident) = &a.name { + if ident.name == "key" { + return true; + } + } + } + } + } + false + } } impl<'a> ReactJsx<'a> { pub fn new(ast: Rc>, options: ReactJsxOptions) -> Self { let imports = ast.new_vec(); - Self { ast, options, imports, import_jsx: false, import_fragment: false } + Self { + ast, + options, + imports, + import_jsx: false, + import_fragment: false, + import_create_element: false, + } } pub fn transform_expression(&mut self, expr: &mut Expression<'a>) { @@ -75,24 +104,46 @@ impl<'a> ReactJsx<'a> { *stmts = self.ast.move_statement_vec(&mut self.imports); } - fn add_import_jsx(&mut self) { - if self.options.runtime.is_classic() || self.import_jsx { + fn add_import<'b>( + &mut self, + e: &JSXElementOrFragment<'a, 'b>, + has_key_after_props_spread: bool, + ) { + if self.options.runtime.is_classic() { return; } - self.import_jsx = true; - self.add_import_jsx_runtime("jsx", "_jsx"); + match e { + JSXElementOrFragment::Element(_) if has_key_after_props_spread => { + self.add_import_create_element(); + } + JSXElementOrFragment::Element(_) => self.add_import_jsx(), + JSXElementOrFragment::Fragment(_) => self.add_import_fragment(), + } + } + + fn add_import_jsx(&mut self) { + if !self.import_jsx { + self.import_jsx = true; + self.add_import_statement("jsx", "_jsx", "react/jsx-runtime"); + } } fn add_import_fragment(&mut self) { - if self.options.runtime.is_classic() || self.import_fragment { - return; + if !self.import_fragment { + self.import_fragment = true; + self.add_import_statement("Fragment", "_Fragment", "react/jsx-runtime"); + self.add_import_jsx(); } - self.import_fragment = true; - self.add_import_jsx_runtime("Fragment", "_Fragment"); - self.add_import_jsx(); } - fn add_import_jsx_runtime(&mut self, imported: &str, local: &str) { + fn add_import_create_element(&mut self) { + if !self.import_create_element { + self.import_create_element = true; + self.add_import_statement("createElement", "_createElement", "react"); + } + } + + fn add_import_statement(&mut self, imported: &str, local: &str, source: &str) { let mut specifiers = self.ast.new_vec_with_capacity(1); specifiers.push(ImportDeclarationSpecifier::ImportSpecifier(ImportSpecifier { span: SPAN, @@ -100,7 +151,7 @@ impl<'a> ReactJsx<'a> { local: BindingIdentifier::new(SPAN, local.into()), import_kind: ImportOrExportKind::Value, })); - let source = StringLiteral::new(SPAN, "react/jsx-runtime".into()); + let source = StringLiteral::new(SPAN, source.into()); let import_statement = self.ast.import_declaration( SPAN, Some(specifiers), @@ -114,7 +165,8 @@ impl<'a> ReactJsx<'a> { } fn transform_jsx<'b>(&mut self, e: &JSXElementOrFragment<'a, 'b>) -> Option> { - let callee = self.get_create_element(); + let has_key_after_props_spread = e.has_key_after_props_spread(); + let callee = self.get_create_element(has_key_after_props_spread); let children = e.children(); // TODO: compute the correct capacity for both runtimes @@ -239,10 +291,7 @@ impl<'a> ReactJsx<'a> { ); } - match e { - JSXElementOrFragment::Element(_) => self.add_import_jsx(), - JSXElementOrFragment::Fragment(_) => self.add_import_fragment(), - } + self.add_import(e, has_key_after_props_spread); Some(self.ast.call_expression(SPAN, callee, arguments, false, None)) } @@ -252,7 +301,7 @@ impl<'a> ReactJsx<'a> { self.ast.identifier_reference_expression(ident) } - fn get_create_element(&mut self) -> Expression<'a> { + fn get_create_element(&mut self, has_key_after_props_spread: bool) -> Expression<'a> { match self.options.runtime { ReactJsxRuntime::Classic => { let object = self.get_react_references(); @@ -260,7 +309,8 @@ impl<'a> ReactJsx<'a> { self.ast.static_member_expression(SPAN, object, property, false) } ReactJsxRuntime::Automatic => { - let ident = IdentifierReference::new(SPAN, "_jsx".into()); + let name = if has_key_after_props_spread { "_createElement" } else { "_jsx" }; + let ident = IdentifierReference::new(SPAN, name.into()); self.ast.identifier_reference_expression(ident) } } diff --git a/tasks/transform_conformance/babel.snap.md b/tasks/transform_conformance/babel.snap.md index a29b36ff7..5d33b1d54 100644 --- a/tasks/transform_conformance/babel.snap.md +++ b/tasks/transform_conformance/babel.snap.md @@ -1,4 +1,4 @@ -Passed: 206/1083 +Passed: 208/1083 # All Passed: * babel-plugin-transform-numeric-separator @@ -804,7 +804,7 @@ Passed: 206/1083 * regression/11061/input.mjs * variable-declaration/non-null-in-optional-chain/input.ts -# babel-plugin-transform-react-jsx (57/172) +# babel-plugin-transform-react-jsx (59/172) * autoImport/after-polyfills/input.mjs * autoImport/after-polyfills-2/input.mjs * autoImport/after-polyfills-compiled-to-cjs/input.mjs @@ -878,7 +878,6 @@ Passed: 206/1083 * react-automatic/handle-nonstatic-children/input.js * react-automatic/handle-spread-with-proto/input.js * react-automatic/handle-static-children/input.js -* react-automatic/key-undefined-works/input.js * react-automatic/optimisation.react.constant-elements/input.js * react-automatic/pragma-works-with-no-space-at-the-end/input.js * react-automatic/should-add-quotes-es3/input.js @@ -902,7 +901,6 @@ Passed: 206/1083 * react-automatic/should-quote-jsx-attributes/input.js * react-automatic/should-support-xml-namespaces-if-flag/input.js * react-automatic/should-throw-when-filter-is-specified/input.js -* react-automatic/should-use-createElement-when-key-comes-after-spread/input.js * react-automatic/should-use-jsx-when-key-comes-before-spread/input.js * react-automatic/should-warn-when-pragma-or-pragmaFrag-is-set/input.js * react-automatic/weird-symbols/input.js