Skip to content

Fix declaration emit synthesizing extensionless import() specifier under allowImportingTsExtensions + nodenext - #4662

Merged
RyanCavanaugh merged 1 commit into
microsoft:mainfrom
RyanCavanaugh:copilot/fix-import-extension-issue-author-fix-mro2e0m9
Jul 17, 2026
Merged

Fix declaration emit synthesizing extensionless import() specifier under allowImportingTsExtensions + nodenext#4662
RyanCavanaugh merged 1 commit into
microsoft:mainfrom
RyanCavanaugh:copilot/fix-import-extension-issue-author-fix-mro2e0m9

Conversation

@RyanCavanaugh

Copy link
Copy Markdown
Member

When using allowImportingTsExtensions: true with .ts-extension relative imports under module: nodenext, declaration emit was generating invalid extensionless import() type references for node_modules packages — import("mylib/lib/Box") instead of import("mylib/lib/Box.js") — causing TS2307 errors in the emitted .d.ts.

Root cause

GetAllowedEndingsInPreferredOrder in preferences.go checked the ESM+NodeNext special case as:

if syntaxImpliedNodeFormat == core.ResolutionModeESM && moduleResolutionIsNodeNext {

TypeScript 6 uses (syntaxImpliedNodeFormat ?? impliedNodeFormat), falling back to the file's implied node format when syntaxImpliedNodeFormat is not provided. The Go port was missing this fallback.

When tryGetModuleNameAsNodeModule calls with ResolutionModeNone, the ESM check was always false, yielding [TsExtension, Minimal, JsExtension, Index] instead of the correct [TsExtension, JsExtension]. With Minimal before JsExtension, processEnding picked the extensionless form for .d.ts declaration files.

Fix

  • internal/modulespecifiers/preferences.go: When syntaxImpliedNodeFormat is ResolutionModeNone, fall back to resolutionMode (the file's default) before the ESM+NodeNext check — matching TypeScript's ?? semantics.

  • testdata/tests/cases/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.ts: Regression test covering the exact repro: ESM package, .ts-extension relative import chain, node_modules subpath with .js extension, verified mod.d.ts output is now import("mylib/lib/Box.js").Box<string> with no TS2307.

…nsions

When a file uses `.ts`-extension relative imports (allowImportingTsExtensions),
declaration emit was synthesizing extensionless import() specifiers for
non-relative (node_modules) module references, which is invalid under nodenext.

Root cause: GetAllowedEndingsInPreferredOrder used syntaxImpliedNodeFormat
directly without falling back to the file's implied format when it's None.
TypeScript 6 uses (syntaxImpliedNodeFormat ?? impliedNodeFormat), properly
falling back to the ESM file's implied mode. This caused the wrong allowedEndings
ordering ([TsExtension, Minimal, JsExtension, ...] instead of [TsExtension, JsExtension]),
which led processEnding to return extensionless paths for .d.ts files.

The fix adds the same fallback: when syntaxImpliedNodeFormat is ResolutionModeNone,
use resolutionMode (the file's default) for the ESM+NodeNext check.

Fixes microsoft#4616

Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 16, 2026 22:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes declaration emit to preserve .js package subpath extensions under NodeNext when TypeScript-extension imports are allowed.

Changes:

  • Falls back to the file’s resolution mode when no syntax-implied mode exists.
  • Adds regression coverage and declaration/type/symbol baselines.
Show a summary per file
File Description
internal/modulespecifiers/preferences.go Implements the upstream resolution-mode fallback.
testdata/tests/cases/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.ts Adds the regression scenario.
testdata/baselines/reference/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.js Verifies .js in declaration emit.
testdata/baselines/reference/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.types Records inferred types.
testdata/baselines/reference/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.symbols Records symbols.

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0
  • Review effort level: Medium

@RyanCavanaugh

Copy link
Copy Markdown
Member Author

This is a redo of #4649 but now GitHub API is down 🙁

@RyanCavanaugh
RyanCavanaugh added this pull request to the merge queue Jul 17, 2026
Merged via the queue into microsoft:main with commit 2d3f632 Jul 17, 2026
51 of 57 checks passed
@RyanCavanaugh
RyanCavanaugh deleted the copilot/fix-import-extension-issue-author-fix-mro2e0m9 branch July 17, 2026 19:53
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.

Behavior difference: declaration emit synthesizes extensionless import() specifier under allowImportingTsExtensions, invalid in nodenext

3 participants