mirror of
https://github.com/danbulant/oxc
synced 2026-05-25 04:42:10 +00:00
feat(transformer): do not elide jsx imports if a jsx element appears somewhere (#3237)
This commit is contained in:
parent
64cd8a9d69
commit
1b29e63300
5 changed files with 139 additions and 21 deletions
|
|
@ -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>,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
79
crates/oxc_transformer/src/typescript/options.rs
Normal file
79
crates/oxc_transformer/src/typescript/options.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue