feat(transformer/typescript): support rewrite_import_extensions option (#5399)

close: #5395

Babel only supports `rewrite`, we also support `remove`
This commit is contained in:
Dunqing 2024-09-03 01:57:42 +00:00
parent a1523c6cc8
commit 0abfc5049f
12 changed files with 312 additions and 17 deletions

View file

@ -48,7 +48,7 @@ pub use crate::{
es2015::{ArrowFunctionsOptions, ES2015Options},
options::{BabelOptions, TransformOptions},
react::{ReactJsxRuntime, ReactOptions, ReactRefreshOptions},
typescript::TypeScriptOptions,
typescript::{RewriteExtensionsMode, TypeScriptOptions},
};
use crate::{
context::{Ctx, TransformCtx},
@ -383,6 +383,30 @@ impl<'a> Traverse<'a> for Transformer<'a> {
self.x3_es2015.enter_variable_declarator(node, ctx);
}
fn enter_import_declaration(
&mut self,
node: &mut ImportDeclaration<'a>,
ctx: &mut TraverseCtx<'a>,
) {
self.x0_typescript.enter_import_declaration(node, ctx);
}
fn enter_export_all_declaration(
&mut self,
node: &mut ExportAllDeclaration<'a>,
ctx: &mut TraverseCtx<'a>,
) {
self.x0_typescript.enter_export_all_declaration(node, ctx);
}
fn enter_export_named_declaration(
&mut self,
node: &mut ExportNamedDeclaration<'a>,
ctx: &mut TraverseCtx<'a>,
) {
self.x0_typescript.enter_export_named_declaration(node, ctx);
}
fn enter_ts_export_assignment(
&mut self,
export_assignment: &mut TSExportAssignment<'a>,

View file

@ -196,12 +196,23 @@ impl TransformOptions {
});
transformer_options.typescript = {
let plugin_name = "transform-typescript";
from_value::<TypeScriptOptions>(get_plugin_options(plugin_name, options))
let preset_name = "typescript";
if options.has_preset("typescript") {
from_value::<TypeScriptOptions>(
get_preset_options("typescript", options).unwrap_or_else(|| json!({})),
)
.unwrap_or_else(|err| {
report_error(plugin_name, &err, false, &mut errors);
report_error(preset_name, &err, true, &mut errors);
TypeScriptOptions::default()
})
} else {
let plugin_name = "transform-typescript";
from_value::<TypeScriptOptions>(get_plugin_options(plugin_name, options))
.unwrap_or_else(|err| {
report_error(plugin_name, &err, false, &mut errors);
TypeScriptOptions::default()
})
}
};
transformer_options.assumptions = if options.assumptions.is_null() {

View file

@ -4,14 +4,16 @@ mod r#enum;
mod module;
mod namespace;
mod options;
mod rewrite_extensions;
use std::rc::Rc;
use oxc_allocator::Vec;
use oxc_ast::ast::*;
use oxc_traverse::TraverseCtx;
use oxc_traverse::{Traverse, TraverseCtx};
use rewrite_extensions::TypeScriptRewriteExtensions;
pub use self::options::TypeScriptOptions;
pub use self::options::{RewriteExtensionsMode, TypeScriptOptions};
use self::{annotations::TypeScriptAnnotations, r#enum::TypeScriptEnum};
use crate::context::Ctx;
@ -43,6 +45,7 @@ pub struct TypeScript<'a> {
annotations: TypeScriptAnnotations<'a>,
r#enum: TypeScriptEnum<'a>,
rewrite_extensions: TypeScriptRewriteExtensions,
}
impl<'a> TypeScript<'a> {
@ -52,12 +55,47 @@ impl<'a> TypeScript<'a> {
Self {
annotations: TypeScriptAnnotations::new(Rc::clone(&options), Rc::clone(&ctx)),
r#enum: TypeScriptEnum::new(Rc::clone(&ctx)),
rewrite_extensions: TypeScriptRewriteExtensions::new(
options.rewrite_import_extensions.clone().unwrap_or_default(),
),
options,
ctx,
}
}
}
impl<'a> Traverse<'a> for TypeScript<'a> {
fn enter_import_declaration(
&mut self,
node: &mut ImportDeclaration<'a>,
ctx: &mut TraverseCtx<'a>,
) {
if self.options.rewrite_import_extensions.is_some() {
self.rewrite_extensions.enter_import_declaration(node, ctx);
}
}
fn enter_export_all_declaration(
&mut self,
node: &mut ExportAllDeclaration<'a>,
ctx: &mut TraverseCtx<'a>,
) {
if self.options.rewrite_import_extensions.is_some() {
self.rewrite_extensions.enter_export_all_declaration(node, ctx);
}
}
fn enter_export_named_declaration(
&mut self,
node: &mut ExportNamedDeclaration<'a>,
ctx: &mut TraverseCtx<'a>,
) {
if self.options.rewrite_import_extensions.is_some() {
self.rewrite_extensions.enter_export_named_declaration(node, ctx);
}
}
}
// Transforms
impl<'a> TypeScript<'a> {
pub fn transform_program(&self, program: &mut Program<'a>, ctx: &mut TraverseCtx) {

View file

@ -1,6 +1,9 @@
use std::borrow::Cow;
use std::{borrow::Cow, fmt};
use serde::Deserialize;
use serde::{
de::{self, Visitor},
Deserialize, Deserializer,
};
use crate::context::TransformCtx;
@ -45,6 +48,16 @@ pub struct TypeScriptOptions {
/// Unused.
pub optimize_const_enums: bool,
// Preset options
/// Modifies extensions in import and export declarations.
///
/// This option, when used together with TypeScript's [`allowImportingTsExtension`](https://www.typescriptlang.org/tsconfig#allowImportingTsExtensions) option,
/// allows to write complete relative specifiers in import declarations while using the same extension used by the source files.
///
/// When set to `true`, same as [`RewriteExtensionsMode::Rewrite`]. Defaults to `false` (do nothing).
#[serde(deserialize_with = "deserialize_rewrite_import_extensions")]
pub rewrite_import_extensions: Option<RewriteExtensionsMode>,
}
impl TypeScriptOptions {
@ -93,6 +106,63 @@ impl Default for TypeScriptOptions {
allow_namespaces: default_as_true(),
allow_declare_fields: default_as_true(),
optimize_const_enums: false,
rewrite_import_extensions: None,
}
}
}
#[derive(Debug, Clone, Default)]
pub enum RewriteExtensionsMode {
/// Rewrite `.ts`/`.mts`/`.cts` extensions in import/export declarations to `.js`/`.mjs`/`.cjs`.
#[default]
Rewrite,
/// Remove `.ts`/`.mts`/`.cts`/`.tsx` extensions in import/export declarations.
Remove,
}
impl RewriteExtensionsMode {
pub fn is_remove(&self) -> bool {
matches!(self, Self::Remove)
}
}
pub fn deserialize_rewrite_import_extensions<'de, D>(
deserializer: D,
) -> Result<Option<RewriteExtensionsMode>, D::Error>
where
D: Deserializer<'de>,
{
struct RewriteExtensionsModeVisitor;
impl<'de> Visitor<'de> for RewriteExtensionsModeVisitor {
type Value = Option<RewriteExtensionsMode>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("true, false, \"rewrite\", or \"remove\"")
}
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
where
E: de::Error,
{
if value {
Ok(Some(RewriteExtensionsMode::Rewrite))
} else {
Ok(None)
}
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
match value {
"rewrite" => Ok(Some(RewriteExtensionsMode::Rewrite)),
"remove" => Ok(Some(RewriteExtensionsMode::Remove)),
_ => Err(E::custom(format!("Expected RewriteExtensionsMode is either \"rewrite\" or \"remove\" but found: {value}"))),
}
}
}
deserializer.deserialize_any(RewriteExtensionsModeVisitor)
}

