From 1b29e63300ab54e643b3ed9b42f7994a3e0317a6 Mon Sep 17 00:00:00 2001 From: Dunqing <29533304+Dunqing@users.noreply.github.com> Date: Sat, 11 May 2024 15:00:26 +0000 Subject: [PATCH] feat(transformer): do not elide jsx imports if a jsx element appears somewhere (#3237) --- crates/oxc_transformer/src/lib.rs | 8 ++ .../src/typescript/annotations.rs | 39 +++++++++ crates/oxc_transformer/src/typescript/mod.rs | 21 ++--- .../oxc_transformer/src/typescript/options.rs | 79 +++++++++++++++++++ tasks/transform_conformance/babel.snap.md | 13 +-- 5 files changed, 139 insertions(+), 21 deletions(-) create mode 100644 crates/oxc_transformer/src/typescript/options.rs diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index a28de7d6e..14bdd8e4c 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -158,6 +158,14 @@ impl<'a> Traverse<'a> for Transformer<'a> { self.x0_typescript.transform_function(func); } + fn enter_jsx_element(&mut self, node: &mut JSXElement<'a>, _ctx: &TraverseCtx<'a>) { + self.x0_typescript.transform_jsx_element(node); + } + + fn enter_jsx_fragment(&mut self, node: &mut JSXFragment<'a>, _ctx: &TraverseCtx<'a>) { + self.x0_typescript.transform_jsx_fragment(node); + } + fn enter_jsx_opening_element( &mut self, elem: &mut JSXOpeningElement<'a>, diff --git a/crates/oxc_transformer/src/typescript/annotations.rs b/crates/oxc_transformer/src/typescript/annotations.rs index 0464a9bd4..c82db5b21 100644 --- a/crates/oxc_transformer/src/typescript/annotations.rs +++ b/crates/oxc_transformer/src/typescript/annotations.rs @@ -20,18 +20,46 @@ pub struct TypeScriptAnnotations<'a> { /// Assignments to be added to the constructor body assignments: Vec<'a, Statement<'a>>, has_super_call: bool, + + has_jsx_element: bool, + has_jsx_fragment: bool, + jsx_element_import_name: String, + jsx_fragment_import_name: String, } impl<'a> TypeScriptAnnotations<'a> { pub fn new(options: &Rc, ctx: &Ctx<'a>) -> Self { + let jsx_element_import_name = if options.jsx_pragma.contains('.') { + options.jsx_pragma.split('.').next().map(String::from).unwrap() + } else { + options.jsx_pragma.to_string() + }; + + let jsx_fragment_import_name = if options.jsx_pragma_frag.contains('.') { + options.jsx_pragma_frag.split('.').next().map(String::from).unwrap() + } else { + options.jsx_pragma_frag.to_string() + }; + Self { has_super_call: false, assignments: ctx.ast.new_vec(), options: Rc::clone(options), ctx: Rc::clone(ctx), + has_jsx_element: false, + has_jsx_fragment: false, + jsx_element_import_name, + jsx_fragment_import_name, } } + /// Check if the given name is a JSX pragma or fragment pragma import + /// and if the file contains JSX elements or fragments + fn is_jsx_imports(&self, name: &str) -> bool { + self.has_jsx_element && name == self.jsx_element_import_name + || self.has_jsx_fragment && name == self.jsx_fragment_import_name + } + // Creates `this.name = name` fn create_this_property_assignment(&self, name: &Atom<'a>) -> Statement<'a> { let ast = &self.ctx.ast; @@ -108,6 +136,7 @@ impl<'a> TypeScriptAnnotations<'a> { } references.has_reference(&s.local.name) + || self.is_jsx_imports(&s.local.name) } ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => { if is_type { @@ -119,6 +148,7 @@ impl<'a> TypeScriptAnnotations<'a> { return true; } references.has_reference(&s.local.name) + || self.is_jsx_imports(&s.local.name) } ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => { if is_type { @@ -130,6 +160,7 @@ impl<'a> TypeScriptAnnotations<'a> { } references.has_reference(&s.local.name) + || self.is_jsx_imports(&s.local.name) } }); } @@ -357,4 +388,12 @@ impl<'a> TypeScriptAnnotations<'a> { ) { expr.type_parameters = None; } + + pub fn transform_jsx_element(&mut self, _elem: &mut JSXElement<'a>) { + self.has_jsx_element = true; + } + + pub fn transform_jsx_fragment(&mut self, _elem: &mut JSXFragment<'a>) { + self.has_jsx_fragment = true; + } } diff --git a/crates/oxc_transformer/src/typescript/mod.rs b/crates/oxc_transformer/src/typescript/mod.rs index f69c862cb..776f35ac5 100644 --- a/crates/oxc_transformer/src/typescript/mod.rs +++ b/crates/oxc_transformer/src/typescript/mod.rs @@ -4,11 +4,10 @@ mod diagnostics; mod r#enum; mod module; mod namespace; +mod options; use std::rc::Rc; -use serde::Deserialize; - use oxc_allocator::Vec; use oxc_ast::ast::*; use oxc_traverse::TraverseCtx; @@ -20,13 +19,7 @@ use self::{ r#enum::TypeScriptEnum, }; -#[derive(Debug, Default, Clone, Deserialize)] -#[serde(default, rename_all = "camelCase")] -pub struct TypeScriptOptions { - /// When set to true, the transform will only remove type-only imports (introduced in TypeScript 3.8). - /// This should only be used if you are using TypeScript >= 3.8. - only_remove_type_imports: bool, -} +pub use self::options::TypeScriptOptions; /// [Preset TypeScript](https://babeljs.io/docs/babel-preset-typescript) /// @@ -61,7 +54,7 @@ pub struct TypeScript<'a> { impl<'a> TypeScript<'a> { pub fn new(options: TypeScriptOptions, ctx: &Ctx<'a>) -> Self { - let options = Rc::new(options); + let options = Rc::new(options.update_with_comments(ctx)); Self { annotations: TypeScriptAnnotations::new(&options, ctx), @@ -210,4 +203,12 @@ impl<'a> TypeScript<'a> { self.transform_ts_export_assignment(ts_export_assignment); } } + + pub fn transform_jsx_element(&mut self, elem: &mut JSXElement<'a>) { + self.annotations.transform_jsx_element(elem); + } + + pub fn transform_jsx_fragment(&mut self, elem: &mut JSXFragment<'a>) { + self.annotations.transform_jsx_fragment(elem); + } } diff --git a/crates/oxc_transformer/src/typescript/options.rs b/crates/oxc_transformer/src/typescript/options.rs new file mode 100644 index 000000000..6b1960b1c --- /dev/null +++ b/crates/oxc_transformer/src/typescript/options.rs @@ -0,0 +1,79 @@ +use std::borrow::Cow; + +use serde::Deserialize; + +use crate::context::Ctx; + +fn default_for_jsx_pragma() -> Cow<'static, str> { + Cow::Borrowed("React.createElement") +} + +fn default_for_jsx_pragma_frag() -> Cow<'static, str> { + Cow::Borrowed("React.Fragment") +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct TypeScriptOptions { + /// Replace the function used when compiling JSX expressions. + /// This is so that we know that the import is not a type import, and should not be removed. + /// defaults to React + #[serde(default = "default_for_jsx_pragma")] + pub jsx_pragma: Cow<'static, str>, + + /// Replace the function used when compiling JSX fragment expressions. + /// This is so that we know that the import is not a type import, and should not be removed. + /// defaults to React.Fragment + #[serde(default = "default_for_jsx_pragma_frag")] + pub jsx_pragma_frag: Cow<'static, str>, + /// When set to true, the transform will only remove type-only imports (introduced in TypeScript 3.8). + /// This should only be used if you are using TypeScript >= 3.8. + pub only_remove_type_imports: bool, +} + +impl TypeScriptOptions { + /// Scan through all comments and find the following pragmas + /// + /// * @jsx React.createElement + /// * @jsxFrag React.Fragment + /// + /// The comment does not need to be a jsdoc, + /// otherwise `JSDoc` could be used instead. + /// + /// This behavior is aligned with babel. + pub(crate) fn update_with_comments(mut self, ctx: &Ctx) -> Self { + for (_, span) in ctx.trivias.comments() { + let mut comment = span.source_text(ctx.source_text).trim_start(); + // strip leading jsdoc comment `*` and then whitespaces + while let Some(cur_comment) = comment.strip_prefix('*') { + comment = cur_comment.trim_start(); + } + // strip leading `@` + let Some(comment) = comment.strip_prefix('@') else { continue }; + + // read jsxFrag + if let Some(pragma_frag) = comment.strip_prefix("jsxFrag").map(str::trim) { + self.jsx_pragma_frag = Cow::from(pragma_frag.to_string()); + continue; + } + + // Put this condition at the end to avoid breaking @jsxXX + // read jsx + if let Some(pragma) = comment.strip_prefix("jsx").map(str::trim) { + self.jsx_pragma = Cow::from(pragma.to_string()); + } + } + + self + } +} + +impl Default for TypeScriptOptions { + fn default() -> Self { + Self { + jsx_pragma: default_for_jsx_pragma(), + jsx_pragma_frag: default_for_jsx_pragma_frag(), + only_remove_type_imports: false, + } + } +} diff --git a/tasks/transform_conformance/babel.snap.md b/tasks/transform_conformance/babel.snap.md index c136fe57b..1d368d2a8 100644 --- a/tasks/transform_conformance/babel.snap.md +++ b/tasks/transform_conformance/babel.snap.md @@ -1,4 +1,4 @@ -Passed: 295/362 +Passed: 304/362 # All Passed: * babel-preset-react @@ -24,7 +24,7 @@ Passed: 295/362 * opts/optimizeConstEnums/input.ts * opts/rewriteImportExtensions/input.ts -# babel-plugin-transform-typescript (110/156) +# babel-plugin-transform-typescript (119/156) * class/accessor-allowDeclareFields-false/input.ts * class/accessor-allowDeclareFields-true/input.ts * enum/mix-references/input.ts @@ -32,15 +32,6 @@ Passed: 295/362 * enum/ts5.0-const-foldable/input.ts * exports/declared-types/input.ts * exports/export-type-star-from/input.ts -* imports/elide-jsx-pragma-namespace-no/input.ts -* imports/elide-jsx-pragma-no/input.ts -* imports/elide-jsx-pragmaFrag-namespace-no/input.ts -* imports/elide-jsx-pragmaFrag-no/input.ts -* imports/elide-preact-no-1/input.ts -* imports/elide-preact-no-2/input.ts -* imports/elide-react-no-1/input.ts -* imports/elide-react-no-2/input.ts -* imports/elide-react-no-3/input.ts * imports/enum-value/input.ts * imports/type-only-export-specifier-2/input.ts * namespace/ambient-module-nested/input.ts