feat(transformer): do not elide jsx imports if a jsx element appears somewhere (#3237)

This commit is contained in:
Dunqing 2024-05-11 15:00:26 +00:00
parent 64cd8a9d69
commit 1b29e63300
5 changed files with 139 additions and 21 deletions

View file

@ -158,6 +158,14 @@ impl<'a> Traverse<'a> for Transformer<'a> {
self.x0_typescript.transform_function(func); 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( fn enter_jsx_opening_element(
&mut self, &mut self,
elem: &mut JSXOpeningElement<'a>, elem: &mut JSXOpeningElement<'a>,

View file

@ -20,18 +20,46 @@ pub struct TypeScriptAnnotations<'a> {
/// Assignments to be added to the constructor body /// Assignments to be added to the constructor body
assignments: Vec<'a, Statement<'a>>, assignments: Vec<'a, Statement<'a>>,
has_super_call: bool, 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> { impl<'a> TypeScriptAnnotations<'a> {
pub fn new(options: &Rc<TypeScriptOptions>, ctx: &Ctx<'a>) -> Self { pub fn new(options: &Rc<TypeScriptOptions>, 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 { Self {
has_super_call: false, has_super_call: false,
assignments: ctx.ast.new_vec(), assignments: ctx.ast.new_vec(),
options: Rc::clone(options), options: Rc::clone(options),
ctx: Rc::clone(ctx), 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` // Creates `this.name = name`
fn create_this_property_assignment(&self, name: &Atom<'a>) -> Statement<'a> { fn create_this_property_assignment(&self, name: &Atom<'a>) -> Statement<'a> {
let ast = &self.ctx.ast; let ast = &self.ctx.ast;
@ -108,6 +136,7 @@ impl<'a> TypeScriptAnnotations<'a> {
} }
references.has_reference(&s.local.name) references.has_reference(&s.local.name)
|| self.is_jsx_imports(&s.local.name)
} }
ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => { ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
if is_type { if is_type {
@ -119,6 +148,7 @@ impl<'a> TypeScriptAnnotations<'a> {
return true; return true;
} }
references.has_reference(&s.local.name) references.has_reference(&s.local.name)
|| self.is_jsx_imports(&s.local.name)
} }
ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => { ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
if is_type { if is_type {
@ -130,6 +160,7 @@ impl<'a> TypeScriptAnnotations<'a> {
} }
references.has_reference(&s.local.name) 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; 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;
}
} }

View file

@ -4,11 +4,10 @@ mod diagnostics;
mod r#enum; mod r#enum;
mod module; mod module;
mod namespace; mod namespace;
mod options;
use std::rc::Rc; use std::rc::Rc;
use serde::Deserialize;
use oxc_allocator::Vec; use oxc_allocator::Vec;
use oxc_ast::ast::*; use oxc_ast::ast::*;
use oxc_traverse::TraverseCtx; use oxc_traverse::TraverseCtx;
@ -20,13 +19,7 @@ use self::{
r#enum::TypeScriptEnum, r#enum::TypeScriptEnum,
}; };
#[derive(Debug, Default, Clone, Deserialize)] pub use self::options::TypeScriptOptions;
#[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,
}
/// [Preset TypeScript](https://babeljs.io/docs/babel-preset-typescript) /// [Preset TypeScript](https://babeljs.io/docs/babel-preset-typescript)
/// ///
@ -61,7 +54,7 @@ pub struct TypeScript<'a> {
impl<'a> TypeScript<'a> { impl<'a> TypeScript<'a> {
pub fn new(options: TypeScriptOptions, ctx: &Ctx<'a>) -> Self { pub fn new(options: TypeScriptOptions, ctx: &Ctx<'a>) -> Self {
let options = Rc::new(options); let options = Rc::new(options.update_with_comments(ctx));
Self { Self {
annotations: TypeScriptAnnotations::new(&options, ctx), annotations: TypeScriptAnnotations::new(&options, ctx),
@ -210,4 +203,12 @@ impl<'a> TypeScript<'a> {
self.transform_ts_export_assignment(ts_export_assignment); 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);
}
} }

View file

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

View file

@ -1,4 +1,4 @@
Passed: 295/362 Passed: 304/362
# All Passed: # All Passed:
* babel-preset-react * babel-preset-react
@ -24,7 +24,7 @@ Passed: 295/362
* opts/optimizeConstEnums/input.ts * opts/optimizeConstEnums/input.ts
* opts/rewriteImportExtensions/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-false/input.ts
* class/accessor-allowDeclareFields-true/input.ts * class/accessor-allowDeclareFields-true/input.ts
* enum/mix-references/input.ts * enum/mix-references/input.ts
@ -32,15 +32,6 @@ Passed: 295/362
* enum/ts5.0-const-foldable/input.ts * enum/ts5.0-const-foldable/input.ts
* exports/declared-types/input.ts * exports/declared-types/input.ts
* exports/export-type-star-from/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/enum-value/input.ts
* imports/type-only-export-specifier-2/input.ts * imports/type-only-export-specifier-2/input.ts
* namespace/ambient-module-nested/input.ts * namespace/ambient-module-nested/input.ts