Skip to content

fix(es/transforms): avoid rewriting unknown relative extensions#11713

Merged
kdy1 merged 2 commits intomainfrom
kdy1/fix-11644-rewrite-relative-import-extensions
Mar 20, 2026
Merged

fix(es/transforms): avoid rewriting unknown relative extensions#11713
kdy1 merged 2 commits intomainfrom
kdy1/fix-11644-rewrite-relative-import-extensions

Conversation

@kdy1
Copy link
Copy Markdown
Member

@kdy1 kdy1 commented Mar 20, 2026

Summary

This PR fixes jsc.rewriteRelativeImportExtensions rewriting unknown relative extensions to .js.

Previously, specifiers like ./index.legacy were rewritten to ./index.js because unknown extensions fell through to a default rewrite. This diverged from TypeScript behavior.

This change makes rewriting extension-aware and only rewrites supported extensions:

  • ts | js -> js
  • tsx | jsx -> jsx
  • mts | mjs -> mjs
  • cts | cjs -> cjs
  • json -> json
  • unknown extensions -> no rewrite

Also adds fixture coverage for issue #11644 to assert:

  • import "./index.legacy" stays unchanged
  • import "./index.legacy.ts" rewrites to import "./index.legacy.js"

Closes #11644.

Testing

  • git submodule update --init --recursive
  • cargo test -p swc --test projects ts_id_tests__typescript__rewrite_relative_import_specifier__issue_11644__input -- --exact --ignored --nocapture
  • cargo test -p swc --test projects rewrite_relative_import_specifier -- --ignored
  • cargo fmt --all
  • cargo clippy --all --all-targets -- -D warnings
  • cargo test -p swc_ecma_transforms_module

cargo test -p swc was also run; unrelated tests/source_map cases failed in this environment because Node module sourcemap-validator is missing.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 20, 2026

🦋 Changeset detected

Latest commit: 15f192c

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 20, 2026

Binary Sizes

File Size
swc.linux-x64-gnu.node 28M (29065032 bytes)

Commit: 5f276f7

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Mar 20, 2026

Merging this PR will not alter performance

✅ 219 untouched benchmarks


Comparing kdy1/fix-11644-rewrite-relative-import-extensions (15f192c) with main (9b3abe0)

Open in CodSpeed

@kdy1 kdy1 marked this pull request as ready for review March 20, 2026 01:34
@kdy1 kdy1 requested a review from a team as a code owner March 20, 2026 01:34
Copilot AI review requested due to automatic review settings March 20, 2026 01:34
@kdy1 kdy1 self-assigned this Mar 20, 2026
kodiakhq[bot]
kodiakhq Bot previously approved these changes Mar 20, 2026
@kdy1 kdy1 requested a review from a team as a code owner March 20, 2026 01:34
@kdy1 kdy1 merged commit ed09218 into main Mar 20, 2026
22 checks passed
@kdy1 kdy1 deleted the kdy1/fix-11644-rewrite-relative-import-extensions branch March 20, 2026 01:34
@github-actions github-actions Bot added this to the Planned milestone Mar 20, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 20, 2026

PR Review: fix(es/transforms): avoid rewriting unknown relative extensions

Summary

Clean, well-scoped fix. The core change (2 lines in import_rewriter_typescript.rs) correctly aligns the static extension rewriting with TypeScript's behavior by only rewriting known extensions and leaving unknown ones untouched.

Code Quality

  • The fix is correct. The old _ => "js" default was too aggressive — it rewrote specifiers like ./index.legacy to ./index.js, which diverges from TypeScript. Returning None for unknown extensions means no rewrite happens, which is the right behavior.
  • Explicit "js" | "ts" => "js" arm is good. Previously "js" fell through to the _ => "js" default (a no-op in practice), but making it explicit is clearer and necessary now that _ returns None. It also mirrors the TypeScript source structure referenced in the comments.
  • Runtime helper is already correct. The regex in _ts_rewrite_relative_import_extension.js only matches known TS extensions, so the runtime path for dynamic require() already handled this correctly. This PR brings the compile-time static path into alignment.

Test Coverage

  • Good fixture test covering the exact reported issue: ./index.legacy stays unchanged, ./index.legacy.ts rewrites to ./index.legacy.js.
  • The .swcrc config is appropriate (typescript parser, nodenext module, rewriteRelativeImportExtensions: true).

Minor Observations

  1. No-op rewrite for .js extension: When the specifier already has a .js extension, get_output_extension now returns Some("./foo.js") (same path), which causes callers to set raw = None unnecessarily. This is harmless but technically a no-op write. Not worth changing — just noting it.

  2. Could consider testing more unknown extensions: The fixture covers .legacy. It might be worth also having a case for extensionless relative imports (e.g., ./utils) to ensure those aren't touched either — though path.extension() returning None already handles that via the ? operator on line 95. Not a blocker.