View file

@ -0,0 +1,89 @@
//! Rewrite import extensions
//!
//! This plugin is used to rewrite/remove extensions from import/export source.
//! It is only handled source that contains `/` or `\` in the source.
//!
//! Based on Babel's [plugin-rewrite-ts-imports](https://github.com/babel/babel/blob/3bcfee232506a4cebe410f02042fb0f0adeeb0b1/packages/babel-preset-typescript/src/plugin-rewrite-ts-imports.ts)
use oxc_ast::ast::{
ExportAllDeclaration, ExportNamedDeclaration, ImportDeclaration, StringLiteral,
};
use oxc_traverse::{Traverse, TraverseCtx};
use super::options::RewriteExtensionsMode;
pub struct TypeScriptRewriteExtensions {
mode: RewriteExtensionsMode,
}
impl TypeScriptRewriteExtensions {
pub fn new(mode: RewriteExtensionsMode) -> Self {
Self { mode }
}
pub fn rewrite_extensions<'a>(
&self,
source: &mut StringLiteral<'a>,
ctx: &mut TraverseCtx<'a>,
) {
let value = source.value.as_str();
if !value.contains(|c| c == '/' || c == '\\') {
return;
}
let Some((_, extension)) = value.rsplit_once('.') else { return };
let replace = match extension {
"mts" => "mjs",
"cts" => "cjs",
"ts" | "tsx" => "js",
_ => return, // do not rewrite or remove other unknown extensions
};
let value = value.trim_end_matches(extension);
source.value = if self.mode.is_remove() {
ctx.ast.atom(value.trim_end_matches('.'))
} else {
let mut value = value.to_string();
value.push_str(replace);
ctx.ast.atom(&value)
};
}
}
impl<'a> Traverse<'a> for TypeScriptRewriteExtensions {
fn enter_import_declaration(
&mut self,
node: &mut ImportDeclaration<'a>,
ctx: &mut TraverseCtx<'a>,
) {
if node.import_kind.is_type() {
return;
}
self.rewrite_extensions(&mut node.source, ctx);
}
fn enter_export_named_declaration(
&mut self,
node: &mut ExportNamedDeclaration<'a>,
ctx: &mut TraverseCtx<'a>,
) {
if node.export_kind.is_type() {
return;
}
if let Some(source) = node.source.as_mut() {
self.rewrite_extensions(source, ctx);
}
}
fn enter_export_all_declaration(
&mut self,
node: &mut ExportAllDeclaration<'a>,
ctx: &mut TraverseCtx<'a>,
) {
if node.export_kind.is_type() {
return;
}
self.rewrite_extensions(&mut node.source, ctx);
}
}

