Skip to content

Commit

Permalink
feat(transformer/typescript): support rewrite_import_extensions opt…
Browse files Browse the repository at this point in the history
…ion (#5399)

close: #5395

Babel only supports `rewrite`, we also support `remove`
  • Loading branch information
Dunqing committed Sep 3, 2024
1 parent a1523c6 commit 0abfc50
Show file tree
Hide file tree
Showing 12 changed files with 312 additions and 17 deletions.
26 changes: 25 additions & 1 deletion crates/oxc_transformer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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>,
Expand Down
17 changes: 14 additions & 3 deletions crates/oxc_transformer/src/options/transformer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
42 changes: 40 additions & 2 deletions crates/oxc_transformer/src/typescript/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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

annotations: TypeScriptAnnotations<'a>,
r#enum: TypeScriptEnum<'a>,
rewrite_extensions: TypeScriptRewriteExtensions,
}

impl<'a> TypeScript<'a> {
Expand All @@ -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) {
Expand Down
74 changes: 72 additions & 2 deletions crates/oxc_transformer/src/typescript/options.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
89 changes: 89 additions & 0 deletions crates/oxc_transformer/src/typescript/rewrite_extensions.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
15 changes: 13 additions & 2 deletions napi/transform/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Loading

0 comments on commit 0abfc50

Please sign in to comment.