Verdict

Looks good to merge. The fix is minimal, correct, well-tested, and aligned with both the TypeScript reference implementation and the existing runtime helper.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 20, 2026

PR Review

Overall: Clean, well-scoped fix. The change correctly aligns SWC's rewriteRelativeImportExtensions behavior with TypeScript's semantics.

Code Quality

The fix is minimal and correct. Changing the catch-all _ => "js" to _ => return None is the right approach — unknown extensions should be left untouched rather than blindly rewritten to .js. The addition of "js" | "ts" => "js" as an explicit arm is necessary since "ts" was previously handled by the catch-all.

The match arms now explicitly enumerate every supported mapping, making the behavior self-documenting and easy to audit against the TypeScript reference implementation.

Potential Issues

  • None identified. The logic correctly mirrors the TypeScript compiler's getOutputExtension behavior at the referenced commit. The path.extension() call returns None for paths without an extension (e.g., ./foo), which already falls through to None correctly — so extensionless specifiers are also unaffected.

Test Coverage

The new fixture covers both key cases from the issue:

  • ./index.legacy (unknown extension) — remains unchanged
  • ./index.legacy.ts (known .ts extension with dots in stem) — correctly rewrites to .index.legacy.js

One minor suggestion: consider also covering require() and export ... from with unknown extensions if not already covered by existing tests, since those code paths (visit_mut_call_expr, visit_mut_named_export, visit_mut_export_all) share the same get_output_extension function. That said, since they all funnel through the same function, the current coverage is likely sufficient.

Performance

No concerns — the change is a constant-time match arm adjustment with no new allocations or branching overhead.

Security

No concerns.

LGTM — this is a clean bug fix with appropriate test coverage.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes jsc.rewriteRelativeImportExtensions so unknown relative extensions are no longer rewritten to .js, aligning behavior with TypeScript (issue #11644).

Changes:

  • Update TypeScript import rewriter to only rewrite a known set of extensions; unknown extensions now result in no rewrite.
  • Add a regression fixture for ./index.legacy (unchanged) and ./index.legacy.ts (rewritten to .js).
  • Add a changeset documenting the patch release.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
crates/swc_ecma_transforms_module/src/rewriter/import_rewriter_typescript.rs Stop defaulting unknown extensions to .js; return None to skip rewrite.
crates/swc/tests/typescript/rewrite-relative-import-specifier/issue-11644/input/main.ts New fixture input reproducing the problematic import specifiers.
crates/swc/tests/typescript/rewrite-relative-import-specifier/issue-11644/output/main.ts New fixture output asserting correct rewrite/no-rewrite behavior.
crates/swc/tests/typescript/rewrite-relative-import-specifier/issue-11644/input/.swcrc Fixture config enabling rewriteRelativeImportExtensions under nodenext.
.changeset/smooth-games-knock.md Patch changeset for swc_core and transform crate.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 95 to 103
let ext = path.extension()?.to_str()?;
let ext = match ext {
"js" | "ts" => "js",
"json" => "json",
"jsx" | "tsx" => "jsx",
"mjs" | "mts" => "mjs",
"cjs" | "cts" => "cjs",
_ => "js",
_ => return None,
};
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the intent is to match TypeScript behavior closely, consider normalizing ext to ASCII-lowercase before matching. As written, specifiers like ./foo.TS (or any mixed-case extension) will not be rewritten even though they are effectively the same extension on case-insensitive platforms.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 15f192c6fd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 96 to +102
let ext = match ext {
"js" | "ts" => "js",
"json" => "json",
"jsx" | "tsx" => "jsx",
"mjs" | "mts" => "mjs",
"cjs" | "cts" => "cjs",
_ => "js",
_ => return None,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep mixed-case TS extensions rewriteable

With jsc.rewriteRelativeImportExtensions enabled, literal specifiers such as import "./foo.TS" or export * from "../bar.MTS" now hit _ => return None, so the emitted JS keeps the .TS/.MTS suffix instead of rewriting to .js/.mjs. That is a regression from the previous fallback behavior, and it also diverges from our dynamic-path helper in crates/swc_ecma_transforms_base/src/helpers/_ts_rewrite_relative_import_extension.js:3, which still matches these extensions case-insensitively via /...ts$/i.

Useful? React with 👍 / 👎.

@github-actions github-actions Bot modified the milestones: Planned, 1.15.21 Mar 22, 2026
@swc-project swc-project locked as resolved and limited conversation to collaborators Apr 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Development

Successfully merging this pull request may close these issues.

jsc.rewriteRelativeImportExtensions does not behave like tsc

2 participants