View file

@ -18,7 +18,7 @@ export interface Es2015BindingOptions {
}
/** TypeScript Isolated Declarations for Standalone DTS Emit */
function isolatedDeclaration(filename: string, sourceText: string, options: IsolatedDeclarationsOptions): IsolatedDeclarationsResult
export declare function isolatedDeclaration(filename: string, sourceText: string, options: IsolatedDeclarationsOptions): IsolatedDeclarationsResult
export interface IsolatedDeclarationsOptions {
sourcemap: boolean
@ -136,7 +136,7 @@ export interface SourceMap {
* @returns an object containing the transformed code, source maps, and any
* errors that occurred during parsing or transformation.
*/
function transform(filename: string, sourceText: string, options?: TransformOptions | undefined | null): TransformResult
export declare function transform(filename: string, sourceText: string, options?: TransformOptions | undefined | null): TransformResult
/**
* Options for transforming a JavaScript or TypeScript file.
@ -230,5 +230,16 @@ export interface TypeScriptBindingOptions {
* @default false
*/
declaration?: boolean
/**
* Rewrite or remove TypeScript import/export declaration extensions.
*
* - When set to `rewrite`, it will change `.ts`, `.mts`, `.cts` extensions to `.js`, `.mjs`, `.cjs` respectively.
* - When set to `remove`, it will remove the extensions entirely.
* - When set to `true`, it's equivalent to `rewrite`.
* - When set to `false` or omitted, no changes will be made to the extensions.
*
* @default false
*/
rewriteImportExtensions?: 'rewrite' | 'remove' | boolean
}

View file

@ -1,9 +1,11 @@
use std::path::PathBuf;
use napi::Either;
use napi_derive::napi;
use oxc_transformer::{
ArrowFunctionsOptions, ES2015Options, ReactJsxRuntime, ReactOptions, TypeScriptOptions,
ArrowFunctionsOptions, ES2015Options, ReactJsxRuntime, ReactOptions, RewriteExtensionsMode,
TypeScriptOptions,
};
#[napi(object)]
@ -22,6 +24,16 @@ pub struct TypeScriptBindingOptions {
///
/// @default false
pub declaration: Option<bool>,
/// Rewrite or remove TypeScript import/export declaration extensions.
///
/// - When set to `rewrite`, it will change `.ts`, `.mts`, `.cts` extensions to `.js`, `.mjs`, `.cjs` respectively.
/// - When set to `remove`, it will remove `.ts`/`.mts`/`.cts`/`.tsx` extension entirely.
/// - When set to `true`, it's equivalent to `rewrite`.
/// - When set to `false` or omitted, no changes will be made to the extensions.
///
/// @default false
#[napi(ts_type = "'rewrite' | 'remove' | boolean")]
pub rewrite_import_extensions: Option<Either<bool, String>>,
}
impl From<TypeScriptBindingOptions> for TypeScriptOptions {
@ -36,6 +48,22 @@ impl From<TypeScriptBindingOptions> for TypeScriptOptions {
allow_namespaces: options.allow_namespaces.unwrap_or(ops.allow_namespaces),
allow_declare_fields: options.allow_declare_fields.unwrap_or(ops.allow_declare_fields),
optimize_const_enums: false,
rewrite_import_extensions: options.rewrite_import_extensions.and_then(|value| {
match value {
Either::A(v) => {
if v {
Some(RewriteExtensionsMode::Rewrite)
} else {
None
}
}
Either::B(v) => match v.as_str() {
"rewrite" => Some(RewriteExtensionsMode::Rewrite),
"remove" => Some(RewriteExtensionsMode::Remove),
_ => None,
},
}
}),
}
}
}

