Skip to content

Commit 0abfc50

Browse files
committed
feat(transformer/typescript): support rewrite_import_extensions option (#5399)
close: #5395 Babel only supports `rewrite`, we also support `remove`
1 parent a1523c6 commit 0abfc50

File tree

12 files changed

+312
-17
lines changed

12 files changed

+312
-17
lines changed

crates/oxc_transformer/src/lib.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ pub use crate::{
4848
es2015::{ArrowFunctionsOptions, ES2015Options},
4949
options::{BabelOptions, TransformOptions},
5050
react::{ReactJsxRuntime, ReactOptions, ReactRefreshOptions},
51-
typescript::TypeScriptOptions,
51+
typescript::{RewriteExtensionsMode, TypeScriptOptions},
5252
};
5353
use crate::{
5454
context::{Ctx, TransformCtx},
@@ -383,6 +383,30 @@ impl<'a> Traverse<'a> for Transformer<'a> {
383383
self.x3_es2015.enter_variable_declarator(node, ctx);
384384
}
385385

386+
fn enter_import_declaration(
387+
&mut self,
388+
node: &mut ImportDeclaration<'a>,
389+
ctx: &mut TraverseCtx<'a>,
390+
) {
391+
self.x0_typescript.enter_import_declaration(node, ctx);
392+
}
393+
394+
fn enter_export_all_declaration(
395+
&mut self,
396+
node: &mut ExportAllDeclaration<'a>,
397+
ctx: &mut TraverseCtx<'a>,
398+
) {
399+
self.x0_typescript.enter_export_all_declaration(node, ctx);
400+
}
401+
402+
fn enter_export_named_declaration(
403+
&mut self,
404+
node: &mut ExportNamedDeclaration<'a>,
405+
ctx: &mut TraverseCtx<'a>,
406+
) {
407+
self.x0_typescript.enter_export_named_declaration(node, ctx);
408+
}
409+
386410
fn enter_ts_export_assignment(
387411
&mut self,
388412
export_assignment: &mut TSExportAssignment<'a>,

crates/oxc_transformer/src/options/transformer.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,12 +196,23 @@ impl TransformOptions {
196196
});
197197

198198
transformer_options.typescript = {
199-
let plugin_name = "transform-typescript";
200-
from_value::<TypeScriptOptions>(get_plugin_options(plugin_name, options))
199+
let preset_name = "typescript";
200+
if options.has_preset("typescript") {
201+
from_value::<TypeScriptOptions>(
202+
get_preset_options("typescript", options).unwrap_or_else(|| json!({})),
203+
)
201204
.unwrap_or_else(|err| {
202-
report_error(plugin_name, &err, false, &mut errors);
205+
report_error(preset_name, &err, true, &mut errors);
203206
TypeScriptOptions::default()
204207
})
208+
} else {
209+
let plugin_name = "transform-typescript";
210+
from_value::<TypeScriptOptions>(get_plugin_options(plugin_name, options))
211+
.unwrap_or_else(|err| {
212+
report_error(plugin_name, &err, false, &mut errors);
213+
TypeScriptOptions::default()
214+
})
215+
}
205216
};
206217

207218
transformer_options.assumptions = if options.assumptions.is_null() {

crates/oxc_transformer/src/typescript/mod.rs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ mod r#enum;
44
mod module;
55
mod namespace;
66
mod options;
7+
mod rewrite_extensions;
78

89
use std::rc::Rc;
910

1011
use oxc_allocator::Vec;
1112
use oxc_ast::ast::*;
12-
use oxc_traverse::TraverseCtx;
13+
use oxc_traverse::{Traverse, TraverseCtx};
14+
use rewrite_extensions::TypeScriptRewriteExtensions;
1315

14-
pub use self::options::TypeScriptOptions;
16+
pub use self::options::{RewriteExtensionsMode, TypeScriptOptions};
1517
use self::{annotations::TypeScriptAnnotations, r#enum::TypeScriptEnum};
1618
use crate::context::Ctx;
1719

@@ -43,6 +45,7 @@ pub struct TypeScript<'a> {
4345

4446
annotations: TypeScriptAnnotations<'a>,
4547
r#enum: TypeScriptEnum<'a>,
48+
rewrite_extensions: TypeScriptRewriteExtensions,
4649
}
4750

4851
impl<'a> TypeScript<'a> {
@@ -52,12 +55,47 @@ impl<'a> TypeScript<'a> {
5255
Self {
5356
annotations: TypeScriptAnnotations::new(Rc::clone(&options), Rc::clone(&ctx)),
5457
r#enum: TypeScriptEnum::new(Rc::clone(&ctx)),
58+
rewrite_extensions: TypeScriptRewriteExtensions::new(
59+
options.rewrite_import_extensions.clone().unwrap_or_default(),
60+
),
5561
options,
5662
ctx,
5763
}
5864
}
5965
}
6066

67+
impl<'a> Traverse<'a> for TypeScript<'a> {
68+
fn enter_import_declaration(
69+
&mut self,
70+
node: &mut ImportDeclaration<'a>,
71+
ctx: &mut TraverseCtx<'a>,
72+
) {
73+
if self.options.rewrite_import_extensions.is_some() {
74+
self.rewrite_extensions.enter_import_declaration(node, ctx);
75+
}
76+
}
77+
78+
fn enter_export_all_declaration(
79+
&mut self,
80+
node: &mut ExportAllDeclaration<'a>,
81+
ctx: &mut TraverseCtx<'a>,
82+
) {
83+
if self.options.rewrite_import_extensions.is_some() {
84+
self.rewrite_extensions.enter_export_all_declaration(node, ctx);
85+
}
86+
}
87+
88+
fn enter_export_named_declaration(
89+
&mut self,
90+
node: &mut ExportNamedDeclaration<'a>,
91+
ctx: &mut TraverseCtx<'a>,
92+
) {
93+
if self.options.rewrite_import_extensions.is_some() {
94+
self.rewrite_extensions.enter_export_named_declaration(node, ctx);
95+
}
96+
}
97+
}
98+
6199
// Transforms
62100
impl<'a> TypeScript<'a> {
63101
pub fn transform_program(&self, program: &mut Program<'a>, ctx: &mut TraverseCtx) {

crates/oxc_transformer/src/typescript/options.rs

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
use std::borrow::Cow;
1+
use std::{borrow::Cow, fmt};
22

3-
use serde::Deserialize;
3+
use serde::{
4+
de::{self, Visitor},
5+
Deserialize, Deserializer,
6+
};
47

58
use crate::context::TransformCtx;
69

@@ -45,6 +48,16 @@ pub struct TypeScriptOptions {
4548

4649
/// Unused.
4750
pub optimize_const_enums: bool,
51+
52+
// Preset options
53+
/// Modifies extensions in import and export declarations.
54+
///
55+
/// This option, when used together with TypeScript's [`allowImportingTsExtension`](https://www.typescriptlang.org/tsconfig#allowImportingTsExtensions) option,
56+
/// allows to write complete relative specifiers in import declarations while using the same extension used by the source files.
57+
///
58+
/// When set to `true`, same as [`RewriteExtensionsMode::Rewrite`]. Defaults to `false` (do nothing).
59+
#[serde(deserialize_with = "deserialize_rewrite_import_extensions")]
60+
pub rewrite_import_extensions: Option<RewriteExtensionsMode>,
4861
}
4962

5063
impl TypeScriptOptions {
@@ -93,6 +106,63 @@ impl Default for TypeScriptOptions {
93106
allow_namespaces: default_as_true(),
94107
allow_declare_fields: default_as_true(),
95108
optimize_const_enums: false,
109+
rewrite_import_extensions: None,
96110
}
97111
}
98112
}
113+
114+
#[derive(Debug, Clone, Default)]
115+
pub enum RewriteExtensionsMode {
116+
/// Rewrite `.ts`/`.mts`/`.cts` extensions in import/export declarations to `.js`/`.mjs`/`.cjs`.
117+
#[default]
118+
Rewrite,
119+
/// Remove `.ts`/`.mts`/`.cts`/`.tsx` extensions in import/export declarations.
120+
Remove,
121+
}
122+
123+
impl RewriteExtensionsMode {
124+
pub fn is_remove(&self) -> bool {
125+
matches!(self, Self::Remove)
126+
}
127+
}
128+
129+
pub fn deserialize_rewrite_import_extensions<'de, D>(
130+
deserializer: D,
131+
) -> Result<Option<RewriteExtensionsMode>, D::Error>
132+
where
133+
D: Deserializer<'de>,
134+
{
135+
struct RewriteExtensionsModeVisitor;
136+
137+
impl<'de> Visitor<'de> for RewriteExtensionsModeVisitor {
138+
type Value = Option<RewriteExtensionsMode>;
139+
140+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
141+
formatter.write_str("true, false, \"rewrite\", or \"remove\"")
142+
}
143+
144+
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
145+
where
146+
E: de::Error,
147+
{
148+
if value {
149+
Ok(Some(RewriteExtensionsMode::Rewrite))
150+
} else {
151+
Ok(None)
152+
}
153+
}
154+
155+
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
156+
where
157+
E: de::Error,
158+
{
159+
match value {
160+
"rewrite" => Ok(Some(RewriteExtensionsMode::Rewrite)),
161+
"remove" => Ok(Some(RewriteExtensionsMode::Remove)),
162+
_ => Err(E::custom(format!("Expected RewriteExtensionsMode is either \"rewrite\" or \"remove\" but found: {value}"))),
163+
}
164+
}
165+
}
166+
167+
deserializer.deserialize_any(RewriteExtensionsModeVisitor)
168+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//! Rewrite import extensions
2+
//!
3+
//! This plugin is used to rewrite/remove extensions from import/export source.
4+
//! It is only handled source that contains `/` or `\` in the source.
5+
//!
6+
//! 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)
7+
8+
use oxc_ast::ast::{
9+
ExportAllDeclaration, ExportNamedDeclaration, ImportDeclaration, StringLiteral,
10+
};
11+
use oxc_traverse::{Traverse, TraverseCtx};
12+
13+
use super::options::RewriteExtensionsMode;
14+
15+
pub struct TypeScriptRewriteExtensions {
16+
mode: RewriteExtensionsMode,
17+
}
18+
19+
impl TypeScriptRewriteExtensions {
20+
pub fn new(mode: RewriteExtensionsMode) -> Self {
21+
Self { mode }
22+
}
23+
24+
pub fn rewrite_extensions<'a>(
25+
&self,
26+
source: &mut StringLiteral<'a>,
27+
ctx: &mut TraverseCtx<'a>,
28+
) {
29+
let value = source.value.as_str();
30+
if !value.contains(|c| c == '/' || c == '\\') {
31+
return;
32+
}
33+
34+
let Some((_, extension)) = value.rsplit_once('.') else { return };
35+
36+
let replace = match extension {
37+
"mts" => "mjs",
38+
"cts" => "cjs",
39+
"ts" | "tsx" => "js",
40+
_ => return, // do not rewrite or remove other unknown extensions
41+
};
42+
43+
let value = value.trim_end_matches(extension);
44+
source.value = if self.mode.is_remove() {
45+
ctx.ast.atom(value.trim_end_matches('.'))
46+
} else {
47+
let mut value = value.to_string();
48+
value.push_str(replace);
49+
ctx.ast.atom(&value)
50+
};
51+
}
52+
}
53+
54+
impl<'a> Traverse<'a> for TypeScriptRewriteExtensions {
55+
fn enter_import_declaration(
56+
&mut self,
57+
node: &mut ImportDeclaration<'a>,
58+
ctx: &mut TraverseCtx<'a>,
59+
) {
60+
if node.import_kind.is_type() {
61+
return;
62+
}
63+
self.rewrite_extensions(&mut node.source, ctx);
64+
}
65+
66+
fn enter_export_named_declaration(
67+
&mut self,
68+
node: &mut ExportNamedDeclaration<'a>,
69+
ctx: &mut TraverseCtx<'a>,
70+
) {
71+
if node.export_kind.is_type() {
72+
return;
73+
}
74+
if let Some(source) = node.source.as_mut() {
75+
self.rewrite_extensions(source, ctx);
76+
}
77+
}
78+
79+
fn enter_export_all_declaration(
80+
&mut self,
81+
node: &mut ExportAllDeclaration<'a>,
82+
ctx: &mut TraverseCtx<'a>,
83+
) {
84+
if node.export_kind.is_type() {
85+
return;
86+
}
87+
self.rewrite_extensions(&mut node.source, ctx);
88+
}
89+
}

