mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
feat(transformer/typescript): support rewrite_import_extensions option (#5399)
close: #5395 Babel only supports `rewrite`, we also support `remove`
This commit is contained in:
parent
a1523c6cc8
commit
0abfc5049f
12 changed files with 312 additions and 17 deletions
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
89
crates/oxc_transformer/src/typescript/rewrite_extensions.rs
Normal file
89
crates/oxc_transformer/src/typescript/rewrite_extensions.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
15
napi/transform/index.d.ts
vendored
15
napi/transform/index.d.ts
vendored
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"sourceType": "module",
|
||||
"presets": [["typescript", { "rewriteImportExtensions": "remove" }]]
|
||||
}
|
||||
|
|
@ -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";
|
||||
Loading…
Reference in a new issue