diff --git a/crates/oxc_ast/src/ast/js.rs b/crates/oxc_ast/src/ast/js.rs index c0d12ce29..5587452f7 100644 --- a/crates/oxc_ast/src/ast/js.rs +++ b/crates/oxc_ast/src/ast/js.rs @@ -376,6 +376,13 @@ pub struct ObjectExpression<'a> { pub trailing_comma: Option, } +impl<'a> ObjectExpression<'a> { + pub fn has_proto(&self) -> bool { + use crate::syntax_directed_operations::PropName; + self.properties.iter().any(|p| p.prop_name().is_some_and(|name| name.0 == "__proto__")) + } +} + #[derive(Debug, Hash)] #[cfg_attr(feature = "serde", derive(Serialize), serde(untagged))] pub enum ObjectPropertyKind<'a> { @@ -1827,6 +1834,10 @@ pub enum ModuleDeclaration<'a> { } impl<'a> ModuleDeclaration<'a> { + pub fn is_import(&self) -> bool { + matches!(self, Self::ImportDeclaration(_)) + } + pub fn is_export(&self) -> bool { matches!( self, diff --git a/crates/oxc_transformer/examples/transformer.rs b/crates/oxc_transformer/examples/transformer.rs index b3558cf6d..984060c8a 100644 --- a/crates/oxc_transformer/examples/transformer.rs +++ b/crates/oxc_transformer/examples/transformer.rs @@ -44,7 +44,7 @@ fn main() { let transform_options = TransformOptions { target: TransformTarget::ES2015, react_jsx: Some(ReactJsxOptions { - runtime: ReactJsxRuntime::Automatic, + runtime: ReactJsxRuntime::Classic, ..ReactJsxOptions::default() }), ..TransformOptions::default() diff --git a/crates/oxc_transformer/src/react_jsx/mod.rs b/crates/oxc_transformer/src/react_jsx/mod.rs index 1697ff1f5..782f1ec27 100644 --- a/crates/oxc_transformer/src/react_jsx/mod.rs +++ b/crates/oxc_transformer/src/react_jsx/mod.rs @@ -92,8 +92,12 @@ impl<'a> ReactJsx<'a> { if self.options.runtime.is_classic() { return; } - self.imports.extend(stmts.drain(..)); - *stmts = self.ast.move_statement_vec(&mut self.imports); + let imports = self.ast.move_statement_vec(&mut self.imports); + let index = stmts + .iter() + .position(|stmt| matches!(stmt, Statement::ModuleDeclaration(m) if m.is_import())) + .map_or(0, |i| i + 1); + stmts.splice(index..index, imports); } fn add_import<'b>( @@ -169,7 +173,6 @@ impl<'a> ReactJsx<'a> { let is_classic = self.options.runtime.is_classic(); let is_automatic = self.options.runtime.is_automatic(); let has_key_after_props_spread = e.has_key_after_props_spread(); - let children = e.children(); // TODO: compute the correct capacity for both runtimes let mut arguments = self.ast.new_vec_with_capacity(1); @@ -181,51 +184,60 @@ impl<'a> ReactJsx<'a> { JSXElementOrFragment::Fragment(_) => self.get_fragment(), })); - let mut key = None; - // TODO: compute the correct capacity for both runtimes - let mut properties = self.ast.new_vec_with_capacity(0); - if let Some(attributes) = e.attributes() { - for attribute in attributes { - let kind = PropertyKind::Init; - match attribute { - JSXAttributeItem::Attribute(attr) => { - if is_automatic && attr.is_key() && !has_key_after_props_spread { - key = attr.value.as_ref(); - continue; - } - let key = self.get_attribute_name(&attr.name); - let value = self.transform_jsx_attribute_value(attr.value.as_ref()); - let object_property = self - .ast - .object_property(SPAN, kind, key, value, None, false, false, false); - let object_property = ObjectPropertyKind::ObjectProperty(object_property); - properties.push(object_property); - } - JSXAttributeItem::SpreadAttribute(attr) => match &attr.argument { - Expression::ObjectExpression(expr) => { - for object_property in &expr.properties { - properties.push(self.ast.copy(object_property)); - } - } - expr => { - let argument = self.ast.copy(expr); - let spread_property = self.ast.spread_element(SPAN, argument); - let object_property = - ObjectPropertyKind::SpreadProperty(spread_property); - properties.push(object_property); - } - }, - } - } - } else if is_classic { + // The key prop in `
` + let mut key_prop = None; + + let attributes = e.attributes(); + let attributes_len = attributes.map_or(0, |attrs| attrs.len()); + + // Add `null` to second argument in classic mode + if is_classic && attributes_len == 0 { let null_expr = self.ast.literal_null_expression(NullLiteral::new(SPAN)); arguments.push(Argument::Expression(null_expr)); } + // The object properties for the second argument of `React.createElement` + let mut properties = self.ast.new_vec_with_capacity(0); + + if let Some(attributes) = attributes { + // TODO: compute the correct capacity for both runtimes + + for attribute in attributes { + // optimize `{...prop}` to `prop` in static mode + if is_classic && attributes_len == 1 { + if let JSXAttributeItem::SpreadAttribute(spread) = attribute { + // deopt if spreading an object with `__proto__` key + if !matches!(&spread.argument, Expression::ObjectExpression(o) if o.has_proto()) + { + arguments.push(Argument::Expression(self.ast.copy(&spread.argument))); + continue; + } + } + } + // In automatic mode, extract the key before spread prop, + // and add it to the third argument later. + if is_automatic && !has_key_after_props_spread { + if let JSXAttributeItem::Attribute(attr) = attribute { + if attr.is_key() { + key_prop = attr.value.as_ref(); + continue; + } + } + } + + // Add attribute to prop object + self.transform_jsx_attribute_item(&mut properties, attribute); + } + } + let mut need_jsxs = false; + + let children = e.children(); + + // Append children to object properties in automatic mode if is_automatic && !children.is_empty() { - let key = - self.ast.property_key_identifier(IdentifierName::new(SPAN, "children".into())); + let ident = IdentifierName::new(SPAN, "children".into()); + let key = self.ast.property_key_identifier(ident); let value = if children.len() == 1 { self.transform_jsx_child(&children[0]) } else { @@ -239,16 +251,9 @@ impl<'a> ReactJsx<'a> { Some(self.ast.array_expression(SPAN, elements, None)) }; if let Some(value) = value { - let object_property = self.ast.object_property( - SPAN, - PropertyKind::Init, - key, - value, - None, - false, - false, - false, - ); + let kind = PropertyKind::Init; + let object_property = + self.ast.object_property(SPAN, kind, key, value, None, false, false, false); properties.push(ObjectPropertyKind::ObjectProperty(object_property)); } } @@ -258,8 +263,8 @@ impl<'a> ReactJsx<'a> { arguments.push(Argument::Expression(object_expression)); } - if is_automatic && key.is_some() { - arguments.push(Argument::Expression(self.transform_jsx_attribute_value(key))); + if is_automatic && key_prop.is_some() { + arguments.push(Argument::Expression(self.transform_jsx_attribute_value(key_prop))); } if is_classic && !children.is_empty() { @@ -273,7 +278,6 @@ impl<'a> ReactJsx<'a> { let callee = self.get_create_element(has_key_after_props_spread, need_jsxs); self.add_import(e, has_key_after_props_spread, need_jsxs); - self.ast.call_expression(SPAN, callee, arguments, false, None) } @@ -365,6 +369,37 @@ impl<'a> ReactJsx<'a> { } } + fn transform_jsx_attribute_item( + &mut self, + properties: &mut Vec<'a, ObjectPropertyKind<'a>>, + attribute: &JSXAttributeItem<'a>, + ) { + match attribute { + JSXAttributeItem::Attribute(attr) => { + let kind = PropertyKind::Init; + let key = self.get_attribute_name(&attr.name); + let value = self.transform_jsx_attribute_value(attr.value.as_ref()); + let object_property = + self.ast.object_property(SPAN, kind, key, value, None, false, false, false); + let object_property = ObjectPropertyKind::ObjectProperty(object_property); + properties.push(object_property); + } + JSXAttributeItem::SpreadAttribute(attr) => match &attr.argument { + Expression::ObjectExpression(expr) if !expr.has_proto() => { + for object_property in &expr.properties { + properties.push(self.ast.copy(object_property)); + } + } + expr => { + let argument = self.ast.copy(expr); + let spread_property = self.ast.spread_element(SPAN, argument); + let object_property = ObjectPropertyKind::SpreadProperty(spread_property); + properties.push(object_property); + } + }, + } + } + fn transform_jsx_attribute_value( &mut self, value: Option<&JSXAttributeValue<'a>>, diff --git a/tasks/transform_conformance/babel.snap.md b/tasks/transform_conformance/babel.snap.md index f7e756a80..c48245221 100644 --- a/tasks/transform_conformance/babel.snap.md +++ b/tasks/transform_conformance/babel.snap.md @@ -1,4 +1,4 @@ -Passed: 219/1083 +Passed: 224/1083 # All Passed: * babel-plugin-transform-numeric-separator @@ -804,7 +804,7 @@ Passed: 219/1083 * regression/11061/input.mjs * variable-declaration/non-null-in-optional-chain/input.ts -# babel-plugin-transform-react-jsx (70/172) +# babel-plugin-transform-react-jsx (75/172) * autoImport/after-polyfills/input.mjs * autoImport/after-polyfills-2/input.mjs * autoImport/after-polyfills-compiled-to-cjs/input.mjs @@ -815,7 +815,6 @@ Passed: 219/1083 * autoImport/complicated-scope-script/input.js * autoImport/import-source/input.js * autoImport/import-source-pragma/input.js -* autoImport/react-defined/input.js * pure/false-default-pragma-automatic-runtime/input.js * pure/false-pragma-comment-automatic-runtime/input.js * pure/false-pragma-comment-classic-runtime/input.js @@ -837,8 +836,6 @@ Passed: 219/1083 * react/assignment-babel-7/input.js * react/avoids-spread-babel-7/input.js * react/does-not-add-source-self-babel-7/input.mjs -* react/flattens-spread/input.js -* react/handle-spread-with-proto/input.js * react/handle-spread-with-proto-babel-7/input.js * react/honor-custom-jsx-comment/input.js * react/honor-custom-jsx-comment-if-jsx-pragma-option-set/input.js @@ -870,9 +867,7 @@ Passed: 219/1083 * react-automatic/.should-properly-handle-comments-adjacent-to-children/input.js * react-automatic/arrow-functions/input.js * react-automatic/does-not-add-source-self-automatic/input.mjs -* react-automatic/handle-fragments-with-key/input.js * react-automatic/handle-nonstatic-children/input.js -* react-automatic/handle-spread-with-proto/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