View file

@ -1,6 +1,6 @@
commit: 3bcfee23
Passed: 309/1021
Passed: 310/1021
# All Passed:
* babel-plugin-transform-optional-catch-binding
@ -2101,7 +2101,7 @@ failed to resolve query: failed to parse the rest of input: ...''
# babel-preset-typescript (4/10)
# babel-preset-typescript (5/10)
* jsx-compat/ts-invalid/input.ts
x Expected `>` but found `/`
,-[tasks/coverage/babel/packages/babel-preset-typescript/test/fixtures/jsx-compat/ts-invalid/input.ts:1:7]
@ -2141,9 +2141,6 @@ failed to resolve query: failed to parse the rest of input: ...''
| rebuilt : SymbolId(0): SymbolFlags(FunctionScopedVariable)
* opts/rewriteImportExtensions/input.ts
# babel-plugin-transform-typescript (41/152)
* cast/as-expression/input.ts

View file

@ -1,9 +1,10 @@
commit: 3bcfee23
Passed: 10/39
Passed: 11/40
# All Passed:
* babel-plugin-transform-optional-catch-binding
* babel-preset-typescript
# babel-plugin-transform-nullish-coalescing-operator (0/1)

View file

@ -0,0 +1,11 @@
import "./a.ts";
import "./a.mts";
import "./a.cts";
import "./react.tsx";
// .mtsx and .ctsx are not valid and should not be transformed.
import "./react.mtsx";
import "./react.ctsx";
import "a-package/file.ts";
// Bare import, it's either a node package or remapped by an import map
import "soundcloud.ts";
import "ipaddr.js";

View file

@ -0,0 +1,4 @@
{
"sourceType": "module",
"presets": [["typescript", { "rewriteImportExtensions": "remove" }]]
}

View file

@ -0,0 +1,11 @@
import "./a";
import "./a";
import "./a";
import "./react";
// .mtsx and .ctsx are not valid and should not be transformed.
import "./react.mtsx";
import "./react.ctsx";
import "a-package/file";
// Bare import, it's either a node package or remapped by an import map
import "soundcloud.ts";
import "ipaddr.js";