Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
14 changes: 7 additions & 7 deletions apps/test-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,20 @@
"@react-native-node-api/ferric-example": "workspace:*",
"@react-native-node-api/node-addon-examples": "workspace:*",
"@react-native-node-api/node-tests": "workspace:*",
"@react-native/babel-preset": "0.81.4",
"@react-native/metro-config": "0.81.4",
"@react-native/typescript-config": "0.81.4",
"@rnx-kit/metro-config": "^2.1.1",
"@react-native/babel-preset": "0.87.0-nightly-20260529-88857d22f",
"@react-native/metro-config": "0.87.0-nightly-20260529-88857d22f",
"@react-native/typescript-config": "0.87.0-nightly-20260529-88857d22f",
"@rnx-kit/metro-config": "^2.2.4",
"@types/mocha": "^10.0.10",
"@types/react": "^19.1.0",
"concurrently": "^9.1.2",
"mocha": "^11.6.0",
"mocha-remote-cli": "^1.13.2",
"mocha-remote-react-native": "^1.13.2",
"react": "19.1.0",
"react-native": "0.81.4",
"react": "19.2.3",
"react-native": "0.87.0-nightly-20260529-88857d22f",
"react-native-node-api": "workspace:*",
"react-native-test-app": "^4.4.7",
"react-native-test-app": "^5.1.9",
"weak-node-api": "workspace:*"
}
}
2 changes: 1 addition & 1 deletion apps/test-app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extends": "@react-native/typescript-config/tsconfig.json",
"extends": "@react-native/typescript-config",
"compilerOptions": {
"types": ["react-native", "mocha"]
},
Expand Down
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default tseslint.config(
globalIgnores([
"**/dist/**",
"**/build/**",
"**/build-tests/**",
"apps/test-app/ios/**",
"apps/macos-test-app/**",
"packages/host/hermes/**",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"globals": "^16.0.0",
"prettier": "3.6.2",
"publint": "^0.3.15",
"react-native": "0.81.4",
"react-native": "0.87.0-nightly-20260529-88857d22f",
"read-pkg": "^9.0.1",
"tsx": "^4.20.6",
"typescript": "^5.8.0",
Expand Down
38 changes: 37 additions & 1 deletion packages/host/cpp/CxxNodeApiHostModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,26 @@
#include "Logger.hpp"
#include "RuntimeNodeApiAsync.hpp"

#include <jsi/hermes-interfaces.h>

using namespace facebook;

// Declared by the vendored Hermes in API/napi/hermes_napi.h. We forward declare
// it here (rather than including that header) to avoid pulling in Hermes' own
// node_api.h alongside the weak-node-api copy already included transitively.
//
// The declaration must be `extern "C"`: since facebook/hermes#2106 (included in
// the pinned Hermes commit) the public hermes_napi.h wraps these entry points
// in `extern "C"`, so Hermes exports the unmangled C symbol. Without matching C
// linkage here the reference would be to the C++-mangled name and the app fails
// to link ("Undefined symbol: hermes_napi_create_env"). Passing host as nullptr
// is enough — async work / thread-safe functions will return failure until a
// host integration is wired up (Phase 3).
extern "C" {
struct hermes_napi_host;
napi_env hermes_napi_create_env(void *hermes_runtime, hermes_napi_host *host);
}

namespace callstack::react_native_node_api {

CxxNodeApiHostModule::CxxNodeApiHostModule(
Expand Down Expand Up @@ -108,7 +126,25 @@ bool CxxNodeApiHostModule::initializeNodeModule(jsi::Runtime &rt,
// TODO: Read the version from the addon
// @see
// https://github.com/callstackincubator/react-native-node-api/issues/4
napi_env env = reinterpret_cast<napi_env>(rt.createNodeApiEnv(8));

// Lazily create the Node-API environment backing this runtime. Hermes binds
// an env to its low-level VM runtime, which we reach through the (unstable)
// IHermes JSI interface. The env is owned by the runtime and shared across
// all addons, so we create it once and cache it.
if (env_ == nullptr) {
// Fully qualified: `using namespace facebook` makes a bare `hermes`
// ambiguous with the top-level `::hermes` (VM) namespace pulled in via
// <jsi/hermes-interfaces.h>.
auto *hermes = facebook::jsi::castInterface<facebook::hermes::IHermes>(&rt);
if (hermes == nullptr) {
log_debug("NapiHost: JSI runtime is not castable to IHermes; cannot "
"create a Node-API environment");
abort();
}
env_ = hermes_napi_create_env(hermes->getVMRuntimeUnsafe(), nullptr);
assert(env_ != nullptr);
}
napi_env env = env_;

// Create the "exports" object
napi_value exports;
Expand Down
5 changes: 5 additions & 0 deletions packages/host/cpp/CxxNodeApiHostModule.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ class JSI_EXPORT CxxNodeApiHostModule : public facebook::react::TurboModule {
std::unordered_map<std::string, NodeAddon> nodeAddons_;
std::shared_ptr<facebook::react::CallInvoker> callInvoker_;

// The Node-API environment backing every addon loaded into this runtime.
// Created lazily on first use from the underlying Hermes VM runtime and
// shared across all addons (Node-API expects a single env per realm).
napi_env env_ = nullptr;

using LoaderPolicy = PosixLoader; // FIXME: HACK: This is temporary workaround
// for my lazyness (work on iOS and Android)

Expand Down
6 changes: 5 additions & 1 deletion packages/host/cpp/Versions.hpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#pragma once

#define NAPI_VERSION 8
// Must be defined before any <node_api.h> include so the header exposes the
// full v10 surface (js_native_api.h otherwise defaults this to 8).
#ifndef NAPI_VERSION
#define NAPI_VERSION 10
#endif
2 changes: 1 addition & 1 deletion packages/host/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
},
"peerDependencies": {
"@babel/core": "^7.26.10",
"react-native": "0.79.1 || 0.79.2 || 0.79.3 || 0.79.4 || 0.79.5 || 0.79.6 || 0.79.7 || 0.80.0 || 0.80.1 || 0.80.2 || 0.81.0 || 0.81.1 || 0.81.2 || 0.81.3 || 0.81.4 || 0.81.5",
"react-native": "0.87.0-nightly-20260529-88857d22f",
"weak-node-api": "workspace:*"
}
}
29 changes: 5 additions & 24 deletions packages/host/scripts/generate-injector.mts
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,22 @@ import { type FunctionDecl, getNodeApiFunctions } from "weak-node-api";

export const CPP_SOURCE_PATH = path.join(import.meta.dirname, "../cpp");

// TODO: Remove when all runtime Node API functions are implemented
const IMPLEMENTED_RUNTIME_FUNCTIONS = [
"napi_create_buffer",
"napi_create_buffer_copy",
"napi_is_buffer",
"napi_get_buffer_info",
"napi_create_external_buffer",
"napi_create_async_work",
"napi_queue_async_work",
"napi_delete_async_work",
"napi_cancel_async_work",
"napi_fatal_error",
"napi_get_node_version",
"napi_get_version",
];

/**
* Generates source code which injects the Node API functions from the host.
*/
export function generateSource(functions: FunctionDecl[]) {
return `
// This file is generated by react-native-node-api
// Versions.hpp must come first so <node_api.h> exposes the full v10 surface.
#include <Versions.hpp>

#include <dlfcn.h>
#include <weak_node_api.hpp>

#include <Logger.hpp>
#include <RuntimeNodeApi.hpp>
#include <RuntimeNodeApiAsync.hpp>

#if defined(__APPLE__)
#define WEAK_NODE_API_LIBRARY_NAME "@rpath/weak-node-api.framework/weak-node-api"
#elif defined(__ANDROID__)
Expand All @@ -61,13 +48,7 @@ export function generateSource(functions: FunctionDecl[]) {

log_debug("Injecting NodeApiHost");
inject_weak_node_api_host(NodeApiHost {
${functions
.filter(
({ kind, name }) =>
kind === "engine" || IMPLEMENTED_RUNTIME_FUNCTIONS.includes(name),
)
.flatMap(({ name }) => `.${name} = ${name},`)
.join("\n")}
${functions.flatMap(({ name }) => `.${name} = ${name},`).join("\n")}
});
}
} // namespace callstack::react_native_node_api
Expand Down
33 changes: 12 additions & 21 deletions packages/host/scripts/patch-hermes.rb
Original file line number Diff line number Diff line change
@@ -1,33 +1,24 @@
Pod::UI.warn "!!! PATCHING HERMES WITH NODE-API SUPPORT !!!"
Pod::UI.warn "!!! CONFIGURING HERMES WITH NODE-API SUPPORT !!!"

if ENV['RCT_USE_PREBUILT_RNCORE'] == '1'
raise "React Native Node-API cannot reliably patch JSI when React Native Core is prebuilt."
end

def get_react_native_package
if caller.any? { |frame| frame.include?("node_modules/react-native-macos/") }
return "react-native-macos"
elsif caller.any? { |frame| frame.include?("node_modules/react-native/") }
return "react-native"
else
raise "Unable to determine React Native package from call stack."
if ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].nil?
def get_react_native_package
if caller.any? { |frame| frame.include?("node_modules/react-native-macos/") }
return "react-native-macos"
elsif caller.any? { |frame| frame.include?("node_modules/react-native/") }
return "react-native"
else
raise "Unable to determine React Native package from call stack."
end
end
end

if ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].nil?
VENDORED_HERMES_DIR ||= `npx react-native-node-api vendor-hermes --react-native-package '#{get_react_native_package()}' --silent '#{Pod::Config.instance.installation_root}'`.strip
# Signal the patched Hermes to React Native
ENV['BUILD_FROM_SOURCE'] = 'true'
ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'] = VENDORED_HERMES_DIR
elsif Dir.exist?(ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'])
# Setting an override path implies building from source
ENV['BUILD_FROM_SOURCE'] = 'true'
end

if !ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].empty?
if ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'] && !ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].empty?
if Dir.exist?(ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'])
Pod::UI.info "[Node-API] Using overridden Hermes in #{ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].inspect}"
else
raise "Hermes patching failed: Expected override to exist in #{ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].inspect}"
raise "Hermes setup failed: Expected override to exist in #{ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].inspect}"
end
end
6 changes: 4 additions & 2 deletions packages/host/scripts/patch-xcode-project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
NODE_BINARY = ENV["NODE_BINARY"] || `command -v node`.strip
CLI_COMMAND = "'#{NODE_BINARY}' '#{File.join(__dir__, "../dist/node/cli/run.js")}'"
PATCH_XCODE_PROJECT_COMMAND = "#{CLI_COMMAND} patch-xcode-project '#{Pod::Config.instance.installation_root}'"

# Using an at_exit hook to ensure the command is executed after the pod install is complete
at_exit do
system(PATCH_XCODE_PROJECT_COMMAND) or raise "Failed to patch the Xcode project"
unless system(PATCH_XCODE_PROJECT_COMMAND)
Pod::UI.warn "[Node-API] Failed to patch the Xcode project (non-fatal)"
end
end
end
Loading
Loading