diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index b91874d2dac36..4c052cef99313 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -704,6 +704,16 @@ impl<'a> Traverse<'a, TransformState<'a>> for TransformerImpl<'a> { } } + fn enter_import_expression( + &mut self, + node: &mut ImportExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(typescript) = self.x0_typescript.as_mut() { + typescript.enter_import_expression(node, ctx); + } + } + fn enter_export_all_declaration( &mut self, node: &mut ExportAllDeclaration<'a>, diff --git a/crates/oxc_transformer/src/typescript/mod.rs b/crates/oxc_transformer/src/typescript/mod.rs index 205576d52bde2..5785bb5e5d907 100644 --- a/crates/oxc_transformer/src/typescript/mod.rs +++ b/crates/oxc_transformer/src/typescript/mod.rs @@ -313,6 +313,16 @@ impl<'a> Traverse<'a, TransformState<'a>> for TypeScript<'a> { } } + fn enter_import_expression( + &mut self, + node: &mut ImportExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(rewrite_extensions) = &mut self.rewrite_extensions { + rewrite_extensions.enter_import_expression(node, ctx); + } + } + fn enter_formal_parameter_rest( &mut self, node: &mut FormalParameterRest<'a>, diff --git a/crates/oxc_transformer/src/typescript/rewrite_extensions.rs b/crates/oxc_transformer/src/typescript/rewrite_extensions.rs index 6544ad8fa1767..e923a0be4c4b3 100644 --- a/crates/oxc_transformer/src/typescript/rewrite_extensions.rs +++ b/crates/oxc_transformer/src/typescript/rewrite_extensions.rs @@ -6,7 +6,8 @@ //! 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, + ExportAllDeclaration, ExportNamedDeclaration, Expression, ImportDeclaration, ImportExpression, + StringLiteral, TemplateLiteral, }; use oxc_span::Atom; use oxc_traverse::Traverse; @@ -19,32 +20,62 @@ pub struct TypeScriptRewriteExtensions { mode: RewriteExtensionsMode, } +/// Given a specifier value, compute the replacement atom if the extension +/// should be rewritten/removed. Returns `None` when no rewriting is needed. +fn rewritten_specifier<'a>( + value: &'a str, + mode: RewriteExtensionsMode, + ctx: &TraverseCtx<'a>, +) -> Option> { + if !value.contains(['/', '\\']) { + return None; + } + + let (without_extension, extension) = value.rsplit_once('.')?; + + let replace = match extension { + "mts" => ".mjs", + "cts" => ".cjs", + "ts" | "tsx" => ".js", + _ => return None, + }; + + Some(if mode.is_remove() { + Atom::from(without_extension) + } else { + ctx.ast.atom_from_strs_array([without_extension, replace]) + }) +} + impl TypeScriptRewriteExtensions { pub fn new(options: &TypeScriptOptions) -> Option { options.rewrite_import_extensions.map(|mode| Self { mode }) } pub fn rewrite_extensions<'a>(&self, source: &mut StringLiteral<'a>, ctx: &TraverseCtx<'a>) { - let value = source.value.as_str(); - if !value.contains(['/', '\\']) { - return; + if let Some(rewritten) = rewritten_specifier(source.value.as_str(), self.mode, ctx) { + source.value = rewritten; + source.raw = None; } + } - let Some((without_extension, 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 - }; - - source.value = if self.mode.is_remove() { - Atom::from(without_extension) - } else { - ctx.ast.atom_from_strs_array([without_extension, replace]) - }; - source.raw = None; + fn rewrite_template_literal<'a>( + &self, + template: &mut TemplateLiteral<'a>, + ctx: &TraverseCtx<'a>, + ) { + if !template.is_no_substitution_template() { + return; + } + let quasi = &mut template.quasis[0]; + // Read the specifier value from raw (always present). + // For no-substitution templates, raw and cooked are identical + // unless the template contains escape sequences, which import + // specifiers never do. + if let Some(rewritten) = rewritten_specifier(quasi.value.raw.as_str(), self.mode, ctx) { + quasi.value.raw = rewritten; + quasi.value.cooked = Some(rewritten); + } } } @@ -83,4 +114,20 @@ impl<'a> Traverse<'a, TransformState<'a>> for TypeScriptRewriteExtensions { } self.rewrite_extensions(&mut node.source, ctx); } + + fn enter_import_expression( + &mut self, + node: &mut ImportExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + match &mut node.source { + Expression::StringLiteral(source) => { + self.rewrite_extensions(source, ctx); + } + Expression::TemplateLiteral(template) => { + self.rewrite_template_literal(template, ctx); + } + _ => {} + } + } } diff --git a/tasks/transform_conformance/snapshots/oxc.snap.md b/tasks/transform_conformance/snapshots/oxc.snap.md index 2fe422b2d1b9d..0ba6144611a94 100644 --- a/tasks/transform_conformance/snapshots/oxc.snap.md +++ b/tasks/transform_conformance/snapshots/oxc.snap.md @@ -1,6 +1,6 @@ commit: de54b9b2 -Passed: 203/334 +Passed: 204/335 # All Passed: * babel-plugin-transform-class-static-block diff --git a/tasks/transform_conformance/tests/babel-preset-typescript/test/fixtures/removeImportExtensions/input.ts b/tasks/transform_conformance/tests/babel-preset-typescript/test/fixtures/removeImportExtensions/input.ts index b3589f2e8d283..74c6c9ba256d0 100644 --- a/tasks/transform_conformance/tests/babel-preset-typescript/test/fixtures/removeImportExtensions/input.ts +++ b/tasks/transform_conformance/tests/babel-preset-typescript/test/fixtures/removeImportExtensions/input.ts @@ -9,3 +9,15 @@ 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"; +// Dynamic imports should also be rewritten. +import("./a.ts"); +import("./a.mts"); +import("./a.cts"); +import("./react.tsx"); +import("a-package/file.ts"); +// Bare dynamic import should not be rewritten. +import("soundcloud.ts"); +// No-substitution template literal should also be rewritten. +import(`./a.ts`); +// Non-string-literal dynamic import should not be rewritten. +import(dynamicPath); diff --git a/tasks/transform_conformance/tests/babel-preset-typescript/test/fixtures/removeImportExtensions/output.js b/tasks/transform_conformance/tests/babel-preset-typescript/test/fixtures/removeImportExtensions/output.js index 0e2d8f559d2a0..9bb819e0effdc 100644 --- a/tasks/transform_conformance/tests/babel-preset-typescript/test/fixtures/removeImportExtensions/output.js +++ b/tasks/transform_conformance/tests/babel-preset-typescript/test/fixtures/removeImportExtensions/output.js @@ -9,3 +9,15 @@ import "a-package/file"; // Bare import, it's either a node package or remapped by an import map import "soundcloud.ts"; import "ipaddr.js"; +// Dynamic imports should also be rewritten. +import("./a"); +import("./a"); +import("./a"); +import("./react"); +import("a-package/file"); +// Bare dynamic import should not be rewritten. +import("soundcloud.ts"); +// No-substitution template literal should also be rewritten. +import(`./a`); +// Non-string-literal dynamic import should not be rewritten. +import(dynamicPath); diff --git a/tasks/transform_conformance/tests/babel-preset-typescript/test/fixtures/rewriteImportExtensions/input.ts b/tasks/transform_conformance/tests/babel-preset-typescript/test/fixtures/rewriteImportExtensions/input.ts new file mode 100644 index 0000000000000..74c6c9ba256d0 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-preset-typescript/test/fixtures/rewriteImportExtensions/input.ts @@ -0,0 +1,23 @@ +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"; +// Dynamic imports should also be rewritten. +import("./a.ts"); +import("./a.mts"); +import("./a.cts"); +import("./react.tsx"); +import("a-package/file.ts"); +// Bare dynamic import should not be rewritten. +import("soundcloud.ts"); +// No-substitution template literal should also be rewritten. +import(`./a.ts`); +// Non-string-literal dynamic import should not be rewritten. +import(dynamicPath); diff --git a/tasks/transform_conformance/tests/babel-preset-typescript/test/fixtures/rewriteImportExtensions/options.json b/tasks/transform_conformance/tests/babel-preset-typescript/test/fixtures/rewriteImportExtensions/options.json new file mode 100644 index 0000000000000..71805263fd146 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-preset-typescript/test/fixtures/rewriteImportExtensions/options.json @@ -0,0 +1,4 @@ +{ + "sourceType": "module", + "presets": [["typescript", { "rewriteImportExtensions": "rewrite" }]] +} diff --git a/tasks/transform_conformance/tests/babel-preset-typescript/test/fixtures/rewriteImportExtensions/output.js b/tasks/transform_conformance/tests/babel-preset-typescript/test/fixtures/rewriteImportExtensions/output.js new file mode 100644 index 0000000000000..5695b99c08edb --- /dev/null +++ b/tasks/transform_conformance/tests/babel-preset-typescript/test/fixtures/rewriteImportExtensions/output.js @@ -0,0 +1,23 @@ +import "./a.js"; +import "./a.mjs"; +import "./a.cjs"; +import "./react.js"; +// .mtsx and .ctsx are not valid and should not be transformed. +import "./react.mtsx"; +import "./react.ctsx"; +import "a-package/file.js"; +// Bare import, it's either a node package or remapped by an import map +import "soundcloud.ts"; +import "ipaddr.js"; +// Dynamic imports should also be rewritten. +import("./a.js"); +import("./a.mjs"); +import("./a.cjs"); +import("./react.js"); +import("a-package/file.js"); +// Bare dynamic import should not be rewritten. +import("soundcloud.ts"); +// No-substitution template literal should also be rewritten. +import(`./a.js`); +// Non-string-literal dynamic import should not be rewritten. +import(dynamicPath);