diff --git a/src/index.ts b/src/index.ts index d46aa45..cf9359b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1529,13 +1529,22 @@ export default nativeModule; const absolutePath = path.resolve(path.dirname(id), relativePath); if (fs.existsSync(absolutePath)) { - const info = registerNativeFile(absolutePath); - replacements.push({ - start: literalNode.start, - end: literalNode.end, - value: `"./${info.hashedFilename}"`, - }); - modified = true; + if ( + isIdentifier(calleeNode) && + (calleeNode.name === "require" || customRequireVars.has(calleeNode.name)) + ) { + // Replace the whole require() so CommonJS importers get the + // native exports rather than the virtual module's namespace. + processNodeFile(absolutePath, node); + } else { + const info = registerNativeFile(absolutePath); + replacements.push({ + start: literalNode.start, + end: literalNode.end, + value: `"./${info.hashedFilename}"`, + }); + modified = true; + } } } } @@ -1657,14 +1666,7 @@ export default nativeModule; const nodeFilePath = resolveNpmPackageNodeFile(packageName, path.dirname(id)); if (nodeFilePath) { - const info = registerNativeFile(nodeFilePath); - const literalNode = node.arguments[0] as LiteralNode; - replacements.push({ - start: literalNode.start, - end: literalNode.end, - value: `"./${info.hashedFilename}"`, - }); - modified = true; + processNodeFile(nodeFilePath, node); } } } @@ -1695,17 +1697,7 @@ export default nativeModule; const result = findPlatformSpecificNativePackage(prefix, path.dirname(id)); if (result) { - const { nodeFilePath } = result; - const info = registerNativeFile(nodeFilePath); - const templateNode = node.arguments[0]; - if (templateNode.start !== undefined && templateNode.end !== undefined) { - replacements.push({ - start: templateNode.start, - end: templateNode.end, - value: `"./${info.hashedFilename}"`, - }); - modified = true; - } + processNodeFile(result.nodeFilePath, node); } } } diff --git a/test/napi-rs.test.ts b/test/napi-rs.test.ts index 92b0cb9..c8ce5de 100644 --- a/test/napi-rs.test.ts +++ b/test/napi-rs.test.ts @@ -179,7 +179,7 @@ describe("NAPI-RS Support", () => { const joinFilename = joinMatch![1]; // Extract hashed filename from require call - const requireMatch = result.code.match(/require\(['"]\.\/([^'"]+)['"]\)/); + const requireMatch = result.code.match(/require\(['"]\.\/([^'"?]+)/); expect(requireMatch).toBeDefined(); const requireFilename = requireMatch![1]; @@ -747,7 +747,7 @@ describe("NAPI-RS Support", () => { expect(transformResult).toBeDefined(); // Extract hashed filename from transformed code - const match = transformResult.code.match(/require\("\.\/([^"]+\.node)"\)/); + const match = transformResult.code.match(/require\("\.\/([^"?]+\.node)/); expect(match).toBeDefined(); const hashedFilename = match![1]; @@ -883,7 +883,7 @@ describe("NAPI-RS Support", () => { expect(transformResult.code).not.toContain("`@libsql/"); // Extract the hashed filename and verify resolveId returns virtual module ID - const match = transformResult.code.match(/require\("\.\/([^"]+\.node)"\)/); + const match = transformResult.code.match(/require\("\.\/([^"?]+\.node)/); expect(match).toBeDefined(); const resolveResult = await (plugin.resolveId as any).call( diff --git a/test/vite-static-import-compat.test.ts b/test/vite-static-import-compat.test.ts index af5780f..6cd7d0d 100644 --- a/test/vite-static-import-compat.test.ts +++ b/test/vite-static-import-compat.test.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { pathToFileURL } from "node:url"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { build as buildVite7 } from "vite7"; import vite7Package from "vite7/package.json"; @@ -133,5 +134,76 @@ describe.each(viteVersions)( expectNativeAddonOutput(result); }); + + it.each([ + { name: "relative path", source: "./build/Release/addon.node", comment: "" }, + // NAPI-RS loaders mention their .node filename, which the transform filter requires. + { name: "package", source: "native-pkg", comment: "// Loads native-pkg/addon.node" }, + ])( + "returns the native exports from a CommonJS $name require in ESM output", + async ({ comment, source }) => { + const entryPath = path.join(tempDir, "index.mjs"); + const releaseDir = path.join(tempDir, "build", "Release"); + fs.mkdirSync(releaseDir, { recursive: true }); + fs.writeFileSync(path.join(releaseDir, "addon.node"), Buffer.from("fake native module")); + const packageDir = path.join(tempDir, "node_modules", "native-pkg"); + fs.mkdirSync(packageDir, { recursive: true }); + fs.writeFileSync(path.join(packageDir, "addon.node"), Buffer.from("fake package module")); + fs.writeFileSync( + path.join(packageDir, "package.json"), + JSON.stringify({ main: "addon.node", name: "native-pkg" }), + ); + fs.writeFileSync( + path.join(tempDir, "lib.cjs"), + ` + ${comment} + const addon = require("${source}"); + class Wrapped extends addon.Base {} + module.exports = { hasDefault: "default" in addon, wrapped: new Wrapped().marker }; + `, + ); + fs.writeFileSync(entryPath, "import lib from './lib.cjs'; export default lib;\n"); + + // Replace createRequire so the bundle can run without a real native binary. + const fakeNodeModule = { + enforce: "pre" as const, + load(id: string) { + if (id === "\0fake-node-module") { + return `class Base { marker = "native"; } +export function createRequire() { + return () => ({ Base }); +}`; + } + }, + name: "fake-node-module", + resolveId(id: string) { + if (id === "node:module" || id === "module") return "\0fake-node-module"; + }, + }; + + const outDir = path.join(tempDir, "dist"); + await build({ + build: { + outDir, + rollupOptions: { + input: entryPath, + output: { entryFileNames: "index.mjs", format: "es" }, + }, + ssr: true, + }, + configFile: false, + logLevel: "silent", + plugins: [fakeNodeModule, nativeFilePlugin()], + root: tempDir, + ssr: { noExternal: true }, + }); + + const bundledModule = await import( + /* @vite-ignore */ pathToFileURL(path.join(outDir, "index.mjs")).href + ); + + expect(bundledModule.default).toEqual({ hasDefault: false, wrapped: "native" }); + }, + ); }, );