Skip to content

Return native exports from CommonJS .node requires in ESM output - #29

Merged
biw merged 1 commit into
mainfrom
biw/fix-issue-28
Sep 26, 2026
Merged

biw merged 1 commit into
mainfrom
biw/fix-issue-28

Conversation

@biw

@biw biw commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Fixes #28.

Problem

When a CommonJS module did require("./build/Release/addon.node") (Pattern 5) or require("pkg") / require(`@scope/${target}`) resolving to a .node file (Patterns 7/8), the plugin only rewrote the string literal. Vite then resolved the hashed path to the plugin's virtual ESM wrapper (export default nativeModule), so a CJS require() got the module namespace, not the addon:

  • Vite 8 / Rolldown ignores syntheticNamedExports, so named properties are missing entirely:
    var pkcs11 = (init_pkcs11(), __toCommonJS(pkcs11_exports));
    class PKCS11 extends pkcs11.PKCS11 {} // TypeError: Class extends value undefined
  • Vite ≤7 / Rollup happened to work via syntheticNamedExports, but the value carried an extra default key, diverging from Node semantics.

The node-gyp-build, bindings, and swift-node patterns already replace the whole call via processNodeFile, which appends .default for ESM output. Patterns 5, 7, and 8 were the outliers.

Fix

  • Pattern 5: use processNodeFile when the callee is require or a tracked createRequire variable. Other callees keep the literal-only rewrite, so e.g. someFn("./addon.node") isn't turned into an addon load.
  • Patterns 7 & 8: already gated on a require-like callee; now use processNodeFile.

Resulting output in ESM builds:

var pkcs11 = (init_pkcs11(), __toCommonJS(pkcs11_exports)).default;

Behavior change (intentional)

On Vite ≤7, CJS code previously received a Rollup interop wrapper instead of the addon itself. That was wrong compared to Node, and this PR drops it:

Inside the CJS module Before After
Object.keys(addon) ['default', ...] addon's own keys only
addon === realNativeObject false true
addon.default the addon undefined (same as Node)
mutations / later-added props not shared with the real addon shared

Only code that explicitly read require("./x.node").default (which is undefined in plain Node) would notice. ESM importers of CJS modules and CJS output builds are unchanged.

Tests

  • New runtime test in test/vite-static-import-compat.test.ts, run against both Vite 7 and 8, covering relative-path and package requires. It builds a CJS lib that does class X extends addon.Base, imports the built bundle, and asserts the exports have no default key. It failed before the fix on both versions (Vite 8 with the exact error from the issue).
  • Relaxed three regexes in test/napi-rs.test.ts that assumed the bare require("./hash.node") shape; ESM builds now emit require("./hash.node?native-require=…").default.
  • Verified manually against the issue repro (pkcs11js, Vite 8.3.0): the build now runs and prints the addon exports.
  • pnpm test (209 passing) and pnpm lint pass.

Relative and package requires only had their specifier rewritten, so
CommonJS importers received the virtual module's namespace instead of the
addon. Route them through processNodeFile like the other loader patterns.

Fixes #28
@biw
biw merged commit 766b88b into main Sep 26, 2026
2 checks passed
@biw
biw deleted the biw/fix-issue-28 branch September 26, 2026 00:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CJS/ESM compatibility issue

1 participant