diff --git a/src/build/types.ts b/src/build/types.ts index 67eedfb262..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, @@ -80,11 +81,17 @@ 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); + 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 fff34e37ad..e77d27afe3 100644 --- a/test/unit/types-imports.test.ts +++ b/test/unit/types-imports.test.ts @@ -1,9 +1,16 @@ +import { execFileSync } from "node:child_process"; import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join } 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-")); @@ -63,4 +70,71 @@ 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(): "resolved"\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(); + + 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, + [ + tscPath, + "--noEmit", + "--module", + "preserve", + "--moduleResolution", + "bundler", + "--noImplicitAny", + declarationPath, + checkPath, + ], + { cwd: fixtureDir } + ); + }); });