Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/build/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Expand Down
92 changes: 91 additions & 1 deletion test/unit/types-imports.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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<string, string>;
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 };
}
Loading