feat(transformer): react jsx transform (#2961)

This commit is contained in:
Boshen 2024-04-14 10:50:17 +08:00 committed by GitHub
parent 01e64bf64d
commit bd9fc6d169
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 770 additions and 75 deletions

View file

@ -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 }
}

View file

@ -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);
}

View file

@ -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:
///
/// * <https://babeljs.io/docs/babel-plugin-transform-react-jsx>
/// * <https://github.com/babel/babel/tree/main/packages/babel-helper-builder-react-jsx>
#[allow(unused)]
pub struct ReactJsx<'a> {
options: Rc<ReactOptions>,
ctx: Ctx<'a>,
// States
imports: std::vec::Vec<Statement<'a>>,
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<ReactOptions>, 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
/// <https://github.com/microsoft/TypeScript/blob/6134091642f57c32f50e7b5604635e4d37dd19e8/src/compiler/transformers/jsx.ts#L264-L278>
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 `<div key={true} />`
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<Expression<'a>> {
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<Expression<'a>> {
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 <https://www.w3.org/TR/html4/struct/text.html#h-9.1> and <https://www.w3.org/TR/CSS2/text.html#white-space-model>
///
/// 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 " ".
///
/// <https://github.com/microsoft/TypeScript/blob/f0374ce2a9c465e27a15b7fa4a347e2bd9079450/src/compiler/transformers/jsx.ts#L557-L608>
fn fixup_whitespace_and_decode_entities(text: &str) -> Option<String> {
let mut acc: Option<String> = None;
let mut first_non_whitespace: Option<usize> = Some(0);
let mut last_non_whitespace: Option<usize> = 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<String>, trimmed_line: &str) -> String {
let decoded = Self::decode_entities(trimmed_line);
if let Some(acc) = acc {
format!("{acc} {decoded}")
} else {
decoded
}
}
/// Replace entities like "&nbsp;", "&#123;", and "&#xDEADBEEF;" with the characters they encode.
/// * See <https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references>
/// Code adapted from <https://github.com/microsoft/TypeScript/blob/514f7e639a2a8466c075c766ee9857a30ed4e196/src/compiler/transformers/jsx.ts#L617C1-L635>
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)
}
}
}

View file

@ -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);
}
}
_ => {}
}

View file

@ -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
}
}

View file

@ -14,6 +14,8 @@ pub struct BabelOptions {
#[serde(default)]
pub plugins: Vec<Value>, // Can be a string or an array
#[serde(default)]
pub presets: Vec<Value>, // 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<Some<Value>>` if the plugin exists with a config
/// * `None` if the plugin does not exist
pub fn get_plugin(&self, name: &str) -> Option<Option<Value>> {
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<Option<Value>> {
self.presets.iter().find_map(|v| Self::get_value(v, name))
}
#[allow(clippy::option_option)]
fn get_value(value: &Value, name: &str) -> Option<Option<Value>> {
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,
})
}
}
}

View file

@ -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

View file

@ -87,13 +87,33 @@ pub trait TestCase {
}
let options = self.options();
let mut react = options
.get_plugin("transform-react-jsx")
.map(get_options::<ReactOptions>)
.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::<ReactOptions>(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::<ReactOptions>(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");