Skip to content

Add a fixture registering via the deprecated napi_module_register - #446

Merged
kraenhansen merged 4 commits into
nextfrom
claude/deprecated-module-register-fixture
Aug 13, 2026
Merged

Add a fixture registering via the deprecated napi_module_register#446
kraenhansen merged 4 commits into
nextfrom
claude/deprecated-module-register-fixture

Conversation

@kraenhansen

@kraenhansen kraenhansen commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #445, which made the host load addons that register themselves by calling napi_module_register while their library loads. Nothing in the repo exercised that path — every other addon here exports napi_register_module_v1, which the loader finds first, so the fallback was never reached.

tests/module-register exports no napi_register_module_v1 symbol, so it only loads if the fallback works. Failure mode is a hard one: requireNodeAddon throws "no napi_register_module_v1 export and no registered napi_module", and the test fails.

The constructor is hand-rolled:

__attribute__((constructor)) static void RegisterAddon(void) {
  napi_module_register(&addon_module);
}

because node_api.h no longer ships a macro that emits one — NAPI_MODULE_X, the historical spelling, is now an alias of the symbol-based NAPI_MODULE. #104 solved this by vendoring a 269-line node_api_deprecated.h; four lines seemed better.

Closes #3.

Leaner than its siblings

The fixture has no binding.gyp, no "gypfile": true, and no bindings dependency.

Nothing builds it from gyp — cmake-rn drives the CMake project directly — and the reason the other fixtures carry a gyp file (staying close to the upstream sources they were derived from) does not apply to an addon written here. CMakeLists.txt is therefore hand-maintained; gyp-to-cmake skips the directory, and generate-root-project still picks it up, since it looks for CMake projects rather than gyp files.

bindings earns its place when an addon has to be found across the several output directories node-gyp might have used. This one is emitted to a single known location, so addon.js does a plain require("./build/RelWithDebInfo/addon.node") — matching what ferric-example already does — which has the side benefit of exercising the Babel plugin's ordinary require path rather than its bindings special case.

The "test": "node addon.js" script went too: without a node-gyp build there is no host-loadable .node for it to require, so it could only ever have failed. Happy to put it back if you'd rather keep the shape uniform.

Verification

  • Built the addon standalone and confirmed with nm -D that it exports neither napi_register_module_v1 nor node_api_module_get_api_version_v1 — i.e. it really does force the fallback.
  • Loaded it into Node.js 24 via process.dlopen and ran the assertion, so the fixture is well-formed independently of our host.
  • Ran the Babel plugin over addon.js against a directory laid out like a completed build, and confirmed it rewrites to require("react-native-node-api").requireNodeAddon("module-register-test--addon").
  • generate-root-project lists tests/module-register among its sub-projects after the gyp file was removed.
  • tsc --build, eslint, prettier --check clean; pnpm install leaves the lockfile untouched.

The fixture itself runs on device, so this needs the iOS and Android jobs — hence the labels.

Notes

  • No changeset: @react-native-node-api/node-addon-examples is private and no published package changes behavior.
  • I dropped a node_api_get_module_file_name assertion I had initially added here. Hermes sets env->moduleFileName_ unconditionally in hermes_napi_load_module, so it holds on our host, but Node.js only populates it when loading through require — via process.dlopen it returns "" for symbol-based and deprecated registration alike. An assertion that only holds in one of the two runtimes a fixture can run in seemed worse than no assertion. That claim from Load addons through Hermes' hermes_napi_load_module #445 stays unverified.
  • Unrelated to this PR but worth recording: Hermes never clears lastRegisteredModule after a load. Node clears its equivalent (nodejs/node@a60056d) precisely so a failed or symbol-less load cannot pick up a stale registration from an earlier addon. Two deprecated addons in one app, or one that fails to load after another registered, can therefore resolve to the wrong init function. Worth folding into the upstream issue drafted in Support Node-API modules declaring their "Node-API version" #4.

The host gained support for addons that register themselves by calling
napi_module_register while their library loads (#445), but nothing in the
repo exercises that path — every other addon here exports
napi_register_module_v1, which the loader finds first.

This addon exports no such symbol, so it only loads if the fallback works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ugFE6vmMUVMTuoupvhMhX
@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) labels Aug 13, 2026 — with Claude
claude added 3 commits August 13, 2026 19:39
The check workflow only re-evaluates its label conditions on opened,
synchronize and reopened events, so the labels added after opening this
PR need a push to take effect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ugFE6vmMUVMTuoupvhMhX
Nothing builds this from binding.gyp — cmake-rn drives the CMake project
directly. The sibling fixtures keep theirs to stay close to upstream
sources they were derived from, which does not apply to an addon written
here.

CMakeLists.txt is now hand-maintained rather than regenerated by
gyp-to-cmake, which skips the directory now that there is no binding.gyp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ugFE6vmMUVMTuoupvhMhX
The bindings package earns its place when an addon has to be found across
the several output directories node-gyp might have used. This addon is
built by cmake-rn to one known location, so a plain require says the same
thing with one less dependency — and it exercises the Babel plugin's
ordinary require path rather than its bindings special case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ugFE6vmMUVMTuoupvhMhX
@kraenhansen
kraenhansen merged commit 0a29fbd into next Aug 13, 2026
16 checks passed
@kraenhansen
kraenhansen deleted the claude/deprecated-module-register-fixture branch August 13, 2026 22:15
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.)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support module registration via calls to napi_module_register

2 participants