Adopt Hermes' first-party Node-API (static_h) - #372
Draft
kraenhansen wants to merge 18 commits into
Draft
Conversation
kraenhansen
force-pushed
the
kh/adopt-static-h-node-api
branch
from
July 13, 2026 20:31
b101c95 to
e9dc7e6
Compare
kraenhansen
force-pushed
the
kh/adopt-static-h-node-api
branch
from
July 16, 2026 18:43
6317f7c to
2338534
Compare
1 task
kraenhansen
force-pushed
the
kh/adopt-static-h-node-api
branch
from
July 20, 2026 05:52
96ecbea to
1675ecc
Compare
Contributor
I think I can test this part on real module when it's ready. Asynchronous bindings were added in the previous update of our sdk. |
kraenhansen
force-pushed
the
kh/adopt-static-h-node-api
branch
from
July 24, 2026 12:03
1675ecc to
6d9a70d
Compare
This was referenced Jul 24, 2026
1 task
Begin migrating off the kraenhansen/hermes fork + JSI-patching path toward Hermes' first-party Node-API (the static_h branch). - vendor-hermes: shallow-fetch facebook/hermes at pinned static_h SHA 0ae42446d1ae669508368b0a18e60c789f76735d; drop the JSI-header copy step - patch-hermes.rb: rely on REACT_NATIVE_OVERRIDE_HERMES_DIR alone to trigger build-from-source; drop the no-op BUILD_FROM_SOURCE var and the obsolete RCT_USE_PREBUILT_RNCORE / JSI-patch guard - CxxNodeApiHostModule: stub env=nullptr (real env arrives in Phase 2 via hermes_napi_create_env) - bump react-native to 0.87.0-nightly-20260529-88857d22f (+ test-app deps, react-native-test-app 5.x); regenerate lockfile - RN 0.87 fallout: add @types/babel__core, fix test-app tsconfig extends for the tightened @react-native/typescript-config exports map, delete the podspec test asserting the removed guard Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
react-native-test-app 5.x generates the app's ReactTestApp.xcodeproj under the nearest node_modules, which in a workspace is the app-local node_modules (apps/test-app/node_modules/.generated), not the hoisted root. The workspace can also accumulate stale references to a project under a different node_modules. findXcodeProject took the first fileRef unconditionally, which could be the stale (non-existent) reference or the Pods project. Resolve every app project reference and pick the first whose project.pbxproj exists on disk, ignoring Pods.xcodeproj. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Bump @rnx-kit/metro-config to ^2.2.4: 2.1.1 called metro-config's
exclusionList as a bare function, but Metro 0.84 changed that module to a
{ default } export, breaking `react-native start`.
- Gradle wrapper bumped to 9.3.1 by react-native-test-app 5.x's
configureGradleWrapper during pod install (RN 0.87 alignment).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the `env = nullptr` stub in CxxNodeApiHostModule with a real
Node-API environment: cast the JSI runtime to `IHermes`, read the
underlying `vm::Runtime*` via `getVMRuntimeUnsafe()`, and create the env
with `hermes_napi_create_env(vm, nullptr)`. The env is owned by the
runtime and cached on the module (shared across all addons).
This flips the Phase 1 baseline abort (`assert(status == napi_ok)` right
after `napi_create_object(env=nullptr, …)`) green: with
`MOCHA_REMOTE_CONTEXT=allTests` the iOS-sim suite now reports 14 passing
(node-addon-examples getting-started incl. the Rust ferric addon,
buffers, async, and a js-native-api node-test).
Linking note: the RN `hermesvm` framework force-loads `hermesNapi`, and
the public `hermes_napi_*` entry points are exported from it as long as
Hermes is built from a checkout that includes facebook/hermes #2044
("Export public hermes_napi entry points with NAPI macros") — which the
pinned SHA (0ae42446) already contains. No pod-side linker surgery or
source patching is required; just ensure the vendored checkout is
actually at the pinned SHA (a stale pre-#2044 checkout is what stripped
the symbol during bring-up).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All Node-API symbols are now sourced from Hermes' hermesNapi, so the old engine (js_native_api → libhermes.so) / runtime (node_api → libnode-api-host.so) distinction and the hand-maintained IMPLEMENTED_RUNTIME_FUNCTIONS allow-list are obsolete. - weak-node-api: getNodeApiFunctions defaults to v10 and no longer computes the dead `kind`/`libraryPath` fields; CMake compiles the generated weak_node_api.cpp at NAPI_VERSION=10 (145 → 155 symbols, adding the v9/v10 node_api_* surface). - generate-injector.mts: bind every symbol (no filter) and emit `#include <Versions.hpp>` first so the injector TU also compiles at v10. - Versions.hpp: guarded bump to NAPI_VERSION 10. Regenerated (gitignored) WeakNodeApiInjector.cpp + weak-node-api/generated now expose all 155 symbols incl. TSFN and napi_make_callback. Verified: build, prettier, lint, workspace unit tests, and the weak-node-api native build + ctest all pass. iOS e2e pending (rides the cold re-vendor). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The clean Hermes build at the pinned SHA does NOT export hermes_napi_create_env (and the other hermes_napi_* entry points). They are declared in API/napi/hermes_napi.h with NAPI_EXTERN (visibility "default") but — unlike the sibling js_native_api.h / node_api.h headers — without any extern "C" wrapping, so they get C++ linkage. The mangled C++ symbols stay out of the framework's dynamic export table under Hermes' global -fvisibility=hidden, and a from-scratch build fails at the app link with "Undefined symbol: hermes_napi_create_env". vendor-hermes now wraps the hermes_napi.h declarations in EXTERN_C_START / EXTERN_C_END (both available via the node_api.h include), giving the entry points C linkage so they export under their unmangled C names. This mirrors the upstream fix in facebook/hermes#2106. The patch is idempotent (guarded on EXTERN_C_START) and asserts its anchors exist so a future Hermes bump fails loudly rather than silently no-op'ing. Also ignore **/build-tests/** in ESLint (CMake writes compiler_depend.ts dependency files there that aren't real TypeScript). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapse the single-argument `.replace()` call in patchHermesNapiVisibility onto one line to satisfy prettier:check (fixup for the hermes_napi patch). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebased onto main after the npm->pnpm migration (#381). The original PR's two package-lock.json maintenance commits (restore public registry URLs, restore pruned optional platform binaries) are dropped: both addressed npm-specific lockfile problems that no longer exist under pnpm. Regenerate pnpm-lock.yaml against the RN 0.87 nightly / react-native-test-app 5.x / @rnx-kit/metro-config bumps so the lockfile matches the workspace manifests. Verified with pnpm install --frozen-lockfile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TVfanKvtyfSsoMgZv3DJtY
Move the pinned Hermes commit forward from 0ae42446 to efcf68e2 on the static_h branch (a descendant, 18 commits ahead). The only relevant change in that range is facebook/hermes#2106 "give hermes_napi.h public API C linkage", which wraps the public hermes_napi_* entry points in extern "C". That is exactly the fix we were applying locally after cloning: without C linkage the mangled hermes_napi_create_env symbol stayed out of the framework export table under Hermes' global -fvisibility=hidden. Now that the fix is upstream at the pinned commit, drop patchHermesNapiVisibility and its header-anchor constants entirely — the vendored checkout exports the entry points as-is. No commit in the bumped range touches getVMRuntimeUnsafe or the IHermes JSI interface we depend on, so the unstable-accessor rationale for pinning still holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TVfanKvtyfSsoMgZv3DJtY
The pinned Hermes commit now includes facebook/hermes#2106, which wraps the public hermes_napi_* entry points in extern "C". Hermes therefore exports the unmangled C symbol for hermes_napi_create_env. CxxNodeApiHostModule forward-declares that entry point (to avoid including Hermes' node_api.h) but did so with C++ linkage, so it referenced the mangled name. After the pin bump the two no longer matched and the iOS app failed to link with "Undefined symbol: hermes_napi_create_env". Wrap the forward declaration in extern "C" so the reference resolves to the exported unmangled symbol. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TVfanKvtyfSsoMgZv3DJtY
RN 0.87 bumps the Gradle wrapper to 9.x, which removed Project.exec(). The
linkNodeApiModules task used the bare `exec {}` closure in its doLast action,
failing every Android build (and gradle.test.ts on all platforms) with
"Could not find method exec()". Inject the ExecOperations service via an
@Inject-annotated interface and call injectedExecOps.execOps.exec {} instead.
Greens the ubuntu and macOS unit-test lanes. Windows surfaces a separate,
pre-existing RN 0.87 / Gradle 9 issue (missing react-native/tmp projectDir)
tracked separately.
* android: patch RN settings.gradle.kts /tmp projectDir for Windows
The Windows unit-test lane failed configuring the React Native build-from-
source composite build:
Configuring project ':packages:react-native' without an existing directory
is not allowed. The configured projectDirectory '...\react-native\tmp'
does not exist
React Native's own settings.gradle.kts declares the intermediate container
projects :packages and :packages:react-native with projectDir = file("/tmp"),
purely to satisfy Gradle 9's rule that every project in a path have an existing
folder. "/tmp" exists on the posix CI hosts but on Windows it is not an
absolute path, so Gradle resolves it to a non-existent <react-native>\tmp and
the build fails before any task runs. This is why only windows-latest was red
while ubuntu and macOS passed.
Add a pnpm patch replacing file("/tmp") with
file(System.getProperty("java.io.tmpdir", "/tmp")): the JVM temp dir is "/tmp"
on posix and %TEMP% on Windows, both of which always exist. Remove the patch
once React Native stops hardcoding "/tmp" upstream.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TVfanKvtyfSsoMgZv3DJtY
* android: point RN /tmp patch at the merged upstream fix
The upstream fix landed on react-native main as 908872a6 (2026-07-28,
react/react-native#57706), after the 0.87 branch cut — so 0.87-stable
does not carry it. Record that in the patch comment so the removal gate is
a concrete react-native version rather than "once upstream fixes it".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
kraenhansen
force-pushed
the
kh/adopt-static-h-node-api
branch
from
August 9, 2026 19:49
8fc7527 to
07d9a52
Compare
AGP 9 ships built-in Kotlin support and enables it by default, which registers the `kotlin` extension itself. Applying `kotlin-android` on top of that fails the consumer's build with "Cannot add extension with name 'kotlin'", so any consumer who has migrated off the `builtInKotlin=false` opt-out currently cannot build against this package. Gate the plugin on the AGP major version and the consumer's opt-out, so the library works both for consumers still on AGP 8 (or opted out while they migrate) and for those already on built-in Kotlin. React Native's own ReactAndroid no longer applies the Kotlin plugin either, as of 0.87. Reuses the `com.android.Version` idiom already used by supportsNamespace(). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moves off the 0.87.0-nightly-20260529 pin onto the 0.87 release
candidate. The motivating change is AGP: the nightly still resolved AGP
8.12, while AGP 9.2.1 landed on the 0.87 line in mid-June. AGP 9 is what
react-native-test-app assumes for React Native >= 0.87 (it forces Gradle
9.4.1 and then uses the built-in Kotlin `kotlin {}` extension), so the
test app could not configure against the old pin.
The Windows `/tmp` projectDir patch is unchanged — settings.gradle.kts is
byte-identical between the two versions (same blob 2036e0f), so only the
file name and the patchedDependencies key move. The fix for it is still
main-only, so the patch stays until we are on 0.88+.
Also switches the two React Native facing tsconfigs to nodenext module
resolution. 0.87.0-rc.4 drops react-native's top-level `types` field and
flips the default `types` export condition to the generated strict API,
neither of which the node10 resolution inherited from
@tsconfig/react-native can see — the package stopped resolving entirely
(TS2688). @tsconfig/react-native is stale at every published version
through 3.0.9, so there is nothing to bump there. Emit is unaffected:
both projects still produce CommonJS. The strict API exports TurboModule
and TurboModuleRegistry, and still references react-native's globals, so
console/require stay typed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With React Native 0.87 the test app builds against AGP 9.2.1, where built-in Kotlin is enabled by default. Nothing in the build needs the Kotlin plugin any more: ReactAndroid dropped it upstream, react-native-test-app's modules are gated on it, and react-native-node-api now only applies it when built-in Kotlin is unavailable. So unlike the React Native app template, we do not set `android.builtInKotlin=false`. The new DSL is a different matter and stays opted out: both of react-native-test-app's Gradle modules still use the old one, and that is third-party code. AGP 10 removes this opt out, so it is tracked in #389 along with the upstream code that has to migrate first. Also pins the Gradle wrapper at 9.4.1, which react-native-test-app rewrites it to at run time for React Native >= 0.87 — pinning it ourselves keeps CI from building with a dirty working tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
React Native 57706 ("Fix build-from-source on Windows: use JVM temp dir
instead of hardcoded /tmp", 908872a6, 2026-07-28) landed on main after
the 0.87 branch cut, so it ships on the 0.88 line and not in 0.87.0-rc.4.
Verified in the published artifact, not just the tree: the tarball for
0.88.0-nightly-20260809-db662caea carries the fix in settings.gradle.kts,
the exact file (and path) we were patching. Our patch is now redundant.
Dropping it is what makes Android build. Patching a dependency makes pnpm
encode the patch hash into the virtual store directory as
`..._patch_hash=<hash>`, and prefab — which the Android Gradle plugin runs
over react-native's package directory — parses a positional path
containing `=` as an option name and dies with "Error: no such option".
That is google/prefab#187, open since March and
hitting every pnpm user with a patched dependency. With no patched
dependencies there is no `=` in the store, so the bug goes untriggered.
Requires react-native-test-app >= 5.4.8, which widened its peer range to
`0.76 - 0.87 || >=0.88.0-0 <0.88.0` — a prerelease window covering exactly
these nightlies. 5.4.5 did not accept 0.88 at all, so the floor moves up.
Everything the AGP 9 work depends on is unchanged on this line: AGP 9.2.1,
Kotlin 2.2.0, and react-native-test-app still resolves Gradle 9.4.1 for
0.88, matching the pinned wrapper.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
React Native renamed the prefab module published by `hermes-engine` from `libhermes` to `hermesvm` between 0.81 and 0.83 — the Android counterpart of the `hermesvm` framework this branch already links against on Apple platforms. This CMakeLists has been on `libhermes` since #308, which was correct while the repo targeted 0.81, and stayed behind when this branch jumped to 0.87/0.88. Without it CMake fails to configure: Target "node-api-host" links to target "hermes-engine::libhermes" but the target was not found. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fa4424b deliberately left `android.builtInKotlin` unset, on the reasoning that nothing in the build still needs the Kotlin plugin. That reasoning was wrong, and only a real Android build showed it: ComponentActivity.kt:33:9 Unresolved reference 'ComponentActivityDelegate' react-native-test-app's app module pulls in version-specific sources with `main.java.srcDirs += [...]` — src/reactactivitydelegate-0.75/java, src/reactapplication-0.76/java, src/camera/java and others. The Kotlin plugin compiles the Kotlin in those directories; AGP's built-in Kotlin only picks up the standard source directories, so every symbol defined in an added one goes unresolved (`testApp`, `reactHost`, `canUseCamera`, `ComponentBottomSheetDialogFragment`, …). Their `useBuiltInKotlin` gate avoids the plugin-conflict failure but does not make the module itself built-in-Kotlin ready, which is why their template ships this opt out. react-native-node-api itself stays built-in-Kotlin ready via the conditional in ee41927 — with this flag set it applies the Kotlin plugin, and for a consumer on built-in Kotlin it steps aside. This is only about the test harness. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Warning
Draft / work in progress. iOS simulator only so far. Opening early for visibility on the approach.
Migrates from the Microsoft/
kraenhansen/hermes-fork Hermes patch flow to Hermes' first-party Node-API, merged onfacebook/hermesbranchstatic_hunderAPI/napi/(targethermesNapi, NAPI v10, built by default). RN has not pulled this in yet, so Hermes is built from source at a pinnedstatic_hSHA.What works now
CxxNodeApiHostModulecasts the JSI runtime toIHermes, reads the low-levelvm::Runtime*viagetVMRuntimeUnsafe(), and creates the env withhermes_napi_create_env(vm, nullptr). The env is owned by the runtime and cached on the module.MOCHA_REMOTE_CONTEXT=allTests→ 14 passing on iOS sim: allnode-addon-examplesgetting-started addons (napi + node-addon-api + Rustferric-example), plusbuffers,async, and ajs-native-apinode-test.Linking
hermesNapi(no pod-side surgery needed)The RN
hermesvmframework already-force_loadshermesNapi, so allnapi_*runtime symbols are exported andhermes_napi_create_env's code is present. The publichermes_napi_*entry points are exported from the framework as long as Hermes is built from a checkout that includes facebook/hermes#2044 "Export public hermes_napi entry points with NAPI macros" — which the pinned SHA already contains. No force_load, no second VM copy, no source patching in this repo. (During bring-up a stale, pre-#2044 vendored checkout briefly stripped the symbol under-fvisibility=hidden; the fix is simply to ensure the vendored checkout is actually at the pinned SHA.)Clean break
Drops RN 0.79–0.81 support, the
kraenhansen/hermesfork tags, JSI-header copying, andrt.createNodeApiEnv(). Targets RN0.87.0-nightly. Old npm releases keep serving old RN versions.Remaining before ready for review
NAPI_VERSION8 → 10 and regenerateweak-node-api+WeakNodeApiInjector.cpp(drop the engine/runtime split)--forcere-vendor at the pinned SHA on the next cold build so the pod matches the pinhermes_napi_host(CallInvoker + worker pool) for async work / thread-safe functionsTest plan
npm run build && npm run prettier:checkreact-native-node-api,cmake-rn,gyp-to-cmake,cmake-file-api)npm run lintMOCHA_REMOTE_CONTEXT=allTests) — 14 passing🤖 Generated with Claude Code