napi/transform/index.d.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface Es2015BindingOptions {
1818
}
1919

2020
/** TypeScript Isolated Declarations for Standalone DTS Emit */
21-
function isolatedDeclaration(filename: string, sourceText: string, options: IsolatedDeclarationsOptions): IsolatedDeclarationsResult
21+
export declare function isolatedDeclaration(filename: string, sourceText: string, options: IsolatedDeclarationsOptions): IsolatedDeclarationsResult
2222

2323
export interface IsolatedDeclarationsOptions {
2424
sourcemap: boolean
@@ -136,7 +136,7 @@ export interface SourceMap {
136136
* @returns an object containing the transformed code, source maps, and any
137137
* errors that occurred during parsing or transformation.
138138
*/
139-
function transform(filename: string, sourceText: string, options?: TransformOptions | undefined | null): TransformResult
139+
export declare function transform(filename: string, sourceText: string, options?: TransformOptions | undefined | null): TransformResult
140140

141141
/**
142142
* Options for transforming a JavaScript or TypeScript file.
@@ -230,5 +230,16 @@ export interface TypeScriptBindingOptions {
230230
* @default false
231231
*/
232232
declaration?: boolean
233+
/**
234+
* Rewrite or remove TypeScript import/export declaration extensions.
235+
*
236+
* - When set to `rewrite`, it will change `.ts`, `.mts`, `.cts` extensions to `.js`, `.mjs`, `.cjs` respectively.
237+
* - When set to `remove`, it will remove the extensions entirely.
238+
* - When set to `true`, it's equivalent to `rewrite`.
239+
* - When set to `false` or omitted, no changes will be made to the extensions.
240+
*
241+
* @default false
242+
*/
243+
rewriteImportExtensions?: 'rewrite' | 'remove' | boolean
233244
}
234245

0 commit comments

Comments
 (0)