Latest Results
fix(es/module): Rewrite `.tsx` imports to `.js` unless JSX is preserved (#11995)
**Description:**
(Note that this work was done by Claude Code)
With `jsc.rewriteRelativeImportExtensions: true`, SWC rewrites `.tsx`
specifiers in relative imports to `.jsx` unconditionally:
```ts
import { Badge } from "./Badge.tsx";
// emits: import { Badge } from "./Badge.jsx";
```
But SWC compiles `Badge.tsx` to `Badge.js`, so the rewritten specifier
points at a file that doesn't exist. TypeScript's `getOutputExtension`
([source](https://github.com/microsoft/TypeScript/blob/3eb7b6a1794a6d2cde7948a3016c57e628b104b9/src/compiler/emitter.ts#L540),
already referenced in a comment in the rewriter) only maps to `.jsx`
when `compilerOptions.jsx === "preserve"`; otherwise it maps to `.js`.
Babel's `rewriteImportExtensions` also emits `.js`. The SWC port dropped
that condition.
This PR mirrors tsc's behavior:
- `.tsx` specifiers now rewrite to `.js`, unless
`jsc.transform.react.runtime` is `"preserve"` (SWC's JSX-passthrough
mode), in which case they rewrite to `.jsx` as before.
- `.jsx` specifiers are never rewritten — tsc's
`shouldRewriteModuleSpecifier`
([source](https://github.com/microsoft/TypeScript/blob/3eb7b6a1794a6d2cde7948a3016c57e628b104b9/src/compiler/utilities.ts#L4331))
only rewrites specifiers with a TS extension, and the
`__rewriteRelativeImportExtension` runtime helper likewise leaves `.jsx`
untouched.
- In preserve mode, the helper injected for non-literal dynamic imports
now receives its `preserveJsx: true` second argument, keeping static and
dynamic rewrites consistent (the helper already supported it and
defaulted `.tsx` → `.js`, so the two paths previously disagreed).
`typescript_import_rewriter()` now takes a `jsx_preserve: bool`; the
only call site (`crates/swc/src/config/mod.rs`) passes a `jsx_preserve`
local shared with the existing `jsx_enabled` check. Since the added
required parameter breaks the public signature re-exported through
`swc_core`, the changeset marks `swc_ecma_transforms_module` and
`swc_core` as `major`.
Tests: the `ts-emit` fixture output changes from `./foo.jsx` to
`./foo.js`, and two new fixture cases cover a `.tsx` file importing
`./Badge.tsx` / `./Legacy.jsx` (plus literal and non-literal dynamic
imports) with `runtime: "automatic"` (expects `.js`, `.jsx` untouched)
and `runtime: "preserve"` (expects `.jsx` and the helper flag).
Also bumps `anyhow` in `Cargo.lock` to fix the RUSTSEC-2026-0190
(unsound `Error::downcast_mut()`) advisory failing CI.
**BREAKING CHANGE:**
`swc_ecma_transforms_module::rewriter::typescript_import_rewriter`
(re-exported via `swc_core`) now takes a `jsx_preserve: bool` argument.
Behavior: `.tsx` specifiers are rewritten to `.js` instead of `.jsx`
unless `jsc.transform.react.runtime` is `"preserve"`, and `.jsx`
specifiers are no longer rewritten.
**Related issue (if exists):**
None filed; related but distinct: #11360, #11644, #11143.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Latest Branches
0%
refactor/module-transform 0%
kdy1/compressed-native-bindings 0%
© 2026 CodSpeed Technology