From bd9fc6d16925ed10ed57ad0253dc8eb7c3de33a2 Mon Sep 17 00:00:00 2001 From: Boshen Date: Sun, 14 Apr 2024 10:50:17 +0800 Subject: [PATCH] feat(transformer): react jsx transform (#2961) --- crates/oxc_ast/src/ast_builder.rs | 4 + crates/oxc_transformer/src/lib.rs | 2 +- crates/oxc_transformer/src/react/jsx/mod.rs | 696 ++++++++++++++++++- crates/oxc_transformer/src/react/mod.rs | 20 +- crates/oxc_transformer/src/react/options.rs | 16 +- tasks/common/src/babel.rs | 15 +- tasks/transform_conformance/babel.snap.md | 54 +- tasks/transform_conformance/src/test_case.rs | 38 +- 8 files changed, 770 insertions(+), 75 deletions(-) diff --git a/crates/oxc_ast/src/ast_builder.rs b/crates/oxc_ast/src/ast_builder.rs index 2f10c7fc8..5b7fbc0e3 100644 --- a/crates/oxc_ast/src/ast_builder.rs +++ b/crates/oxc_ast/src/ast_builder.rs @@ -148,6 +148,10 @@ impl<'a> AstBuilder<'a> { BooleanLiteral { span, value } } + pub fn string_literal(&self, span: Span, name: &str) -> StringLiteral<'a> { + StringLiteral::new(span, self.new_atom(name)) + } + pub fn bigint_literal(&self, span: Span, raw: Atom<'a>, base: BigintBase) -> BigIntLiteral<'a> { BigIntLiteral { span, raw, base } } diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 4858e194c..5bd3b7478 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -79,7 +79,7 @@ impl<'a> Transformer<'a> { impl<'a> VisitMut<'a> for Transformer<'a> { fn visit_program(&mut self, program: &mut Program<'a>) { walk_mut::walk_program_mut(self, program); - + self.x1_react.transform_program_on_exit(program); self.x0_typescript.transform_program_on_exit(program); } diff --git a/crates/oxc_transformer/src/react/jsx/mod.rs b/crates/oxc_transformer/src/react/jsx/mod.rs index 580dd94d3..beae98cba 100644 --- a/crates/oxc_transformer/src/react/jsx/mod.rs +++ b/crates/oxc_transformer/src/react/jsx/mod.rs @@ -1,30 +1,714 @@ use std::rc::Rc; +use oxc_allocator::Vec; +use oxc_ast::{ast::*, AstBuilder}; +use oxc_span::{CompactStr, SPAN}; +use oxc_syntax::{ + identifier::{is_irregular_whitespace, is_line_terminator}, + xml_entities::XML_ENTITIES, +}; + use crate::context::Ctx; -pub use super::options::ReactOptions; +pub use super::options::{ReactJsxRuntime, ReactOptions}; /// [plugin-transform-react-jsx](https://babeljs.io/docs/babel-plugin-transform-react-jsx) /// /// This plugin generates production-ready JS code. /// -/// If you are developing a React app in a development environment, -/// please use @babel/plugin-transform-react-jsx-development for a better debugging experience. -/// /// This plugin is included in `preset-react`. /// /// References: /// /// * /// * -#[allow(unused)] pub struct ReactJsx<'a> { options: Rc, + ctx: Ctx<'a>, + + // States + imports: std::vec::Vec>, + require_jsx_runtime: bool, + jsx_runtime_importer: CompactStr, + default_runtime: ReactJsxRuntime, + + import_jsx: bool, + import_jsxs: bool, + import_fragment: bool, + import_create_element: bool, } +// Transforms impl<'a> ReactJsx<'a> { pub fn new(options: &Rc, ctx: &Ctx<'a>) -> Self { - Self { options: Rc::clone(options), ctx: Rc::clone(ctx) } + let default_runtime = options.runtime; + let jsx_runtime_importer = + if options.import_source == "react" || default_runtime.is_classic() { + CompactStr::from("react/jsx-runtime") + } else { + CompactStr::from(format!("{}/jsx-runtime", options.import_source)) + }; + + Self { + options: Rc::clone(options), + ctx: Rc::clone(ctx), + imports: vec![], + require_jsx_runtime: false, + jsx_runtime_importer, + import_jsx: false, + import_jsxs: false, + import_fragment: false, + import_create_element: false, + default_runtime, + } + } + + pub fn transform_program_on_exit(&mut self, program: &mut Program<'a>) { + self.add_runtime_imports(program); + } + + pub fn transform_jsx_element(&mut self, e: &JSXElement<'a>) -> Expression<'a> { + self.transform_jsx(&JSXElementOrFragment::Element(e)) + } + + pub fn transform_jsx_fragment(&mut self, e: &JSXFragment<'a>) -> Expression<'a> { + self.transform_jsx(&JSXElementOrFragment::Fragment(e)) + } + + fn is_script(&self) -> bool { + self.ctx.semantic.source_type().is_script() + } + + fn ast(&self) -> &AstBuilder<'a> { + &self.ctx.ast + } +} + +// Add imports +impl<'a> ReactJsx<'a> { + pub fn add_runtime_imports(&mut self, program: &mut Program<'a>) { + if self.options.runtime.is_classic() { + if self.options.import_source != "react" { + // self.ctx.error(ImportSourceCannotBeSet); + } + return; + } + + if self.options.pragma != "React.createElement" + || self.options.pragma_frag != "React.Fragment" + { + // self.ctx.error(PragmaAndPragmaFragCannotBeSet); + return; + } + + let imports = self.ctx.ast.new_vec_from_iter(self.imports.drain(..)); + let index = program + .body + .iter() + .rposition(|stmt| matches!(stmt, Statement::ModuleDeclaration(m) if m.is_import())) + .map_or(0, |i| i + 1); + program.body.splice(index..index, imports); + } + + fn add_import<'b>( + &mut self, + e: &JSXElementOrFragment<'a, 'b>, + has_key_after_props_spread: bool, + need_jsxs: bool, + ) { + if self.options.runtime.is_classic() { + return; + } + match e { + JSXElementOrFragment::Element(_) if has_key_after_props_spread => { + self.add_import_create_element(); + } + JSXElementOrFragment::Element(_) if need_jsxs => self.add_import_jsxs(), + JSXElementOrFragment::Element(_) => self.add_import_jsx(), + JSXElementOrFragment::Fragment(_) => { + self.add_import_fragment(); + if need_jsxs { + self.add_import_jsxs(); + } + } + } + } + + fn add_require_jsx_runtime(&mut self) { + if !self.require_jsx_runtime { + self.require_jsx_runtime = true; + let source = self.ast().string_literal(SPAN, self.jsx_runtime_importer.as_str()); + self.add_require_statement("_reactJsxRuntime", source, false); + } + } + + fn get_jsx_runtime_source(&self) -> StringLiteral<'a> { + self.ast().string_literal(SPAN, self.jsx_runtime_importer.as_str()) + } + + fn add_import_jsx(&mut self) { + if self.is_script() { + self.add_require_jsx_runtime(); + } else if !self.import_jsx { + self.import_jsx = true; + self.add_import_statement("jsx", "_jsx", self.get_jsx_runtime_source()); + } + } + + fn add_import_jsxs(&mut self) { + if self.is_script() { + self.add_require_jsx_runtime(); + } else if !self.import_jsxs { + self.import_jsxs = true; + self.add_import_statement("jsxs", "_jsxs", self.get_jsx_runtime_source()); + } + } + + fn add_import_fragment(&mut self) { + if self.is_script() { + self.add_require_jsx_runtime(); + } else if !self.import_fragment { + self.import_fragment = true; + self.add_import_statement("Fragment", "_Fragment", self.get_jsx_runtime_source()); + self.add_import_jsx(); + } + } + + fn add_import_create_element(&mut self) { + if !self.import_create_element { + self.import_create_element = true; + let source = self.ast().string_literal(SPAN, self.options.import_source.as_ref()); + if self.is_script() { + self.add_require_statement("_react", source, true); + } else { + self.add_import_statement("createElement", "_createElement", source); + } + } + } + + fn add_import_statement(&mut self, imported: &str, local: &str, source: StringLiteral<'a>) { + let mut specifiers = self.ast().new_vec_with_capacity(1); + let imported = + ModuleExportName::Identifier(IdentifierName::new(SPAN, self.ast().new_atom(imported))); + specifiers.push(ImportDeclarationSpecifier::ImportSpecifier(ImportSpecifier { + span: SPAN, + imported, + local: BindingIdentifier::new(SPAN, self.ast().new_atom(local)), + import_kind: ImportOrExportKind::Value, + })); + let value = ImportOrExportKind::Value; + let import_stmt = + self.ast().import_declaration(SPAN, Some(specifiers), source, None, value); + let decl = self.ast().module_declaration(ModuleDeclaration::ImportDeclaration(import_stmt)); + self.imports.push(decl); + } + + /// `var variable_name = require(source);` + fn add_require_statement( + &mut self, + variable_name: &str, + source: StringLiteral<'a>, + front: bool, + ) { + let var_kind = VariableDeclarationKind::Var; + let callee = { + let ident = IdentifierReference::new(SPAN, "require".into()); + self.ctx.ast.identifier_reference_expression(ident) + }; + let args = { + let arg = Argument::Expression(self.ast().literal_string_expression(source)); + self.ctx.ast.new_vec_single(arg) + }; + let id = { + let ident = BindingIdentifier::new(SPAN, self.ast().new_atom(variable_name)); + self.ast().binding_pattern(self.ast().binding_pattern_identifier(ident), None, false) + }; + let decl = { + let init = self.ast().call_expression(SPAN, callee, args, false, None); + let decl = self.ast().variable_declarator(SPAN, var_kind, id, Some(init), false); + self.ast().new_vec_single(decl) + }; + let variable_declaration = + self.ast().variable_declaration(SPAN, var_kind, decl, Modifiers::empty()); + let stmt = Statement::Declaration(Declaration::VariableDeclaration(variable_declaration)); + if front { + self.imports.insert(0, stmt); + } else { + self.imports.push(stmt); + } + } +} + +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) if !e.opening_element.attributes.is_empty() => { + Some(&e.opening_element.attributes) + } + _ => None, + } + } + + fn children(&self) -> &'b Vec<'a, JSXChild<'a>> { + match self { + Self::Element(e) => &e.children, + 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 && matches!(attr, JSXAttributeItem::Attribute(a) if a.is_key()) { + return true; + } + } + false + } +} + +// Transform jsx +impl<'a> ReactJsx<'a> { + fn transform_jsx<'b>(&mut self, e: &JSXElementOrFragment<'a, 'b>) -> Expression<'a> { + let is_classic = self.default_runtime.is_classic(); + let is_automatic = self.default_runtime.is_automatic(); + let has_key_after_props_spread = e.has_key_after_props_spread(); + + let mut arguments = self.ast().new_vec(); + arguments.push(Argument::Expression(match e { + JSXElementOrFragment::Element(e) => { + self.transform_element_name(&e.opening_element.name) + } + JSXElementOrFragment::Fragment(_) => self.get_fragment(), + })); + + // 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(); + + if let Some(attributes) = attributes { + for attribute in attributes { + match attribute { + // optimize `{...prop}` to `prop` in static mode + JSXAttributeItem::SpreadAttribute(spread) + if is_classic && attributes_len == 1 => + { + // 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; + } + } + JSXAttributeItem::Attribute(attr) if attr.is_key() => { + // if attr.value.is_none() { + // self.ctx.error(ValuelessKey(attr.name.span())); + // } + // 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 { + 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 { + let allocator = self.ast().allocator; + let mut children = Vec::from_iter_in( + children.iter().filter_map(|child| self.transform_jsx_child(child)), + allocator, + ); + let children_len = children.len(); + if children_len != 0 { + let value = if children_len == 1 { + children.pop().unwrap() + } else { + let elements = Vec::from_iter_in( + children.into_iter().map(ArrayExpressionElement::Expression), + allocator, + ); + need_jsxs = true; + self.ast().array_expression(SPAN, elements, None) + }; + let object_property = { + let kind = PropertyKind::Init; + let ident = IdentifierName::new(SPAN, "children".into()); + let key = self.ast().property_key_identifier(ident); + self.ast().object_property(SPAN, kind, key, value, None, false, false, false) + }; + properties.push(ObjectPropertyKind::ObjectProperty(object_property)); + } + } + + self.add_import(e, has_key_after_props_spread, need_jsxs); + + if !properties.is_empty() || is_automatic { + let object_expression = self.ast().object_expression(SPAN, properties, None); + arguments.push(Argument::Expression(object_expression)); + } + + if is_automatic && key_prop.is_some() { + arguments.push(Argument::Expression(self.transform_jsx_attribute_value(key_prop))); + } + + if is_classic && !children.is_empty() { + arguments.extend( + children + .iter() + .filter_map(|child| self.transform_jsx_child(child)) + .map(Argument::Expression), + ); + } + + let callee = self.get_create_element(has_key_after_props_spread, need_jsxs); + self.ast().call_expression(SPAN, callee, arguments, false, None) + } + + fn transform_element_name(&self, name: &JSXElementName<'a>) -> Expression<'a> { + match name { + JSXElementName::Identifier(ident) => { + if ident.name == "this" { + self.ast().this_expression(SPAN) + } else if ident.name.chars().next().is_some_and(|c| c.is_ascii_lowercase()) { + let string = StringLiteral::new(SPAN, ident.name.clone()); + self.ast().literal_string_expression(string) + } else { + let ident = IdentifierReference::new(SPAN, ident.name.clone()); + self.ctx.ast.identifier_reference_expression(ident) + } + } + JSXElementName::MemberExpression(member_expr) => { + self.transform_jsx_member_expression(member_expr) + } + JSXElementName::NamespacedName(name) => { + // if self.options.throw_if_namespace { + // self.ctx.error(NamespaceDoesNotSupport(name.span)); + // } + let name = self.ast().new_atom(&name.to_string()); + let string_literal = StringLiteral::new(SPAN, name); + self.ast().literal_string_expression(string_literal) + } + } + } + + fn get_fragment(&self) -> Expression<'a> { + match self.options.runtime { + ReactJsxRuntime::Classic => { + if self.options.pragma_frag == "React.Fragment" { + let object = self.get_react_references(); + let property = IdentifierName::new(SPAN, "Fragment".into()); + self.ast().static_member_expression(SPAN, object, property, false) + } else { + self.get_call_expression_callee(self.options.pragma_frag.as_ref()) + } + } + ReactJsxRuntime::Automatic => { + if self.is_script() { + self.get_static_member_expression("_reactJsxRuntime", "Fragment") + } else { + let ident = IdentifierReference::new(SPAN, "_Fragment".into()); + self.ast().identifier_reference_expression(ident) + } + } + } + } + + fn get_create_element(&self, has_key_after_props_spread: bool, jsxs: bool) -> Expression<'a> { + match self.options.runtime { + ReactJsxRuntime::Classic => { + if self.options.pragma == "React.createElement" { + let object = self.get_react_references(); + let property = IdentifierName::new(SPAN, "createElement".into()); + self.ast().static_member_expression(SPAN, object, property, false) + } else { + self.get_call_expression_callee(self.options.pragma.as_ref()) + } + } + ReactJsxRuntime::Automatic => { + let name = if self.is_script() { + if has_key_after_props_spread { + "createElement" + } else if jsxs { + "jsxs" + } else { + "jsx" + } + } else if has_key_after_props_spread { + "_createElement" + } else if jsxs { + "_jsxs" + } else { + "_jsx" + }; + if self.is_script() { + let object_ident_name = + if has_key_after_props_spread { "_react" } else { "_reactJsxRuntime" }; + self.get_static_member_expression(object_ident_name, name) + } else { + let ident = IdentifierReference::new(SPAN, name.into()); + self.ast().identifier_reference_expression(ident) + } + } + } + } + + fn get_react_references(&self) -> Expression<'a> { + let ident = IdentifierReference::new(SPAN, "React".into()); + self.ast().identifier_reference_expression(ident) + } + + fn get_static_member_expression( + &self, + object_ident_name: &str, + property_name: &str, + ) -> Expression<'a> { + let property = IdentifierName::new(SPAN, self.ast().new_atom(property_name)); + let ident = IdentifierReference::new(SPAN, self.ast().new_atom(object_ident_name)); + let object = self.ast().identifier_reference_expression(ident); + self.ast().static_member_expression(SPAN, object, property, false) + } + + /// Get the callee from `pragma` and `pragmaFrag` + fn get_call_expression_callee(&self, literal_callee: &str) -> Expression<'a> { + let mut callee = literal_callee.split('.'); + let member = callee.next().unwrap(); + let property = callee.next(); + property.map_or_else( + || { + let ident = IdentifierReference::new(SPAN, self.ast().new_atom(member)); + self.ast().identifier_reference_expression(ident) + }, + |property_name| self.get_static_member_expression(member, property_name), + ) + } + + fn transform_jsx_member_expression(&self, expr: &JSXMemberExpression<'a>) -> Expression<'a> { + let object = match &expr.object { + JSXMemberExpressionObject::Identifier(ident) => { + let ident = IdentifierReference::new(SPAN, ident.name.clone()); + self.ast().identifier_reference_expression(ident) + } + JSXMemberExpressionObject::MemberExpression(expr) => { + self.transform_jsx_member_expression(expr) + } + }; + let property = IdentifierName::new(SPAN, expr.property.name.clone()); + self.ast().static_member_expression(SPAN, object, property, false) + } + + 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() => { + properties.extend(self.ast().copy(&expr.properties)); + } + 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>>, + ) -> Expression<'a> { + match value { + Some(JSXAttributeValue::StringLiteral(s)) => { + let jsx_text = Self::decode_entities(s.value.as_str()); + let literal = StringLiteral::new(s.span, self.ast().new_atom(&jsx_text)); + self.ast().literal_string_expression(literal) + } + Some(JSXAttributeValue::Element(e)) => { + self.transform_jsx(&JSXElementOrFragment::Element(e)) + } + Some(JSXAttributeValue::Fragment(e)) => { + self.transform_jsx(&JSXElementOrFragment::Fragment(e)) + } + Some(JSXAttributeValue::ExpressionContainer(c)) => match &c.expression { + JSXExpression::Expression(e) => self.ast().copy(e), + JSXExpression::EmptyExpression(_e) => { + self.ast().literal_boolean_expression(BooleanLiteral::new(SPAN, true)) + } + }, + None => self.ast().literal_boolean_expression(BooleanLiteral::new(SPAN, true)), + } + } + + fn transform_jsx_child(&mut self, child: &JSXChild<'a>) -> Option> { + match child { + JSXChild::Text(text) => self.transform_jsx_text(text.value.as_str()), + JSXChild::ExpressionContainer(e) => match &e.expression { + JSXExpression::Expression(e) => Some(self.ast().copy(e)), + JSXExpression::EmptyExpression(_) => None, + }, + JSXChild::Element(e) => Some(self.transform_jsx(&JSXElementOrFragment::Element(e))), + JSXChild::Fragment(e) => Some(self.transform_jsx(&JSXElementOrFragment::Fragment(e))), + JSXChild::Spread(_e) => { + // self.ctx.error(SpreadChildrenAreNotSupported(e.span)); + None + } + } + } + + fn get_attribute_name(&self, name: &JSXAttributeName<'a>) -> PropertyKey<'a> { + match name { + JSXAttributeName::Identifier(ident) => { + let name = ident.name.clone(); + if ident.name.contains('-') { + let expr = self.ast().literal_string_expression(StringLiteral::new(SPAN, name)); + self.ast().property_key_expression(expr) + } else { + self.ast().property_key_identifier(IdentifierName::new(SPAN, name)) + } + } + JSXAttributeName::NamespacedName(name) => { + let name = self.ast().new_atom(&name.to_string()); + let expr = self.ast().literal_string_expression(StringLiteral::new(SPAN, name)); + self.ast().property_key_expression(expr) + } + } + } + + fn transform_jsx_text(&self, text: &str) -> Option> { + Self::fixup_whitespace_and_decode_entities(text).map(|s| { + let s = StringLiteral::new(SPAN, self.ast().new_atom(&s)); + self.ast().literal_string_expression(s) + }) + } + + /// JSX trims whitespace at the end and beginning of lines, except that the + /// start/end of a tag is considered a start/end of a line only if that line is + /// on the same line as the closing tag. See examples in + /// tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx + /// See also and + /// + /// An equivalent algorithm would be: + /// - If there is only one line, return it. + /// - If there is only whitespace (but multiple lines), return `undefined`. + /// - Split the text into lines. + /// - 'trimRight' the first line, 'trimLeft' the last line, 'trim' middle lines. + /// - Decode entities on each line (individually). + /// - Remove empty lines and join the rest with " ". + /// + /// + fn fixup_whitespace_and_decode_entities(text: &str) -> Option { + let mut acc: Option = None; + let mut first_non_whitespace: Option = Some(0); + let mut last_non_whitespace: Option = None; + let mut i: usize = 0; + for c in text.chars() { + if is_line_terminator(c) { + if let (Some(first), Some(last)) = (first_non_whitespace, last_non_whitespace) { + acc = Some(Self::add_line_of_jsx_text(acc, &text[first..=last])); + } + first_non_whitespace = None; + } else if c != ' ' && !is_irregular_whitespace(c) { + last_non_whitespace = Some(i); + if first_non_whitespace.is_none() { + first_non_whitespace.replace(i); + } + } + i += c.len_utf8(); + } + if let Some(first) = first_non_whitespace { + Some(Self::add_line_of_jsx_text(acc, &text[first..])) + } else { + acc + } + } + + fn add_line_of_jsx_text(acc: Option, trimmed_line: &str) -> String { + let decoded = Self::decode_entities(trimmed_line); + if let Some(acc) = acc { + format!("{acc} {decoded}") + } else { + decoded + } + } + + /// Replace entities like " ", "{", and "�" with the characters they encode. + /// * See + /// Code adapted from + fn decode_entities(s: &str) -> String { + let mut buffer = vec![]; + let mut chars = s.bytes().enumerate(); + let mut prev = 0; + while let Some((i, c)) = chars.next() { + if c == b'&' { + let start = i; + let mut end = None; + for (j, c) in chars.by_ref() { + if c == b';' { + end.replace(j); + break; + } + } + if let Some(end) = end { + let word = &s[start + 1..end]; + buffer.extend_from_slice(s[prev..start].as_bytes()); + prev = end + 1; + if let Some(c) = XML_ENTITIES.get(word) { + buffer.extend_from_slice(c.to_string().as_bytes()); + } + } + } + } + buffer.extend_from_slice(s[prev..].as_bytes()); + #[allow(unsafe_code)] + // SAFETY: The buffer is constructed from valid utf chars. + unsafe { + String::from_utf8_unchecked(buffer) + } } } diff --git a/crates/oxc_transformer/src/react/mod.rs b/crates/oxc_transformer/src/react/mod.rs index cfdc90087..3340e69d3 100644 --- a/crates/oxc_transformer/src/react/mod.rs +++ b/crates/oxc_transformer/src/react/mod.rs @@ -53,18 +53,28 @@ impl<'a> React<'a> { // Transforms impl<'a> React<'a> { - pub fn transform_expression(&self, expr: &mut Expression<'a>) { + pub fn transform_program_on_exit(&mut self, program: &mut Program<'a>) { + if self.options.jsx_plugin { + self.jsx.transform_program_on_exit(program); + } + } + + pub fn transform_expression(&mut self, expr: &mut Expression<'a>) { match expr { Expression::AssignmentExpression(e) => { if self.options.display_name_plugin { self.display_name.transform_assignment_expression(e); } } - Expression::JSXElement(_e) => { - // *expr = unimplemented!(); + Expression::JSXElement(e) => { + if self.options.jsx_plugin { + *expr = self.jsx.transform_jsx_element(e); + } } - Expression::JSXFragment(_e) => { - // *expr = unimplemented!(); + Expression::JSXFragment(e) => { + if self.options.jsx_plugin { + *expr = self.jsx.transform_jsx_fragment(e); + } } _ => {} } diff --git a/crates/oxc_transformer/src/react/options.rs b/crates/oxc_transformer/src/react/options.rs index d2c123ed6..15216e73c 100644 --- a/crates/oxc_transformer/src/react/options.rs +++ b/crates/oxc_transformer/src/react/options.rs @@ -5,6 +5,9 @@ use serde::Deserialize; #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ReactOptions { + #[serde(skip)] + pub jsx_plugin: bool, + #[serde(skip)] pub display_name_plugin: bool, @@ -65,6 +68,7 @@ pub struct ReactOptions { impl Default for ReactOptions { fn default() -> Self { Self { + jsx_plugin: false, display_name_plugin: false, jsx_self_plugin: false, jsx_source_plugin: false, @@ -100,10 +104,20 @@ fn default_for_pragma_frag() -> Cow<'static, str> { /// /// Auto imports the functions that JSX transpiles to. /// classic does not automatic import anything. -#[derive(Debug, Default, Clone, Deserialize)] +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Deserialize)] pub enum ReactJsxRuntime { Classic, /// The default runtime is switched to automatic in Babel 8. #[default] Automatic, } + +impl ReactJsxRuntime { + pub fn is_classic(self) -> bool { + self == Self::Classic + } + + pub fn is_automatic(self) -> bool { + self == Self::Automatic + } +} diff --git a/tasks/common/src/babel.rs b/tasks/common/src/babel.rs index 65ddcab5d..939f0a120 100644 --- a/tasks/common/src/babel.rs +++ b/tasks/common/src/babel.rs @@ -14,6 +14,8 @@ pub struct BabelOptions { #[serde(default)] pub plugins: Vec, // Can be a string or an array #[serde(default)] + pub presets: Vec, // Can be a string or an array + #[serde(default)] pub allow_return_outside_function: bool, #[serde(default)] pub allow_await_outside_function: bool, @@ -88,12 +90,21 @@ impl BabelOptions { /// * `Some>` if the plugin exists with a config /// * `None` if the plugin does not exist pub fn get_plugin(&self, name: &str) -> Option> { - self.plugins.iter().find_map(|v| match v { + self.plugins.iter().find_map(|v| Self::get_value(v, name)) + } + + pub fn get_preset(&self, name: &str) -> Option> { + self.presets.iter().find_map(|v| Self::get_value(v, name)) + } + + #[allow(clippy::option_option)] + fn get_value(value: &Value, name: &str) -> Option> { + match value { Value::String(s) if s == name => Some(None), Value::Array(a) if a.first().and_then(Value::as_str).is_some_and(|s| s == name) => { Some(a.get(1).cloned()) } _ => None, - }) + } } } diff --git a/tasks/transform_conformance/babel.snap.md b/tasks/transform_conformance/babel.snap.md index 05cc73510..924c5932d 100644 --- a/tasks/transform_conformance/babel.snap.md +++ b/tasks/transform_conformance/babel.snap.md @@ -1,4 +1,4 @@ -Passed: 83/392 +Passed: 131/392 # All Passed: * babel-plugin-transform-react-jsx-source @@ -125,44 +125,34 @@ Passed: 83/392 * regression/15768/input.ts * variable-declaration/non-null-in-optional-chain/input.ts -# babel-preset-react (2/13) +# babel-preset-react (4/13) * preset-options/development/input.js * preset-options/development-runtime-automatic/input.js * preset-options/development-runtime-automatic-windows/input.js * preset-options/development-windows/input.js -* preset-options/empty-options/input.js -* preset-options/runtime-automatic/input.js * preset-options/runtime-classic/input.js * preset-options/runtime-classic-pragma-no-frag/input.js * regression/11294/input.mjs * regression/another-preset-with-custom-jsx-keep-source-self/input.mjs * regression/runtime-classic-allow-multiple-source-self/input.mjs -# babel-plugin-transform-react-jsx (1/156) -* autoImport/after-polyfills/input.mjs -* autoImport/after-polyfills-2/input.mjs +# babel-plugin-transform-react-jsx (47/156) * autoImport/after-polyfills-compiled-to-cjs/input.mjs -* autoImport/after-polyfills-script-not-supported/input.js * autoImport/auto-import-react-source-type-module/input.js -* autoImport/auto-import-react-source-type-script/input.js * autoImport/complicated-scope-module/input.js -* 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-default-pragma-classic-runtime/input.js * pure/false-pragma-comment-automatic-runtime/input.js * pure/false-pragma-comment-classic-runtime/input.js * pure/false-pragma-option-automatic-runtime/input.js * pure/false-pragma-option-classic-runtime/input.js -* pure/true-default-pragma-automatic-runtime/input.js * pure/true-default-pragma-classic-runtime/input.js * pure/true-pragma-comment-automatic-runtime/input.js * pure/true-pragma-comment-classic-runtime/input.js * pure/true-pragma-option-automatic-runtime/input.js * pure/true-pragma-option-classic-runtime/input.js -* pure/unset-default-pragma-automatic-runtime/input.js * pure/unset-default-pragma-classic-runtime/input.js * pure/unset-pragma-comment-automatic-runtime/input.js * pure/unset-pragma-comment-classic-runtime/input.js @@ -223,62 +213,26 @@ Passed: 83/392 * react/wraps-props-in-react-spread-for-first-spread-attributes/input.js * react/wraps-props-in-react-spread-for-last-spread-attributes/input.js * react/wraps-props-in-react-spread-for-middle-spread-attributes/input.js -* react-automatic/adds-appropriate-newlines-when-using-spread-attribute/input.js * react-automatic/arrow-functions/input.js -* react-automatic/assignment/input.js * 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-constructor-as-prop/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-jsxtext/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 -* react-automatic/should-not-strip-tags-with-a-single-child-of-nbsp/input.js -* react-automatic/should-properly-handle-comments-between-props/input.js * react-automatic/should-properly-handle-keys/input.js -* react-automatic/should-properly-handle-null-prop-spread/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-throw-when-filter-is-specified/input.js -* react-automatic/should-transform-known-hyphenated-tags/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 -* react-automatic/wraps-props-in-react-spread-for-last-spread-attributes/input.js -* react-automatic/wraps-props-in-react-spread-for-middle-spread-attributes/input.js * regression/issue-12478-automatic/input.js * regression/issue-12478-classic/input.js * regression/pragma-frag-set-default-classic-runtime/input.js @@ -287,10 +241,8 @@ Passed: 83/392 * removed-options/invalid-use-spread-false/input.js * removed-options/invalid-use-spread-true/input.js * runtime/classic/input.js -* runtime/defaults-to-automatic/input.js * runtime/invalid-runtime/input.js * runtime/pragma-runtime-classsic/input.js -* runtime/runtime-automatic/input.js * sourcemaps/JSXText/input.js * spread-transform/transform-to-babel-extend/input.js * spread-transform/transform-to-object-assign/input.js diff --git a/tasks/transform_conformance/src/test_case.rs b/tasks/transform_conformance/src/test_case.rs index fc40b38a6..f346b244d 100644 --- a/tasks/transform_conformance/src/test_case.rs +++ b/tasks/transform_conformance/src/test_case.rs @@ -87,13 +87,33 @@ pub trait TestCase { } let options = self.options(); - let mut react = options - .get_plugin("transform-react-jsx") - .map(get_options::) - .unwrap_or_default(); - react.display_name_plugin = options.get_plugin("transform-react-display-name").is_some(); - react.jsx_self_plugin = options.get_plugin("transform-react-jsx-self").is_some(); - react.jsx_source_plugin = options.get_plugin("transform-react-jsx-source").is_some(); + let react = options.get_preset("react").map_or_else( + || { + let mut react_options = options + .get_plugin("transform-react-jsx") + .map(|options| { + let mut options = get_options::(options); + options.jsx_plugin = true; + options + }) + .unwrap_or_default(); + react_options.display_name_plugin = + options.get_plugin("transform-react-display-name").is_some(); + react_options.jsx_self_plugin = + options.get_plugin("transform-react-jsx-self").is_some(); + react_options.jsx_source_plugin = + options.get_plugin("transform-react-jsx-source").is_some(); + react_options + }, + |options| { + let mut react_options = get_options::(options); + react_options.jsx_plugin = true; + react_options.jsx_self_plugin = true; + react_options.jsx_source_plugin = true; + react_options.display_name_plugin = true; + react_options + }, + ); TransformOptions { assumptions: serde_json::from_value(options.assumptions.clone()).unwrap_or_default(), @@ -263,10 +283,10 @@ impl TestCase for ConformanceTestCase { let passed = transformed_code == output || (!output.is_empty() && actual_errors.contains(&output)); if filtered { - println!("Input:\n"); - println!("{input}\n"); println!("Options:"); println!("{transform_options:#?}\n"); + println!("Input:\n"); + println!("{input}\n"); if babel_options.throws.is_some() { println!("Expected Errors:\n"); println!("{output}\n");