From 8c0b563c7f887a592a70d237f84a5adb5bebd17d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Fri, 5 Dec 2025 23:51:40 +0900 Subject: [PATCH 1/4] Add test for self-import resolution in .d.ts files (#62806) --- .../unittests/tsbuild/moduleResolution.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/testRunner/unittests/tsbuild/moduleResolution.ts b/src/testRunner/unittests/tsbuild/moduleResolution.ts index 0d5d1f5310121..fe6bb55b2b94e 100644 --- a/src/testRunner/unittests/tsbuild/moduleResolution.ts +++ b/src/testRunner/unittests/tsbuild/moduleResolution.ts @@ -251,3 +251,45 @@ describe("unittests:: tsbuild:: moduleResolution:: resolution sharing", () => { }], }); }); + +describe("unittests:: tsbuild:: moduleResolution:: self-import in .d.ts with project reference redirect", () => { + verifyTsc({ + scenario: "moduleResolution", + subScenario: "self-import in .d.ts resolves to dist", + sys: () => + TestServerHost.createWatchedSystem({ + "/home/src/workspaces/project/packages/pkg1/package.json": jsonToReadableText({ + name: "pkg1", + version: "1.0.0", + main: "dist/index.js", + types: "dist/index.d.ts", + exports: { + ".": { + types: "./dist/index.d.ts", + default: "./dist/index.js" + } + } + }), + "/home/src/workspaces/project/packages/pkg1/tsconfig.json": jsonToReadableText({ + compilerOptions: { + composite: true, + outDir: "dist", + rootDir: "src", + module: "nodenext", + moduleResolution: "nodenext" + }, + include: ["src"] + }), + "/home/src/workspaces/project/packages/pkg1/src/index.ts": `export class C {}`, + "/home/src/workspaces/project/packages/pkg1/src/other.d.ts": dedent` + import { C } from "pkg1"; + export declare const c: C; + `, + "/home/src/workspaces/project/packages/pkg1/src/usage.ts": dedent` + import { c } from "./other"; + export const usage = c; + ` + }), + commandLineArgs: ["-b", "packages/pkg1", "--verbose", "--traceResolution"], + }); +}); From 51317c3b23966f8dcf9cabc792cbad3ac3513109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Fri, 5 Dec 2025 23:51:53 +0900 Subject: [PATCH 2/4] Fix self-import resolution in declaration files during build mode --- src/compiler/moduleNameResolver.ts | 64 ++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 1c5f22e25bd9c..0707c7c98fa14 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -15,6 +15,7 @@ import { contains, containsPath, createCompilerDiagnostic, + createGetCanonicalFileName, Debug, deduplicate, Diagnostic, @@ -1474,6 +1475,69 @@ export function resolveModuleName(moduleName: string, containingFile: string, co } } + if (result.resolvedModule && isDeclarationFileName(containingFile)) { + const { extension, resolvedFileName } = result.resolvedModule; + if (extension === Extension.Ts || extension === Extension.Tsx || extension === Extension.Mts || extension === Extension.Cts) { + let projectDir: string | undefined; + let options: CompilerOptions | undefined; + + if (redirectedReference) { + projectDir = getDirectoryPath(redirectedReference.sourceFile.fileName); + options = redirectedReference.commandLine.options; + } else { + projectDir = getDirectoryPath(containingFile); + options = compilerOptions; + } + + let currentDir = projectDir; + let packageJsonInfo: PackageJsonInfoCacheEntry | undefined; + while (currentDir) { + const packageJsonPath = combinePaths(currentDir, "package.json"); + packageJsonInfo = cache?.getPackageJsonInfo(packageJsonPath); + if (isPackageJsonInfo(packageJsonInfo)) break; + const parent = getDirectoryPath(currentDir); + if (parent === currentDir) break; + currentDir = parent; + } + + if (isPackageJsonInfo(packageJsonInfo) && packageJsonInfo.contents.packageJsonContent.name === moduleName) { + if (options) { + let outputDts: string | undefined; + if (options.outFile) { + outputDts = changeAnyExtension(options.outFile, ".d.ts"); + } + else if (options.outDir) { + const getCanonicalFileName = createGetCanonicalFileName( + typeof host.useCaseSensitiveFileNames === "function" + ? host.useCaseSensitiveFileNames() + : host.useCaseSensitiveFileNames ?? true + ); + + let rootDir = options.rootDir; + if (!rootDir && redirectedReference) { + rootDir = getCommonSourceDirectory( + options, + () => redirectedReference.commandLine.fileNames, + host.getCurrentDirectory ? host.getCurrentDirectory() : "", + getCanonicalFileName + ); + } + + if (rootDir) { + const relativePath = getRelativePathFromDirectory(rootDir, resolvedFileName, getCanonicalFileName); + outputDts = combinePaths(options.outDir, changeAnyExtension(relativePath, ".d.ts")); + } + } + + if (outputDts) { + result.resolvedModule.resolvedFileName = outputDts; + result.resolvedModule.extension = Extension.Dts; + } + } + } + } + } + return result; } From 5f324191b3cd525093dedc91be6bc99caa8098a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Fri, 5 Dec 2025 23:51:58 +0900 Subject: [PATCH 3/4] Accept baseline for self-import resolution test --- .../self-import-in-.d.ts-resolves-to-dist.js | 240 ++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 tests/baselines/reference/tsbuild/moduleResolution/self-import-in-.d.ts-resolves-to-dist.js diff --git a/tests/baselines/reference/tsbuild/moduleResolution/self-import-in-.d.ts-resolves-to-dist.js b/tests/baselines/reference/tsbuild/moduleResolution/self-import-in-.d.ts-resolves-to-dist.js new file mode 100644 index 0000000000000..d3ed4bc765752 --- /dev/null +++ b/tests/baselines/reference/tsbuild/moduleResolution/self-import-in-.d.ts-resolves-to-dist.js @@ -0,0 +1,240 @@ +currentDirectory:: /home/src/workspaces/project useCaseSensitiveFileNames:: false +Input:: +//// [/home/src/workspaces/project/packages/pkg1/package.json] +{ + "name": "pkg1", + "version": "1.0.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } +} + +//// [/home/src/workspaces/project/packages/pkg1/tsconfig.json] +{ + "compilerOptions": { + "composite": true, + "outDir": "dist", + "rootDir": "src", + "module": "nodenext", + "moduleResolution": "nodenext" + }, + "include": [ + "src" + ] +} + +//// [/home/src/workspaces/project/packages/pkg1/src/index.ts] +export class C {} + +//// [/home/src/workspaces/project/packages/pkg1/src/other.d.ts] +import { C } from "pkg1"; +export declare const c: C; + + +//// [/home/src/workspaces/project/packages/pkg1/src/usage.ts] +import { c } from "./other"; +export const usage = c; + + +//// [/home/src/tslibs/TS/Lib/lib.d.ts] +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +declare const console: { log(msg: any): void; }; + + +/home/src/tslibs/TS/Lib/tsc.js -b packages/pkg1 --verbose --traceResolution +Output:: +[HH:MM:SS AM] Projects in this build: + * packages/pkg1/tsconfig.json + +[HH:MM:SS AM] Project 'packages/pkg1/tsconfig.json' is out of date because output file 'packages/pkg1/tsconfig.tsbuildinfo' does not exist + +[HH:MM:SS AM] Building project '/home/src/workspaces/project/packages/pkg1/tsconfig.json'... + +File '/home/src/workspaces/project/packages/pkg1/src/package.json' does not exist. +Found 'package.json' at '/home/src/workspaces/project/packages/pkg1/package.json'. +File '/home/src/workspaces/project/packages/pkg1/src/package.json' does not exist according to earlier cached lookups. +File '/home/src/workspaces/project/packages/pkg1/package.json' exists according to earlier cached lookups. +======== Resolving module 'pkg1' from '/home/src/workspaces/project/packages/pkg1/src/other.d.ts'. ======== +Explicitly specified module resolution kind: 'NodeNext'. +Resolving in CJS mode with conditions 'require', 'types', 'node'. +File '/home/src/workspaces/project/packages/pkg1/src/package.json' does not exist according to earlier cached lookups. +File '/home/src/workspaces/project/packages/pkg1/package.json' exists according to earlier cached lookups. +Entering conditional exports. +Matched 'exports' condition 'types'. +Using 'exports' subpath '.' with target './dist/index.d.ts'. +File '/home/src/workspaces/project/packages/pkg1/src/index.ts' exists - use it as a name resolution result. +'package.json' does not have a 'peerDependencies' field. +Resolved under condition 'types'. +Exiting conditional exports. +======== Module name 'pkg1' was successfully resolved to '/home/src/workspaces/project/packages/pkg1/src/index.ts' with Package ID 'pkg1/src/index.ts@1.0.0'. ======== +File '/home/src/workspaces/project/packages/pkg1/package.json' exists according to earlier cached lookups. +File '/home/src/workspaces/project/packages/pkg1/src/package.json' does not exist according to earlier cached lookups. +File '/home/src/workspaces/project/packages/pkg1/package.json' exists according to earlier cached lookups. +======== Resolving module './other' from '/home/src/workspaces/project/packages/pkg1/src/usage.ts'. ======== +Explicitly specified module resolution kind: 'NodeNext'. +Resolving in CJS mode with conditions 'require', 'types', 'node'. +Loading module as file / folder, candidate module location '/home/src/workspaces/project/packages/pkg1/src/other', target file types: TypeScript, JavaScript, Declaration, JSON. +File '/home/src/workspaces/project/packages/pkg1/src/other.ts' does not exist. +File '/home/src/workspaces/project/packages/pkg1/src/other.tsx' does not exist. +File '/home/src/workspaces/project/packages/pkg1/src/other.d.ts' exists - use it as a name resolution result. +======== Module name './other' was successfully resolved to '/home/src/workspaces/project/packages/pkg1/src/other.d.ts'. ======== +File '/home/src/tslibs/TS/Lib/package.json' does not exist. +File '/home/src/tslibs/TS/package.json' does not exist. +File '/home/src/tslibs/package.json' does not exist. +File '/home/src/package.json' does not exist. +File '/home/package.json' does not exist. +File '/package.json' does not exist. +error TS5055: Cannot write file '/home/src/workspaces/project/packages/pkg1/dist/index.d.ts' because it would overwrite input file. + + +Found 1 error. + + + +//// [/home/src/tslibs/TS/Lib/lib.esnext.full.d.ts] *Lib* + +//// [/home/src/workspaces/project/packages/pkg1/dist/index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.C = void 0; +class C { +} +exports.C = C; + + +//// [/home/src/workspaces/project/packages/pkg1/dist/usage.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.usage = void 0; +const other_1 = require("./other"); +exports.usage = other_1.c; + + +//// [/home/src/workspaces/project/packages/pkg1/dist/usage.d.ts] +export declare const usage: C; + + +//// [/home/src/workspaces/project/packages/pkg1/tsconfig.tsbuildinfo] +{"fileNames":["../../../../tslibs/ts/lib/lib.esnext.full.d.ts","./src/index.ts","./src/other.d.ts","./src/usage.ts"],"fileIdsList":[[3]],"fileInfos":[{"version":"-25093698414-interface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedFormat":1},{"version":"-7264672072-export class C {}","impliedFormat":1},{"version":"13493985673-import { C } from \"pkg1\";\nexport declare const c: C;\n","impliedFormat":1},{"version":"-4480060038-import { c } from \"./other\";\nexport const usage = c;\n","signature":"-7622683403-export declare const usage: C;\n","impliedFormat":1}],"root":[[2,4]],"options":{"composite":true,"module":199,"outDir":"./dist","rootDir":"./src"},"referencedMap":[[4,1]],"semanticDiagnosticsPerFile":[1,2,3,4],"emitSignatures":[2],"latestChangedDtsFile":"./dist/usage.d.ts","version":"FakeTSVersion"} + +//// [/home/src/workspaces/project/packages/pkg1/tsconfig.tsbuildinfo.readable.baseline.txt] +{ + "fileNames": [ + "../../../../tslibs/ts/lib/lib.esnext.full.d.ts", + "./src/index.ts", + "./src/other.d.ts", + "./src/usage.ts" + ], + "fileIdsList": [ + [ + "./src/other.d.ts" + ] + ], + "fileInfos": { + "../../../../tslibs/ts/lib/lib.esnext.full.d.ts": { + "original": { + "version": "-25093698414-interface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ndeclare const console: { log(msg: any): void; };", + "affectsGlobalScope": true, + "impliedFormat": 1 + }, + "version": "-25093698414-interface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ndeclare const console: { log(msg: any): void; };", + "signature": "-25093698414-interface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ndeclare const console: { log(msg: any): void; };", + "affectsGlobalScope": true, + "impliedFormat": "commonjs" + }, + "./src/index.ts": { + "original": { + "version": "-7264672072-export class C {}", + "impliedFormat": 1 + }, + "version": "-7264672072-export class C {}", + "signature": "-7264672072-export class C {}", + "impliedFormat": "commonjs" + }, + "./src/other.d.ts": { + "original": { + "version": "13493985673-import { C } from \"pkg1\";\nexport declare const c: C;\n", + "impliedFormat": 1 + }, + "version": "13493985673-import { C } from \"pkg1\";\nexport declare const c: C;\n", + "signature": "13493985673-import { C } from \"pkg1\";\nexport declare const c: C;\n", + "impliedFormat": "commonjs" + }, + "./src/usage.ts": { + "original": { + "version": "-4480060038-import { c } from \"./other\";\nexport const usage = c;\n", + "signature": "-7622683403-export declare const usage: C;\n", + "impliedFormat": 1 + }, + "version": "-4480060038-import { c } from \"./other\";\nexport const usage = c;\n", + "signature": "-7622683403-export declare const usage: C;\n", + "impliedFormat": "commonjs" + } + }, + "root": [ + [ + [ + 2, + 4 + ], + [ + "./src/index.ts", + "./src/other.d.ts", + "./src/usage.ts" + ] + ] + ], + "options": { + "composite": true, + "module": 199, + "outDir": "./dist", + "rootDir": "./src" + }, + "referencedMap": { + "./src/usage.ts": [ + "./src/other.d.ts" + ] + }, + "semanticDiagnosticsPerFile": [ + [ + "../../../../tslibs/ts/lib/lib.esnext.full.d.ts", + "not cached or not changed" + ], + [ + "./src/index.ts", + "not cached or not changed" + ], + [ + "./src/other.d.ts", + "not cached or not changed" + ], + [ + "./src/usage.ts", + "not cached or not changed" + ] + ], + "emitSignatures": [ + "./src/index.ts" + ], + "latestChangedDtsFile": "./dist/usage.d.ts", + "version": "FakeTSVersion", + "size": 1179 +} + + +exitCode:: ExitStatus.DiagnosticsPresent_OutputsSkipped From 7d331a194da90ed600ff2b2f9efc1cba77afe6a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=AC=EC=9A=B1?= Date: Sat, 6 Dec 2025 00:18:49 +0900 Subject: [PATCH 4/4] Apply code formatting --- src/compiler/moduleNameResolver.ts | 25 ++++++++++--------- .../unittests/tsbuild/moduleResolution.ts | 12 ++++----- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 0707c7c98fa14..d35168b56f53f 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -1484,7 +1484,8 @@ export function resolveModuleName(moduleName: string, containingFile: string, co if (redirectedReference) { projectDir = getDirectoryPath(redirectedReference.sourceFile.fileName); options = redirectedReference.commandLine.options; - } else { + } + else { projectDir = getDirectoryPath(containingFile); options = compilerOptions; } @@ -1492,16 +1493,16 @@ export function resolveModuleName(moduleName: string, containingFile: string, co let currentDir = projectDir; let packageJsonInfo: PackageJsonInfoCacheEntry | undefined; while (currentDir) { - const packageJsonPath = combinePaths(currentDir, "package.json"); - packageJsonInfo = cache?.getPackageJsonInfo(packageJsonPath); - if (isPackageJsonInfo(packageJsonInfo)) break; - const parent = getDirectoryPath(currentDir); - if (parent === currentDir) break; - currentDir = parent; + const packageJsonPath = combinePaths(currentDir, "package.json"); + packageJsonInfo = cache?.getPackageJsonInfo(packageJsonPath); + if (isPackageJsonInfo(packageJsonInfo)) break; + const parent = getDirectoryPath(currentDir); + if (parent === currentDir) break; + currentDir = parent; } if (isPackageJsonInfo(packageJsonInfo) && packageJsonInfo.contents.packageJsonContent.name === moduleName) { - if (options) { + if (options) { let outputDts: string | undefined; if (options.outFile) { outputDts = changeAnyExtension(options.outFile, ".d.ts"); @@ -1510,16 +1511,16 @@ export function resolveModuleName(moduleName: string, containingFile: string, co const getCanonicalFileName = createGetCanonicalFileName( typeof host.useCaseSensitiveFileNames === "function" ? host.useCaseSensitiveFileNames() - : host.useCaseSensitiveFileNames ?? true + : host.useCaseSensitiveFileNames ?? true, ); let rootDir = options.rootDir; if (!rootDir && redirectedReference) { - rootDir = getCommonSourceDirectory( + rootDir = getCommonSourceDirectory( options, () => redirectedReference.commandLine.fileNames, host.getCurrentDirectory ? host.getCurrentDirectory() : "", - getCanonicalFileName + getCanonicalFileName, ); } @@ -1533,7 +1534,7 @@ export function resolveModuleName(moduleName: string, containingFile: string, co result.resolvedModule.resolvedFileName = outputDts; result.resolvedModule.extension = Extension.Dts; } - } + } } } } diff --git a/src/testRunner/unittests/tsbuild/moduleResolution.ts b/src/testRunner/unittests/tsbuild/moduleResolution.ts index fe6bb55b2b94e..a70121770aaaf 100644 --- a/src/testRunner/unittests/tsbuild/moduleResolution.ts +++ b/src/testRunner/unittests/tsbuild/moduleResolution.ts @@ -266,9 +266,9 @@ describe("unittests:: tsbuild:: moduleResolution:: self-import in .d.ts with pro exports: { ".": { types: "./dist/index.d.ts", - default: "./dist/index.js" - } - } + default: "./dist/index.js", + }, + }, }), "/home/src/workspaces/project/packages/pkg1/tsconfig.json": jsonToReadableText({ compilerOptions: { @@ -276,9 +276,9 @@ describe("unittests:: tsbuild:: moduleResolution:: self-import in .d.ts with pro outDir: "dist", rootDir: "src", module: "nodenext", - moduleResolution: "nodenext" + moduleResolution: "nodenext", }, - include: ["src"] + include: ["src"], }), "/home/src/workspaces/project/packages/pkg1/src/index.ts": `export class C {}`, "/home/src/workspaces/project/packages/pkg1/src/other.d.ts": dedent` @@ -288,7 +288,7 @@ describe("unittests:: tsbuild:: moduleResolution:: self-import in .d.ts with pro "/home/src/workspaces/project/packages/pkg1/src/usage.ts": dedent` import { c } from "./other"; export const usage = c; - ` + `, }), commandLineArgs: ["-b", "packages/pkg1", "--verbose", "--traceResolution"], });