mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(transformer): implement react transform attributes (#1071)
This commit is contained in:
parent
162c720ef1
commit
e16e7e44e0
3 changed files with 233 additions and 120 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -17,33 +17,85 @@ pub struct ReactJsx<'a> {
|
|||
ast: Rc<AstBuilder<'a>>,
|
||||
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<AstBuilder<'a>>, 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<Expression<'a>> {
|
||||
let callee = self.transform_create_element();
|
||||
fn transform_jsx<'b>(&mut self, e: &JSXElementOrFragment<'a, 'b>) -> Option<Expression<'a>> {
|
||||
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<Expression<'a>> {
|
||||
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<Expression<'a>> {
|
||||
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<Expression<'a>> {
|
||||
fn transform_jsx_child(&mut self, child: &JSXChild<'a>) -> Option<Expression<'a>> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue