diff --git a/crates/oxc_transformer/examples/transformer.rs b/crates/oxc_transformer/examples/transformer.rs index c900b5281..b3558cf6d 100644 --- a/crates/oxc_transformer/examples/transformer.rs +++ b/crates/oxc_transformer/examples/transformer.rs @@ -5,7 +5,9 @@ use oxc_codegen::{Codegen, CodegenOptions}; use oxc_parser::Parser; use oxc_semantic::SemanticBuilder; use oxc_span::SourceType; -use oxc_transformer::{ReactJsxOptions, TransformOptions, TransformTarget, Transformer}; +use oxc_transformer::{ + ReactJsxOptions, ReactJsxRuntime, TransformOptions, TransformTarget, Transformer, +}; // Instruction: // create a `test.js`, @@ -41,7 +43,10 @@ fn main() { let program = allocator.alloc(ret.program); let transform_options = TransformOptions { target: TransformTarget::ES2015, - react_jsx: Some(ReactJsxOptions::default()), + react_jsx: Some(ReactJsxOptions { + runtime: ReactJsxRuntime::Automatic, + ..ReactJsxOptions::default() + }), ..TransformOptions::default() }; Transformer::new(&allocator, source_type, &symbols, &scopes, transform_options).build(program); diff --git a/crates/oxc_transformer/src/react_jsx/mod.rs b/crates/oxc_transformer/src/react_jsx/mod.rs index d05d1aa1d..e0a6f72d4 100644 --- a/crates/oxc_transformer/src/react_jsx/mod.rs +++ b/crates/oxc_transformer/src/react_jsx/mod.rs @@ -17,33 +17,85 @@ pub struct ReactJsx<'a> { ast: Rc>, options: ReactJsxOptions, - has_jsx: bool, + imports: Vec<'a, Statement<'a>>, + import_jsx: bool, + import_fragment: bool, +} + +enum JSXElementOrFragment<'a, 'b> { + Element(&'b JSXElement<'a>), + Fragment(&'b JSXFragment<'a>), +} + +impl<'a, 'b> JSXElementOrFragment<'a, 'b> { + fn attributes(&self) -> Option<&'b Vec<'a, JSXAttributeItem<'a>>> { + match self { + Self::Element(e) => Some(&e.opening_element.attributes), + Self::Fragment(_) => None, + } + } + + fn children(&self) -> &'b Vec<'a, JSXChild<'a>> { + match self { + Self::Element(e) => &e.children, + Self::Fragment(e) => &e.children, + } + } } impl<'a> ReactJsx<'a> { pub fn new(ast: Rc>, options: ReactJsxOptions) -> Self { - Self { ast, options, has_jsx: false } + let imports = ast.new_vec(); + Self { ast, options, imports, import_jsx: false, import_fragment: false } } - pub fn transform_expression<'b>(&mut self, expr: &'b mut Expression<'a>) { - if let Expression::JSXElement(e) = expr { - self.has_jsx = true; - if let Some(e) = self.transform_jsx_element(e) { - *expr = e; + pub fn transform_expression(&mut self, expr: &mut Expression<'a>) { + match expr { + Expression::JSXElement(e) => { + if let Some(e) = self.transform_jsx(&JSXElementOrFragment::Element(e)) { + *expr = e; + } } + Expression::JSXFragment(e) => { + if let Some(e) = self.transform_jsx(&JSXElementOrFragment::Fragment(e)) { + *expr = e; + } + } + _ => {} } } - pub fn add_react_jsx_runtime_import(&self, stmts: &mut Vec<'a, Statement<'a>>) { - if self.options.runtime.is_classic() || !self.has_jsx { + pub fn add_react_jsx_runtime_import(&mut self, stmts: &mut Vec<'a, Statement<'a>>) { + if self.options.runtime.is_classic() { return; } + self.imports.extend(stmts.drain(..)); + *stmts = self.ast.move_statement_vec(&mut self.imports); + } + fn add_import_jsx(&mut self) { + if self.options.runtime.is_classic() || self.import_jsx { + return; + } + self.import_jsx = true; + self.add_import_jsx_runtime("jsx", "_jsx"); + } + + fn add_import_fragment(&mut self) { + if self.options.runtime.is_classic() || self.import_fragment { + return; + } + 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) { let mut specifiers = self.ast.new_vec_with_capacity(1); specifiers.push(ImportDeclarationSpecifier::ImportSpecifier(ImportSpecifier { span: SPAN, - imported: ModuleExportName::Identifier(IdentifierName::new(SPAN, "jsx".into())), - local: BindingIdentifier::new(SPAN, "_jsx".into()), + imported: ModuleExportName::Identifier(IdentifierName::new(SPAN, imported.into())), + local: BindingIdentifier::new(SPAN, local.into()), import_kind: ImportOrExportKind::Value, })); let source = StringLiteral::new(SPAN, "react/jsx-runtime".into()); @@ -56,35 +108,155 @@ impl<'a> ReactJsx<'a> { ); let decl = self.ast.module_declaration(ModuleDeclaration::ImportDeclaration(import_statement)); - stmts.insert(0, decl); + self.imports.push(decl); } - fn transform_jsx_element(&self, e: &JSXElement<'a>) -> Option> { - let callee = self.transform_create_element(); + fn transform_jsx<'b>(&mut self, e: &JSXElementOrFragment<'a, 'b>) -> Option> { + let callee = self.get_create_element(); + let children = e.children(); - let mut arguments = self.ast.new_vec_with_capacity(2 + e.children.len()); - arguments.push(Argument::Expression(self.transform_element_name(&e.opening_element.name)?)); - arguments.push(Argument::Expression( - self.transform_jsx_attributes(&e.opening_element.attributes)?, - )); - arguments.extend( - e.children - .iter() - .filter_map(|child| self.transform_jsx_child(child)) - .map(Argument::Expression), - ); + // TODO: compute the correct capacity for both runtimes + let mut arguments = self.ast.new_vec_with_capacity(1); + + arguments.push(Argument::Expression(match e { + JSXElementOrFragment::Element(e) => { + self.transform_element_name(&e.opening_element.name)? + } + JSXElementOrFragment::Fragment(_) => self.get_fragment(), + })); + + if self.options.runtime.is_classic() && e.attributes().is_some_and(|attrs| attrs.is_empty()) + { + let null_expr = self.ast.literal_null_expression(NullLiteral::new(SPAN)); + arguments.push(Argument::Expression(null_expr)); + } + + // 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) => { + let key = match &attr.name { + JSXAttributeName::Identifier(ident) => PropertyKey::Identifier( + self.ast.alloc(IdentifierName::new(SPAN, ident.name.clone())), + ), + JSXAttributeName::NamespacedName(_ident) => { + /* TODO */ + continue; + } + }; + let value = match &attr.value { + Some(value) => { + match value { + JSXAttributeValue::StringLiteral(s) => { + self.ast.literal_string_expression(s.clone()) + } + JSXAttributeValue::Element(_) + | JSXAttributeValue::Fragment(_) => { + /* TODO */ + continue; + } + JSXAttributeValue::ExpressionContainer(c) => { + match &c.expression { + JSXExpression::Expression(e) => self.ast.copy(e), + JSXExpression::EmptyExpression(_e) => + /* TODO */ + { + continue; + } + } + } + } + } + None => { + self.ast.literal_boolean_expression(BooleanLiteral::new(SPAN, true)) + } + }; + 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); + } + }, + } + } + } + + if self.options.runtime.is_automatic() && !children.is_empty() { + let key = PropertyKey::Identifier( + self.ast.alloc(IdentifierName::new(SPAN, "children".into())), + ); + let value = if children.len() == 1 { + self.transform_jsx_child(&children[0])? + } else { + let mut elements = self.ast.new_vec_with_capacity(children.len()); + for child in children { + if let Some(e) = self.transform_jsx_child(child) { + elements.push(ArrayExpressionElement::Expression(e)); + } + } + self.ast.array_expression(SPAN, elements, None) + }; + let object_property = self.ast.object_property( + SPAN, + PropertyKind::Init, + key, + value, + None, + false, + false, + false, + ); + properties.push(ObjectPropertyKind::ObjectProperty(object_property)); + } + + if !properties.is_empty() || self.options.runtime.is_automatic() { + let object_expression = self.ast.object_expression(SPAN, properties, None); + arguments.push(Argument::Expression(object_expression)); + } + + if self.options.runtime.is_classic() && !children.is_empty() { + arguments.extend( + children + .iter() + .filter_map(|child| self.transform_jsx_child(child)) + .map(Argument::Expression), + ); + } + + match e { + JSXElementOrFragment::Element(_) => self.add_import_jsx(), + JSXElementOrFragment::Fragment(_) => self.add_import_fragment(), + } Some(self.ast.call_expression(SPAN, callee, arguments, false, None)) } - fn transform_create_element(&self) -> Expression<'a> { + fn get_react_references(&mut self) -> Expression<'a> { + let ident = IdentifierReference::new(SPAN, "React".into()); + self.ast.identifier_reference_expression(ident) + } + + fn get_create_element(&mut self) -> Expression<'a> { match self.options.runtime { ReactJsxRuntime::Classic => { - // React - let object = IdentifierReference::new(SPAN, "React".into()); - let object = self.ast.identifier_reference_expression(object); - - // React.createElement + let object = self.get_react_references(); let property = IdentifierName::new(SPAN, "createElement".into()); self.ast.static_member_expression(SPAN, object, property, false) } @@ -95,6 +267,20 @@ impl<'a> ReactJsx<'a> { } } + fn get_fragment(&mut self) -> Expression<'a> { + match self.options.runtime { + ReactJsxRuntime::Classic => { + let object = self.get_react_references(); + let property = IdentifierName::new(SPAN, "Fragment".into()); + self.ast.static_member_expression(SPAN, object, property, false) + } + ReactJsxRuntime::Automatic => { + let ident = IdentifierReference::new(SPAN, "_Fragment".into()); + self.ast.identifier_reference_expression(ident) + } + } + } + fn transform_element_name(&self, name: &JSXElementName<'a>) -> Option> { match name { JSXElementName::Identifier(ident) => { @@ -143,72 +329,7 @@ impl<'a> ReactJsx<'a> { self.ast.static_member_expression(SPAN, object, property, false) } - fn transform_jsx_attributes( - &self, - attributes: &Vec<'a, JSXAttributeItem<'a>>, - ) -> Option> { - if attributes.is_empty() { - return Some(self.ast.literal_null_expression(NullLiteral::new(SPAN))); - } - - let mut properties = self.ast.new_vec_with_capacity(attributes.len()); - for attribute in attributes { - let kind = PropertyKind::Init; - let object_property = match attribute { - JSXAttributeItem::Attribute(attr) => { - let key = match &attr.name { - JSXAttributeName::Identifier(ident) => PropertyKey::Identifier( - self.ast.alloc(IdentifierName::new(SPAN, ident.name.clone())), - ), - JSXAttributeName::NamespacedName(_ident) => { - /* TODO */ - return None; - } - }; - let value = match &attr.value { - Some(value) => { - match value { - JSXAttributeValue::StringLiteral(s) => { - self.ast.literal_string_expression(s.clone()) - } - JSXAttributeValue::Element(_) | JSXAttributeValue::Fragment(_) => { - /* TODO */ - return None; - } - JSXAttributeValue::ExpressionContainer(c) => { - match &c.expression { - JSXExpression::Expression(e) => self.ast.copy(e), - JSXExpression::EmptyExpression(_e) => - /* TODO */ - { - return None - } - } - } - } - } - None => { - self.ast.literal_boolean_expression(BooleanLiteral::new(SPAN, true)) - } - }; - let object_property = - self.ast.object_property(SPAN, kind, key, value, None, false, false, false); - ObjectPropertyKind::ObjectProperty(object_property) - } - JSXAttributeItem::SpreadAttribute(attr) => { - let argument = self.ast.copy(&attr.argument); - let spread_property = self.ast.spread_element(SPAN, argument); - ObjectPropertyKind::SpreadProperty(spread_property) - } - }; - properties.push(object_property); - } - - let object_expression = self.ast.object_expression(SPAN, properties, None); - Some(object_expression) - } - - fn transform_jsx_child(&self, child: &JSXChild<'a>) -> Option> { + fn transform_jsx_child(&mut self, child: &JSXChild<'a>) -> Option> { match child { JSXChild::Text(text) => { let text = text.value.trim(); @@ -227,8 +348,9 @@ impl<'a> ReactJsx<'a> { JSXExpression::Expression(e) => Some(self.ast.copy(e)), JSXExpression::EmptyExpression(_) => None, }, - JSXChild::Element(e) => self.transform_jsx_element(e), - _ => None, + JSXChild::Element(e) => self.transform_jsx(&JSXElementOrFragment::Element(e)), + JSXChild::Fragment(e) => self.transform_jsx(&JSXElementOrFragment::Fragment(e)), + JSXChild::Spread(_) => None, } } } diff --git a/tasks/transform_conformance/babel.snap.md b/tasks/transform_conformance/babel.snap.md index 205275881..843de56fa 100644 --- a/tasks/transform_conformance/babel.snap.md +++ b/tasks/transform_conformance/babel.snap.md @@ -1,4 +1,4 @@ -Passed: 190/1083 +Passed: 204/1083 # All Passed: * babel-plugin-transform-numeric-separator @@ -804,7 +804,7 @@ Passed: 190/1083 * regression/11061/input.mjs * variable-declaration/non-null-in-optional-chain/input.ts -# babel-plugin-transform-react-jsx (41/172) +# babel-plugin-transform-react-jsx (55/172) * autoImport/after-polyfills/input.mjs * autoImport/after-polyfills-2/input.mjs * autoImport/after-polyfills-compiled-to-cjs/input.mjs @@ -838,8 +838,8 @@ Passed: 190/1083 * react/avoids-spread-babel-7/input.js * react/does-not-add-source-self/input.mjs * react/does-not-add-source-self-babel-7/input.mjs -* react/duplicate-props/input.js * 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 @@ -876,37 +876,25 @@ Passed: 190/1083 * react-automatic/concatenates-adjacent-string-literals/input.js * react-automatic/does-not-add-source-self-automatic/input.mjs * react-automatic/dont-coerce-expression-containers/input.js -* react-automatic/duplicate-props/input.js -* react-automatic/flattens-spread/input.js -* react-automatic/handle-fragments/input.js * react-automatic/handle-fragments-with-key/input.js -* react-automatic/handle-fragments-with-no-children/input.js * react-automatic/handle-nonstatic-children/input.js * react-automatic/handle-spread-with-proto/input.js * react-automatic/handle-static-children/input.js -* react-automatic/jsx-with-retainlines-option/input.js -* react-automatic/jsx-without-retainlines-option/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 -* react-automatic/should-allow-deeper-js-namespacing/input.js * react-automatic/should-allow-elements-as-attributes/input.js -* react-automatic/should-allow-js-namespacing/input.js * react-automatic/should-allow-nested-fragments/input.js * react-automatic/should-avoid-wrapping-in-extra-parens-if-not-needed/input.js -* react-automatic/should-convert-simple-tags/input.js -* react-automatic/should-convert-simple-text/input.js -* react-automatic/should-disallow-spread-children/input.js * react-automatic/should-disallow-valueless-key/input.js -* react-automatic/should-disallow-xml-namespacing/input.js * react-automatic/should-escape-xhtml-jsxattribute/input.js * react-automatic/should-escape-xhtml-jsxattribute-babel-7/input.js * react-automatic/should-escape-xhtml-jsxtext/input.js * react-automatic/should-escape-xhtml-jsxtext-babel-7/input.js * react-automatic/should-handle-attributed-elements/input.js -* react-automatic/should-handle-has-own-property-correctly/input.js * react-automatic/should-have-correct-comma-in-nested-children/input.js +* react-automatic/should-insert-commas-after-expressions-before-whitespace/input.js * react-automatic/should-not-add-quotes-to-identifier-names/input.js * react-automatic/should-not-mangle-expressioncontainer-attribute-values/input.js * react-automatic/should-not-strip-nbsp-even-coupled-with-other-whitespace/input.js @@ -915,12 +903,10 @@ Passed: 190/1083 * react-automatic/should-properly-handle-keys/input.js * react-automatic/should-quote-jsx-attributes/input.js * react-automatic/should-support-xml-namespaces-if-flag/input.js -* react-automatic/should-throw-error-namespaces-if-not-flag/input.js -* react-automatic/should-transform-known-hyphenated-tags/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/this-tag-name/input.js * react-automatic/weird-symbols/input.js * regression/issue-12478-automatic/input.js * regression/issue-12478-classic/input.js