Skip to content

Build addon paths in TypeScript and expose dlopen(module, filename) - #500

Merged
kraenhansen merged 3 commits into
nextfrom
claude/zen-keller-9d9536
Nov 1, 2026
Merged

kraenhansen merged 3 commits into
nextfrom
claude/zen-keller-9d9536

Conversation

@kraenhansen

Copy link
Copy Markdown
Collaborator

Closes #498.

What changed

  • Native: CxxNodeApiHostModule no longer exposes requireNodeAddon. It exposes dlopen(module, filename) instead: it creates a fresh napi_env, calls hermes_napi_load_module(env, filename) and assigns the result to module.exports. It has no cache, so a second call runs the addon's init again, as in Node. nodeAddons_ and the #if __APPLE__ / __ANDROID__ path construction are gone.
  • TypeScript (src/react-native/index.ts):
    • New dlopen(module, filename) export, with the same semantics as Node's process.dlopen and no flags.
    • requireNodeAddon(libraryName) builds the filename itself (lib<name>.so on Android, @rpath/<name>.framework/<name> on ios / macos / visionos, and it throws on any other platform). It calls dlopen and caches module.exports in a module-level Map. A failed load is not cached. The error message is unchanged: Failed to load '<name>' addon from '<path>': <reason>.
    • Its library name argument and the Babel output are unchanged.
  • Docs (HOW-IT-WORKS.md) and a minor changeset.

How exports reach module.exports

Hermes has no public bridge between napi_value and jsi::Value. The value is set on the global as RN$NodeAddon_<env ptr> through Node-API, read back through JSI, and then removed with napi_delete_property. Previously that global stayed set for the whole life of the runtime. Errors are passed back the same way: the exception Hermes left pending is rethrown as the original JS error, not flattened into a string in C++. If no exception is pending, an Error("Node-API status N") is created instead.

Cache is per runtime

The cache is a module-scoped Map in the RN entrypoint. Each JS runtime (for example after a reload) evaluates the bundle again, so it gets its own cache, as nodeAddons_ on the per-runtime TurboModule did before.

Known differences / notes (from the issue)

  • hermes_napi_load_module creates its own exports object, so the module.exports we pass in is overwritten, not handed to init.
  • Each dlopen call creates a new napi_env that lives until runtime teardown.
  • Calling dlopen directly a second time is now possible, so the reload case from NAPI: hermes_napi_load_module can run another addon's init through lastRegisteredModule meta/hermes#2280 (the wrong init can run for addons that use napi_module_register) is no longer hidden for those callers. requireNodeAddon still hides it through its cache.
  • requireNodeAddon<T> keeps its generic return type through an overload signature, so no as assertion is needed. Before this PR, the native Spec generic did the same job.

Testing

  • tsc --build tsconfig.react-native.json, eslint and prettier --check pass.
  • CxxNodeApiHostModule.cpp passes a clang++ -fsyntax-only check against React Native's JSI/TurboModule headers and the node-api-headers headers.
  • Not run: iOS/Android device builds and the test app. These need the Apple 🍎 / Android 🤖 jobs or a local run.

Generated by Claude Code

Move platform path construction and the exports cache out of
CxxNodeApiHostModule into TypeScript, replacing the native
requireNodeAddon with a dlopen(module, filename) matching Node's
process.dlopen. requireNodeAddon is rebuilt on top of it.

Refs #498

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2xSE4KqJbTcKKr4FKKUX2
@kraenhansen kraenhansen added Apple 🍎 Anything related to the Apple platform (iOS, macOS, Cocoapods, Xcode, XCFrameworks, etc.) Android 🤖 Anything related to the Android platform (Gradle, NDK, Android SDK) Host 🏡 Our `react-native-node-api-modules` package Linking 🔗 Discovering and copying prebuilds from packages into the host labels Nov 1, 2026 — with Claude
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2xSE4KqJbTcKKr4FKKUX2
@kraenhansen
kraenhansen changed the base branch from main to next November 1, 2026 21:54
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2xSE4KqJbTcKKr4FKKUX2
@kraenhansen kraenhansen self-assigned this Nov 1, 2026
@kraenhansen
kraenhansen marked this pull request as ready for review November 1, 2026 22:01
@kraenhansen
kraenhansen merged commit 29ca6a3 into next Nov 1, 2026
18 checks passed
@kraenhansen
kraenhansen deleted the claude/zen-keller-9d9536 branch November 1, 2026 22:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Android 🤖 Anything related to the Android platform (Gradle, NDK, Android SDK) Apple 🍎 Anything related to the Apple platform (iOS, macOS, Cocoapods, Xcode, XCFrameworks, etc.) Host 🏡 Our `react-native-node-api-modules` package Linking 🔗 Discovering and copying prebuilds from packages into the host

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build addon paths in TypeScript and expose dlopen(module, filename)

2 participants