From 11fe82e40463328c7121858b228b002e30728816 Mon Sep 17 00:00:00 2001 From: onmax Date: Mon, 24 Aug 2026 15:59:10 +0000 Subject: [PATCH 1/3] fix(types): resolve exported auto-import subpaths to files --- src/build/types.ts | 6 +++- test/unit/types-imports.test.ts | 56 +++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/build/types.ts b/src/build/types.ts index 67eedfb262..88575368bd 100644 --- a/src/build/types.ts +++ b/src/build/types.ts @@ -80,7 +80,11 @@ export async function writeTypes(nitro: Nitro) { path = resolvedPath; } else { const subpath = await lookupNodeModuleSubpath(resolvedPath); - path = subpath && subpath !== "./" ? join(dir, name, subpath) : resolvedPath; + const subpathPath = subpath && subpath !== "./" && join(dir, name, subpath); + path = + subpathPath && existsSync(subpathPath) && !(await isDirectory(subpathPath)) + ? subpathPath + : resolvedPath; } } } diff --git a/test/unit/types-imports.test.ts b/test/unit/types-imports.test.ts index fff34e37ad..475304b1a5 100644 --- a/test/unit/types-imports.test.ts +++ b/test/unit/types-imports.test.ts @@ -1,6 +1,6 @@ -import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join } from "pathe"; +import { dirname, join, resolve } from "pathe"; import { describe, expect, it } from "vitest"; import { createNitro, writeTypes } from "nitro/builder"; @@ -63,4 +63,56 @@ describe("writeTypes auto-import resolution", () => { `specifier should not end at the package directory, got ${specifier}` ).toBe(false); }); + + it("emits a resolvable path for package export subpaths", async () => { + const pkgDir = join(fixtureDir, "node_modules", "export-subpath-pkg"); + mkdirSync(join(pkgDir, "lib"), { recursive: true }); + writeFileSync( + join(pkgDir, "package.json"), + JSON.stringify({ + name: "export-subpath-pkg", + type: "module", + exports: { + "./h3": "./lib/h3.mjs", + }, + }) + ); + writeFileSync(join(pkgDir, "lib", "h3.mjs"), "export function useH3() {}\n"); + writeFileSync(join(pkgDir, "lib", "h3.d.mts"), "export declare function useH3(): void\n"); + + const nitro = await createNitro({ + rootDir: fixtureDir, + builder: "rolldown", + imports: { + presets: [ + { + from: "export-subpath-pkg/h3", + imports: ["useH3"], + }, + ], + }, + }); + + await writeTypes(nitro); + + const declarationPath = join( + fixtureDir, + "node_modules", + ".nitro", + "types", + "nitro-imports.d.ts" + ); + const generated = readFileSync(declarationPath, "utf8"); + const match = generated.match(/typeof import\('([^']*export-subpath-pkg[^']*)'\)/); + expect( + match, + `expected import() referencing export-subpath-pkg in:\n${generated}` + ).toBeTruthy(); + + const resolvedTypePath = resolve(dirname(declarationPath), `${match![1]!}.d.mts`); + expect(existsSync(resolvedTypePath), `unresolvable type import: ${resolvedTypePath}`).toBe( + true + ); + expect(resolvedTypePath).toBe(join(pkgDir, "lib", "h3.d.mts")); + }); }); From 9ab15833ff03601cf49c2cf448a9023f7e028e85 Mon Sep 17 00:00:00 2001 From: onmax Date: Mon, 24 Aug 2026 18:57:36 +0000 Subject: [PATCH 2/3] fix(types): preserve resolved export target extensions --- src/build/types.ts | 13 ++++++++----- test/unit/types-imports.test.ts | 29 +++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/build/types.ts b/src/build/types.ts index 88575368bd..dee6bc2652 100644 --- a/src/build/types.ts +++ b/src/build/types.ts @@ -66,6 +66,7 @@ export async function writeTypes(nitro: Nitro) { continue; } let path = resolveAlias(from, nitro.options.alias); + let keepResolvedExtension = false; if (!isAbsolute(path)) { const resolvedPath = resolveModulePath(from, { try: true, @@ -81,14 +82,16 @@ export async function writeTypes(nitro: Nitro) { } else { const subpath = await lookupNodeModuleSubpath(resolvedPath); const subpathPath = subpath && subpath !== "./" && join(dir, name, subpath); - path = - subpathPath && existsSync(subpathPath) && !(await isDirectory(subpathPath)) - ? subpathPath - : resolvedPath; + if (subpathPath && existsSync(subpathPath) && !(await isDirectory(subpathPath))) { + path = subpathPath; + } else { + path = resolvedPath; + keepResolvedExtension = Boolean(subpathPath); + } } } } - if (existsSync(path) && !(await isDirectory(path))) { + if (!keepResolvedExtension && existsSync(path) && !(await isDirectory(path))) { path = path.replace(/\.[a-z]+$/, ""); } if (isAbsolute(path)) { diff --git a/test/unit/types-imports.test.ts b/test/unit/types-imports.test.ts index 475304b1a5..0a1c3259d8 100644 --- a/test/unit/types-imports.test.ts +++ b/test/unit/types-imports.test.ts @@ -1,9 +1,16 @@ -import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { dirname, join, resolve } from "pathe"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "pathe"; import { describe, expect, it } from "vitest"; import { createNitro, writeTypes } from "nitro/builder"; +const tscPath = join( + dirname(fileURLToPath(import.meta.resolve("typescript/package.json"))), + "bin/tsc" +); + describe("writeTypes auto-import resolution", () => { const fixtureDir = mkdtempSync(join(tmpdir(), "nitro-types-")); @@ -109,10 +116,20 @@ describe("writeTypes auto-import resolution", () => { `expected import() referencing export-subpath-pkg in:\n${generated}` ).toBeTruthy(); - const resolvedTypePath = resolve(dirname(declarationPath), `${match![1]!}.d.mts`); - expect(existsSync(resolvedTypePath), `unresolvable type import: ${resolvedTypePath}`).toBe( - true + expect(match![1]).toBe("../../export-subpath-pkg/lib/h3.mjs"); + + execFileSync( + process.execPath, + [ + tscPath, + "--noEmit", + "--module", + "preserve", + "--moduleResolution", + "bundler", + declarationPath, + ], + { cwd: fixtureDir } ); - expect(resolvedTypePath).toBe(join(pkgDir, "lib", "h3.d.mts")); }); }); From 488c00b7dc5729945b91fc15574f68a3023bf0cd Mon Sep 17 00:00:00 2001 From: onmax Date: Mon, 24 Aug 2026 19:06:02 +0000 Subject: [PATCH 3/3] test(types): verify resolved declaration type --- test/unit/types-imports.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/unit/types-imports.test.ts b/test/unit/types-imports.test.ts index 0a1c3259d8..e77d27afe3 100644 --- a/test/unit/types-imports.test.ts +++ b/test/unit/types-imports.test.ts @@ -85,7 +85,7 @@ describe("writeTypes auto-import resolution", () => { }) ); writeFileSync(join(pkgDir, "lib", "h3.mjs"), "export function useH3() {}\n"); - writeFileSync(join(pkgDir, "lib", "h3.d.mts"), "export declare function useH3(): void\n"); + writeFileSync(join(pkgDir, "lib", "h3.d.mts"), 'export declare function useH3(): "resolved"\n'); const nitro = await createNitro({ rootDir: fixtureDir, @@ -118,6 +118,9 @@ describe("writeTypes auto-import resolution", () => { expect(match![1]).toBe("../../export-subpath-pkg/lib/h3.mjs"); + const checkPath = join(fixtureDir, "check.ts"); + writeFileSync(checkPath, 'const result: "resolved" = useH3()\n'); + execFileSync( process.execPath, [ @@ -127,7 +130,9 @@ describe("writeTypes auto-import resolution", () => { "preserve", "--moduleResolution", "bundler", + "--noImplicitAny", declarationPath, + checkPath, ], { cwd: fixtureDir } );