Skip to content

fix(resolution): an import naming the emitted .js extension resolves to its .ts source - #1706

Open
bompus wants to merge 1 commit into
colbymchenry:mainfrom
bompus:upstream/fix-ts-js-specifier-imports
Open

fix(resolution): an import naming the emitted .js extension resolves to its .ts source#1706
bompus wants to merge 1 commit into
colbymchenry:mainfrom
bompus:upstream/fix-ts-js-specifier-imports

Conversation

@bompus

@bompus bompus commented Sep 5, 2026

Copy link
Copy Markdown

Fixes #1705.

What

resolveRelativeImport and the aliased path's tryWithExt now fall back to findSourceForEmittedSpecifier: when a specifier ends in .js / .jsx / .mjs / .cjs and no file of that exact name exists, the source extensions TypeScript compiles from are tried (.ts, .tsx, .d.ts / .tsx / .mts, .d.mts / .cts, .d.cts). Only for the TS/JS-family languages (typescript, tsx, javascript, jsx, vue, svelte, astro, arkts); a real .js beside the .ts still wins because the existing as-is check runs first.

Why

Under moduleResolution: node16 | nodenext | bundler the emitted extension is what TypeScript requires in the specifier, so every import in such a project was invisible to the import resolver and each imported name fell through to bare-name matching. The clearest symptom is a method that wraps the same-named function it imports resolving to itself (a calls self-edge at confidence 0.4); the wider one is that cross-module edges in these projects were name guesses instead of import-backed.

Measured

Real repo, 582 files, 92 .ts files with .js specifiers, indexed with and without this change on the same build:

before after
calls/imports edges resolvedBy: import 4,002 7,312
resolvedBy: exact-match 15,419 12,784
resolvedBy: fuzzy 44 23
self-calls edges 31 22 (the 8 wrapper-method ones gone; the rest are recursion)

Tests

__tests__/import-emitted-specifier.test.ts: unit cases through resolveImportPath for .js, .jsx, .mjs, .cjs, the real-.js-wins case, an aliased @/lib/util.js, a missing target, and a non-TS language; plus an end-to-end CodeGraph.init on the wrapper-method repro asserting the edge lands on the imported function and not on the method. Full suite: no failure outside the host's known EPERM/EBUSY teardown set.

…to its .ts source

Under moduleResolution node16/nodenext/bundler the specifier carries the
emitted extension and no file of that name exists, so resolveRelativeImport
and the aliased path returned null and every imported name fell through to
bare-name matching: a method wrapping the same-named helper it imports
resolved to itself. Retry with the source extensions TypeScript compiles from
when the emitted file is absent; a real .js beside the .ts still wins.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TypeScript imports naming the emitted .js extension never resolve through the import resolver (wrapper methods become self-edges)

1 participant