From cf2224b295cd467167f46997ac667536dc7ebc48 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:26:41 +0000 Subject: [PATCH 1/2] Initial plan From 2b2a354731c7dbc7450bed76f35a22a118be670b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:50:02 +0000 Subject: [PATCH 2/2] Fix declaration emit dropping .js extension with allowImportingTsExtensions 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 #4616 Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- internal/modulespecifiers/preferences.go | 8 ++++- ...tSpecifierWithTsExtensionRelativeImport.js | 28 +++++++++++++++ ...ifierWithTsExtensionRelativeImport.symbols | 35 +++++++++++++++++++ ...ecifierWithTsExtensionRelativeImport.types | 30 ++++++++++++++++ ...tSpecifierWithTsExtensionRelativeImport.ts | 26 ++++++++++++++ 5 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 testdata/baselines/reference/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.js create mode 100644 testdata/baselines/reference/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.symbols create mode 100644 testdata/baselines/reference/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.types create mode 100644 testdata/tests/cases/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.ts diff --git a/internal/modulespecifiers/preferences.go b/internal/modulespecifiers/preferences.go index 16c8e9e4aad..f3981b24166 100644 --- a/internal/modulespecifiers/preferences.go +++ b/internal/modulespecifiers/preferences.go @@ -174,7 +174,13 @@ func GetAllowedEndingsInPreferredOrder( moduleResolution := compilerOptions.GetModuleResolutionKind() moduleResolutionIsNodeNext := core.ModuleResolutionKindNode16 <= moduleResolution && moduleResolution <= core.ModuleResolutionKindNodeNext allowImportingTsExtension := shouldAllowImportingTsExtension(compilerOptions, importingSourceFile.FileName()) - if syntaxImpliedNodeFormat == core.ResolutionModeESM && moduleResolutionIsNodeNext { + // TypeScript uses `(syntaxImpliedNodeFormat ?? impliedNodeFormat)` here - fall back to the + // file's default resolution mode when no syntax-implied mode is given. + effectiveSyntaxMode := syntaxImpliedNodeFormat + if effectiveSyntaxMode == core.ResolutionModeNone { + effectiveSyntaxMode = resolutionMode + } + if effectiveSyntaxMode == core.ResolutionModeESM && moduleResolutionIsNodeNext { if allowImportingTsExtension { return []ModuleSpecifierEnding{ModuleSpecifierEndingTsExtension, ModuleSpecifierEndingJsExtension} } diff --git a/testdata/baselines/reference/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.js b/testdata/baselines/reference/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.js new file mode 100644 index 00000000000..fbdc9677d65 --- /dev/null +++ b/testdata/baselines/reference/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.js @@ -0,0 +1,28 @@ +//// [tests/cases/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.ts] //// + +//// [package.json] +{ "type": "module" } + +//// [package.json] +{ "name": "mylib", "version": "1.0.0", "main": "./lib/index.js" } + +//// [Box.d.ts] +export interface Box { readonly value: T } +export declare function box(value: T): Box; + +//// [helper.ts] +import * as B from 'mylib/lib/Box.js'; +export const inner = B.box('hello'); + +//// [mod.ts] +import { inner } from './helper.ts'; +export const out = inner; + + + + +//// [helper.d.ts] +import * as B from 'mylib/lib/Box.js'; +export declare const inner: B.Box; +//// [mod.d.ts] +export declare const out: import("mylib/lib/Box.js").Box; diff --git a/testdata/baselines/reference/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.symbols b/testdata/baselines/reference/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.symbols new file mode 100644 index 00000000000..d40eb9868ab --- /dev/null +++ b/testdata/baselines/reference/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.symbols @@ -0,0 +1,35 @@ +//// [tests/cases/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.ts] //// + +=== /node_modules/mylib/lib/Box.d.ts === +export interface Box { readonly value: T } +>Box : Symbol(Box, Decl(Box.d.ts, 0, 0)) +>T : Symbol(T, Decl(Box.d.ts, 0, 21)) +>value : Symbol(Box.value, Decl(Box.d.ts, 0, 25)) +>T : Symbol(T, Decl(Box.d.ts, 0, 21)) + +export declare function box(value: T): Box; +>box : Symbol(box, Decl(Box.d.ts, 0, 45)) +>T : Symbol(T, Decl(Box.d.ts, 1, 28)) +>value : Symbol(value, Decl(Box.d.ts, 1, 31)) +>T : Symbol(T, Decl(Box.d.ts, 1, 28)) +>Box : Symbol(Box, Decl(Box.d.ts, 0, 0)) +>T : Symbol(T, Decl(Box.d.ts, 1, 28)) + +=== /helper.ts === +import * as B from 'mylib/lib/Box.js'; +>B : Symbol(B, Decl(helper.ts, 0, 6)) + +export const inner = B.box('hello'); +>inner : Symbol(inner, Decl(helper.ts, 1, 12)) +>B.box : Symbol(B.box, Decl(Box.d.ts, 0, 45)) +>B : Symbol(B, Decl(helper.ts, 0, 6)) +>box : Symbol(B.box, Decl(Box.d.ts, 0, 45)) + +=== /mod.ts === +import { inner } from './helper.ts'; +>inner : Symbol(inner, Decl(mod.ts, 0, 8)) + +export const out = inner; +>out : Symbol(out, Decl(mod.ts, 1, 12)) +>inner : Symbol(inner, Decl(mod.ts, 0, 8)) + diff --git a/testdata/baselines/reference/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.types b/testdata/baselines/reference/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.types new file mode 100644 index 00000000000..67d2b168d20 --- /dev/null +++ b/testdata/baselines/reference/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.types @@ -0,0 +1,30 @@ +//// [tests/cases/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.ts] //// + +=== /node_modules/mylib/lib/Box.d.ts === +export interface Box { readonly value: T } +>value : T + +export declare function box(value: T): Box; +>box : (value: T) => Box +>value : T + +=== /helper.ts === +import * as B from 'mylib/lib/Box.js'; +>B : typeof B + +export const inner = B.box('hello'); +>inner : B.Box +>B.box('hello') : B.Box +>B.box : (value: T) => B.Box +>B : typeof B +>box : (value: T) => B.Box +>'hello' : "hello" + +=== /mod.ts === +import { inner } from './helper.ts'; +>inner : import("mylib/lib/Box.js").Box + +export const out = inner; +>out : import("mylib/lib/Box.js").Box +>inner : import("mylib/lib/Box.js").Box + diff --git a/testdata/tests/cases/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.ts b/testdata/tests/cases/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.ts new file mode 100644 index 00000000000..85934e53472 --- /dev/null +++ b/testdata/tests/cases/compiler/declarationEmitImportSpecifierWithTsExtensionRelativeImport.ts @@ -0,0 +1,26 @@ +// @module: nodenext +// @declaration: true +// @emitDeclarationOnly: true +// @allowImportingTsExtensions: true + +// Regression test for https://github.com/microsoft/typescript-go/issues/4616 +// When a file uses `.ts` extension relative imports with allowImportingTsExtensions, +// declaration emit should preserve `.js` extensions on non-relative (node_modules) imports. + +// @Filename: /package.json +{ "type": "module" } + +// @Filename: /node_modules/mylib/package.json +{ "name": "mylib", "version": "1.0.0", "main": "./lib/index.js" } + +// @Filename: /node_modules/mylib/lib/Box.d.ts +export interface Box { readonly value: T } +export declare function box(value: T): Box; + +// @Filename: /helper.ts +import * as B from 'mylib/lib/Box.js'; +export const inner = B.box('hello'); + +// @Filename: /mod.ts +import { inner } from './helper.ts'; +export const out = inner;