diff --git a/src/build/types.ts b/src/build/types.ts index 67eedfb262..70da2e533b 100644 --- a/src/build/types.ts +++ b/src/build/types.ts @@ -80,7 +80,15 @@ export async function writeTypes(nitro: Nitro) { path = resolvedPath; } else { const subpath = await lookupNodeModuleSubpath(resolvedPath); - path = subpath && subpath !== "./" ? join(dir, name, subpath) : resolvedPath; + const subpathFile = subpath && subpath !== "./" ? join(dir, name, subpath) : undefined; + if (subpathFile && !existsSync(subpathFile)) { + // `subpath` is an `exports` key rather than an on-disk path, so emit a + // bare specifier and let the package `exports` `types` condition resolve + // it. A relativised path would not point at a real file. + resolvedImportPathMap.set(from, join(name, subpath!)); + continue; + } + path = subpathFile ?? resolvedPath; } } } diff --git a/test/unit/types-imports.test.ts b/test/unit/types-imports.test.ts index fff34e37ad..2ef02df006 100644 --- a/test/unit/types-imports.test.ts +++ b/test/unit/types-imports.test.ts @@ -1,6 +1,7 @@ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join } from "pathe"; +import { resolveModulePath } from "exsolve"; +import { dirname, join } from "pathe"; import { describe, expect, it } from "vitest"; import { createNitro, writeTypes } from "nitro/builder"; @@ -63,4 +64,93 @@ describe("writeTypes auto-import resolution", () => { `specifier should not end at the package directory, got ${specifier}` ).toBe(false); }); + + it("emits a bare specifier for an `exports` subpath with no matching file on disk", async () => { + const { specifier } = await generateForPackage({ + name: "subpath-exports-pkg", + exports: { + ".": "./dist/index.js", + "./utils": { types: "./dist/utils.d.ts", import: "./dist/utils.js" }, + }, + files: { + "dist/index.js": "export {}\n", + "dist/utils.js": "export function useSubpathExports() { return true }\n", + "dist/utils.d.ts": "export declare function useSubpathExports(): boolean\n", + }, + from: "subpath-exports-pkg/utils", + imports: ["useSubpathExports"], + }); + + expect(specifier).toBe("subpath-exports-pkg/utils"); + }); + + it("emits a resolvable specifier for a subpath only reachable through wildcard `exports`", async () => { + const { specifier, rootDir } = await generateForPackage({ + name: "wildcard-exports-pkg", + exports: { + ".": "./dist/index.js", + "./drivers/*": "./dist/drivers/*.js", + }, + files: { + "dist/index.js": "export {}\n", + "dist/drivers/fs.js": "export function useWildcardDriver() { return true }\n", + "dist/drivers/fs.d.ts": "export declare function useWildcardDriver(): boolean\n", + }, + from: "wildcard-exports-pkg/drivers/fs", + imports: ["useWildcardDriver"], + }); + + const generatedTypesDir = join(rootDir, "node_modules", ".nitro", "types"); + expect( + resolveModulePath(specifier, { + try: true, + from: join(generatedTypesDir, "nitro-imports.d.ts"), + conditions: ["types", "import"], + extensions: ["", ".d.ts", ".js"], + }), + `specifier ${specifier} should be resolvable` + ).toBeTruthy(); + }); }); + +async function generateForPackage(options: { + name: string; + exports: unknown; + files: Record; + from: string; + imports: string[]; +}) { + const rootDir = mkdtempSync(join(tmpdir(), "nitro-types-")); + const pkgDir = join(rootDir, "node_modules", options.name); + mkdirSync(pkgDir, { recursive: true }); + writeFileSync( + join(pkgDir, "package.json"), + JSON.stringify({ name: options.name, type: "module", exports: options.exports }) + ); + for (const [file, contents] of Object.entries(options.files)) { + mkdirSync(dirname(join(pkgDir, file)), { recursive: true }); + writeFileSync(join(pkgDir, file), contents); + } + + mkdirSync(join(rootDir, "server"), { recursive: true }); + writeFileSync(join(rootDir, "server", "package.json"), '{ "type": "module" }\n'); + + const nitro = await createNitro({ + rootDir, + builder: "rolldown", + imports: { + presets: [{ from: options.from, imports: options.imports }], + }, + }); + + await writeTypes(nitro); + + const generated = readFileSync( + join(rootDir, "node_modules", ".nitro", "types", "nitro-imports.d.ts"), + "utf8" + ); + const match = generated.match(new RegExp(`typeof import\\('([^']*${options.name}[^']*)'\\)`)); + expect(match, `expected import() referencing ${options.name} in:\n${generated}`).toBeTruthy(); + + return { specifier: match![1]!, generated, rootDir }; +}