From d8f1a7fce6fa3b9c7b97edc1b9d8a8bd87afc5da Mon Sep 17 00:00:00 2001 From: Boshen Date: Thu, 26 Oct 2023 17:27:05 +0800 Subject: [PATCH] feat(transformer): start implementing react jsx transform (#1057) --- crates/oxc_ast/src/ast/literal.rs | 4 + .../oxc_transformer/examples/transformer.rs | 9 +- crates/oxc_transformer/src/lib.rs | 4 +- crates/oxc_transformer/src/react_jsx/mod.rs | 205 ++++++++++++++++-- .../oxc_transformer/src/react_jsx/options.rs | 27 +++ tasks/transform_conformance/babel.snap.md | 60 ++--- tasks/transform_conformance/src/test_case.rs | 4 +- 7 files changed, 255 insertions(+), 58 deletions(-) create mode 100644 crates/oxc_transformer/src/react_jsx/options.rs diff --git a/crates/oxc_ast/src/ast/literal.rs b/crates/oxc_ast/src/ast/literal.rs index 8cc202f72..170df53fd 100644 --- a/crates/oxc_ast/src/ast/literal.rs +++ b/crates/oxc_ast/src/ast/literal.rs @@ -21,6 +21,10 @@ pub struct BooleanLiteral { } impl BooleanLiteral { + pub fn new(span: Span, value: bool) -> Self { + Self { span, value } + } + pub fn as_str(&self) -> &'static str { if self.value { "true" diff --git a/crates/oxc_transformer/examples/transformer.rs b/crates/oxc_transformer/examples/transformer.rs index b71d59d8c..c900b5281 100644 --- a/crates/oxc_transformer/examples/transformer.rs +++ b/crates/oxc_transformer/examples/transformer.rs @@ -5,7 +5,7 @@ use oxc_codegen::{Codegen, CodegenOptions}; use oxc_parser::Parser; use oxc_semantic::SemanticBuilder; use oxc_span::SourceType; -use oxc_transformer::{TransformOptions, TransformTarget, Transformer}; +use oxc_transformer::{ReactJsxOptions, TransformOptions, TransformTarget, Transformer}; // Instruction: // create a `test.js`, @@ -39,8 +39,11 @@ fn main() { let scopes = Rc::new(RefCell::new(scopes)); let program = allocator.alloc(ret.program); - let transform_options = - TransformOptions { target: TransformTarget::ES2015, ..TransformOptions::default() }; + let transform_options = TransformOptions { + target: TransformTarget::ES2015, + react_jsx: Some(ReactJsxOptions::default()), + ..TransformOptions::default() + }; Transformer::new(&allocator, source_type, &symbols, &scopes, transform_options).build(program); let printed = Codegen::::new(source_text.len(), codegen_options).build(program); println!("Transformed:\n"); diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index d78ad66dd..ffae6a942 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -45,7 +45,6 @@ pub use crate::{ pub struct Transformer<'a> { #[allow(unused)] typescript: Option>, - #[allow(unused)] react_jsx: Option>, regexp_flags: Option>, // es2022 @@ -110,6 +109,7 @@ impl<'a> VisitMut<'a> for Transformer<'a> { for stmt in stmts.iter_mut() { self.visit_statement(stmt); } + self.react_jsx.as_mut().map(|t| t.add_react_jsx_runtime_import(stmts)); // TODO: we need scope id to insert the vars into the correct statements self.es2021_logical_assignment_operators.as_mut().map(|t| t.add_vars_to_statements(stmts)); self.es2020_nullish_coalescing_operators.as_mut().map(|t| t.add_vars_to_statements(stmts)); @@ -118,7 +118,7 @@ impl<'a> VisitMut<'a> for Transformer<'a> { fn visit_expression(&mut self, expr: &mut Expression<'a>) { // self.typescript.as_mut().map(|t| t.transform_expression(expr)); - // self.react_jsx.as_mut().map(|t| t.transform_expression(expr)); + self.react_jsx.as_mut().map(|t| t.transform_expression(expr)); self.regexp_flags.as_mut().map(|t| t.transform_expression(expr)); self.es2021_logical_assignment_operators.as_mut().map(|t| t.transform_expression(expr)); diff --git a/crates/oxc_transformer/src/react_jsx/mod.rs b/crates/oxc_transformer/src/react_jsx/mod.rs index 37483f049..f6e6f71f5 100644 --- a/crates/oxc_transformer/src/react_jsx/mod.rs +++ b/crates/oxc_transformer/src/react_jsx/mod.rs @@ -1,19 +1,14 @@ +mod options; + use std::rc::Rc; -use oxc_ast::AstBuilder; +use oxc_allocator::Vec; +use oxc_ast::{ast::*, AstBuilder}; +use oxc_span::Span; -#[derive(Debug, Default, Clone, Copy)] -pub struct ReactJsxOptions { - _runtime: ReactJsxRuntime, -} +pub use self::options::{ReactJsxOptions, ReactJsxRuntime}; -#[derive(Debug, Default, Clone, Copy)] -pub enum ReactJsxRuntime { - #[default] - Classic, - #[allow(unused)] - Automatic, -} +const SPAN: Span = Span::new(0, 0); /// Transform React JSX /// @@ -21,12 +16,190 @@ pub enum ReactJsxRuntime { /// * /// * pub struct ReactJsx<'a> { - _ast: Rc>, - _options: ReactJsxOptions, + ast: Rc>, + options: ReactJsxOptions, + + has_jsx: bool, } impl<'a> ReactJsx<'a> { - pub fn new(_ast: Rc>, _options: ReactJsxOptions) -> Self { - Self { _ast, _options } + pub fn new(ast: Rc>, options: ReactJsxOptions) -> Self { + Self { ast, options, has_jsx: 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 add_react_jsx_runtime_import(&self, stmts: &mut Vec<'a, Statement<'a>>) { + if self.options.runtime.is_classic() || !self.has_jsx { + return; + } + + 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()), + import_kind: ImportOrExportKind::Value, + })); + let source = StringLiteral::new(SPAN, "react/jsx-runtime".into()); + let import_statement = self.ast.import_declaration( + SPAN, + Some(specifiers), + source, + None, + ImportOrExportKind::Value, + ); + let decl = + self.ast.module_declaration(ModuleDeclaration::ImportDeclaration(import_statement)); + stmts.insert(0, decl); + } + + fn transform_jsx_element(&self, e: &JSXElement<'a>) -> Option> { + let callee = self.transform_create_element(); + + 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), + ); + + Some(self.ast.call_expression(SPAN, callee, arguments, false, None)) + } + + fn transform_create_element(&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 property = IdentifierName::new(SPAN, "createElement".into()); + self.ast.static_member_expression(SPAN, object, property, false) + } + ReactJsxRuntime::Automatic => { + let ident = IdentifierReference::new(SPAN, "_jsx".into()); + self.ast.identifier_reference_expression(ident) + } + } + } + + fn transform_element_name(&self, name: &JSXElementName<'a>) -> Option> { + match name { + JSXElementName::Identifier(ident) => { + let name = ident.name.clone(); + Some(if ident.name.chars().next().is_some_and(|c| c.is_ascii_lowercase()) { + self.ast.literal_string_expression(StringLiteral::new(SPAN, name)) + } else { + self.ast.identifier_reference_expression(IdentifierReference::new(SPAN, name)) + }) + } + _ => { + /* TODO */ + None + } + } + } + + 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> { + match child { + JSXChild::Text(text) => { + let text = text.value.trim(); + (!text.trim().is_empty()).then(|| { + let text = text + .split(char::is_whitespace) + .map(str::trim) + .filter(|c| !c.is_empty()) + .collect::>() + .join(" "); + let s = StringLiteral::new(SPAN, text.into()); + self.ast.literal_string_expression(s) + }) + } + JSXChild::ExpressionContainer(e) => match &e.expression { + JSXExpression::Expression(e) => Some(self.ast.copy(e)), + JSXExpression::EmptyExpression(_) => None, + }, + JSXChild::Element(e) => self.transform_jsx_element(e), + _ => None, + } } } diff --git a/crates/oxc_transformer/src/react_jsx/options.rs b/crates/oxc_transformer/src/react_jsx/options.rs new file mode 100644 index 000000000..34fe15dc9 --- /dev/null +++ b/crates/oxc_transformer/src/react_jsx/options.rs @@ -0,0 +1,27 @@ +use serde::Deserialize; + +#[derive(Debug, Default, Clone, Copy, Deserialize)] +pub struct ReactJsxOptions { + /// Decides which runtime to use. + pub runtime: ReactJsxRuntime, +} + +#[derive(Debug, Default, Clone, Copy, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ReactJsxRuntime { + /// Does not automatically import anything (default). + #[default] + Classic, + /// Auto imports the functions that JSX transpiles to. + Automatic, +} + +impl ReactJsxRuntime { + pub fn is_classic(&self) -> bool { + matches!(self, Self::Classic) + } + + pub fn is_automatic(&self) -> bool { + matches!(self, Self::Automatic) + } +} diff --git a/tasks/transform_conformance/babel.snap.md b/tasks/transform_conformance/babel.snap.md index 1c0115594..acd37e1c5 100644 --- a/tasks/transform_conformance/babel.snap.md +++ b/tasks/transform_conformance/babel.snap.md @@ -1,4 +1,4 @@ -Passed: 175/1083 +Passed: 187/1083 # All Passed: * babel-plugin-transform-numeric-separator @@ -804,7 +804,7 @@ Passed: 175/1083 * regression/11061/input.mjs * variable-declaration/non-null-in-optional-chain/input.ts -# babel-plugin-transform-react-jsx (26/172) +# babel-plugin-transform-react-jsx (38/172) * autoImport/after-polyfills/input.mjs * autoImport/after-polyfills-2/input.mjs * autoImport/after-polyfills-compiled-to-cjs/input.mjs @@ -817,41 +817,36 @@ Passed: 175/1083 * 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 +* pure/unset-pragma-option-automatic-runtime/input.js * pure/unset-pragma-option-classic-runtime/input.js -* react/adds-appropriate-newlines-when-using-spread-attribute/input.js +* react/.should-properly-handle-comments-adjacent-to-children/input.js * react/adds-appropriate-newlines-when-using-spread-attribute-babel-7/input.js * react/arrow-functions/input.js -* react/assignment/input.js * react/assignment-babel-7/input.js * react/avoids-spread-babel-7/input.js -* react/concatenates-adjacent-string-literals/input.js * react/does-not-add-source-self/input.mjs * react/does-not-add-source-self-babel-7/input.mjs -* react/dont-coerce-expression-containers/input.js * 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 * react/honor-custom-jsx-pragma-option/input.js -* react/jsx-with-retainlines-option/input.js -* react/jsx-without-retainlines-option/input.js * react/optimisation.react.constant-elements/input.js * react/pragma-works-with-no-space-at-the-end/input.js -* react/proto-in-jsx-attribute/input.js * react/should-add-quotes-es3/input.js -* react/should-allow-constructor-as-prop/input.js * react/should-allow-deeper-js-namespacing/input.js * react/should-allow-elements-as-attributes/input.js * react/should-allow-js-namespacing/input.js @@ -859,37 +854,28 @@ Passed: 175/1083 * react/should-allow-nested-fragments/input.js * react/should-allow-no-pragmafrag-if-frag-unused/input.js * react/should-allow-pragmafrag-and-frag/input.js -* react/should-avoid-wrapping-in-extra-parens-if-not-needed/input.js -* react/should-convert-simple-tags/input.js -* react/should-convert-simple-text/input.js +* react/should-disallow-spread-children/input.js +* react/should-disallow-valueless-key/input.js * react/should-escape-xhtml-jsxattribute/input.js * react/should-escape-xhtml-jsxattribute-babel-7/input.js * react/should-escape-xhtml-jsxtext/input.js * react/should-escape-xhtml-jsxtext-babel-7/input.js * react/should-handle-attributed-elements/input.js -* react/should-handle-has-own-property-correctly/input.js -* react/should-have-correct-comma-in-nested-children/input.js -* react/should-insert-commas-after-expressions-before-whitespace/input.js * react/should-not-add-quotes-to-identifier-names/input.js -* react/should-not-allow-jsx-pragma-to-be-anywhere-in-comment/input.js * react/should-not-mangle-expressioncontainer-attribute-values/input.js * react/should-not-strip-nbsp-even-coupled-with-other-whitespace/input.js * react/should-not-strip-tags-with-a-single-child-of-nbsp/input.js -* react/should-properly-handle-comments-between-props/input.js * react/should-quote-jsx-attributes/input.js * react/should-support-xml-namespaces-if-flag/input.js -* react/should-transform-known-hyphenated-tags/input.js +* react/should-warn-when-importSource-is-set/input.js +* react/should-warn-when-importSource-pragma-is-set/input.js * react/this-tag-name/input.js * react/weird-symbols/input.js -* react/wraps-props-in-react-spread-for-first-spread-attributes/input.js * react/wraps-props-in-react-spread-for-first-spread-attributes-babel-7/input.js -* react/wraps-props-in-react-spread-for-last-spread-attributes/input.js * react/wraps-props-in-react-spread-for-last-spread-attributes-babel-7/input.js -* react/wraps-props-in-react-spread-for-middle-spread-attributes/input.js * react/wraps-props-in-react-spread-for-middle-spread-attributes-babel-7/input.js -* react-automatic/adds-appropriate-newlines-when-using-spread-attribute/input.js +* react-automatic/.should-properly-handle-comments-adjacent-to-children/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 @@ -907,7 +893,6 @@ Passed: 175/1083 * 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 @@ -915,6 +900,9 @@ Passed: 175/1083 * 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 @@ -922,33 +910,33 @@ Passed: 175/1083 * 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-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/issue-15353-classic/input.js * regression/pragma-frag-set-default-classic-runtime/input.js -* runtime/classic/input.js +* removed-options/invalid-use-builtins-false/input.js +* removed-options/invalid-use-builtins-true/input.js +* removed-options/invalid-use-spread-false/input.js +* removed-options/invalid-use-spread-true/input.js * runtime/defaults-to-automatic/input.js -* runtime/defaults-to-classis-babel-7/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 4f1615014..45da77e59 100644 --- a/tasks/transform_conformance/src/test_case.rs +++ b/tasks/transform_conformance/src/test_case.rs @@ -88,7 +88,9 @@ pub trait TestCase { let options = self.options(); TransformOptions { target: TransformTarget::ESNext, - react_jsx: Some(ReactJsxOptions::default()), + react_jsx: options + .get_plugin("transform-react-jsx") + .map(get_options::), assumptions: options.assumptions, class_static_block: options.get_plugin("transform-class-static-block").is_some(), logical_assignment_operators: options