Skip to content

Commit 84c46c5

Browse files
kraenhansenclaude
andcommitted
Phase 1: vendor static_h Hermes, bump to RN 0.87 nightly
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>
1 parent 3ea96e4 commit 84c46c5

9 files changed

Lines changed: 62 additions & 128 deletions

File tree

apps/test-app/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,20 @@
3030
"@react-native-node-api/ferric-example": "workspace:*",
3131
"@react-native-node-api/node-addon-examples": "workspace:*",
3232
"@react-native-node-api/node-tests": "workspace:*",
33-
"@react-native/babel-preset": "0.81.4",
34-
"@react-native/metro-config": "0.81.4",
35-
"@react-native/typescript-config": "0.81.4",
33+
"@react-native/babel-preset": "0.87.0-nightly-20260529-88857d22f",
34+
"@react-native/metro-config": "0.87.0-nightly-20260529-88857d22f",
35+
"@react-native/typescript-config": "0.87.0-nightly-20260529-88857d22f",
3636
"@rnx-kit/metro-config": "^2.1.1",
3737
"@types/mocha": "^10.0.10",
3838
"@types/react": "^19.1.0",
3939
"concurrently": "^9.1.2",
4040
"mocha": "^11.6.0",
4141
"mocha-remote-cli": "^1.13.2",
4242
"mocha-remote-react-native": "^1.13.2",
43-
"react": "19.1.0",
44-
"react-native": "0.81.4",
43+
"react": "19.2.3",
44+
"react-native": "0.87.0-nightly-20260529-88857d22f",
4545
"react-native-node-api": "workspace:*",
46-
"react-native-test-app": "^4.4.7",
46+
"react-native-test-app": "^5.1.9",
4747
"weak-node-api": "workspace:*"
4848
}
4949
}

apps/test-app/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"extends": "@react-native/typescript-config/tsconfig.json",
2+
"extends": "@react-native/typescript-config",
33
"compilerOptions": {
44
"types": ["react-native", "mocha"]
55
},

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
"globals": "^16.0.0",
5959
"prettier": "3.6.2",
6060
"publint": "^0.3.15",
61-
"react-native": "0.81.4",
61+
"react-native": "0.87.0-nightly-20260529-88857d22f",
6262
"read-pkg": "^9.0.1",
6363
"tsx": "^4.20.6",
6464
"typescript": "^5.8.0",

packages/host/cpp/CxxNodeApiHostModule.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ bool CxxNodeApiHostModule::initializeNodeModule(jsi::Runtime &rt,
108108
// TODO: Read the version from the addon
109109
// @see
110110
// https://github.com/callstackincubator/react-native-node-api/issues/4
111-
napi_env env = reinterpret_cast<napi_env>(rt.createNodeApiEnv(8));
111+
// TODO: Phase 2-3 will replace this with hermes_napi_create_env
112+
napi_env env = nullptr;
112113

113114
// Create the "exports" object
114115
napi_value exports;

packages/host/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
},
8686
"peerDependencies": {
8787
"@babel/core": "^7.26.10",
88-
"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",
88+
"react-native": "0.87.0-nightly-20260529-88857d22f",
8989
"weak-node-api": "workspace:*"
9090
}
9191
}
Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,24 @@
1-
Pod::UI.warn "!!! PATCHING HERMES WITH NODE-API SUPPORT !!!"
1+
Pod::UI.warn "!!! CONFIGURING HERMES WITH NODE-API SUPPORT !!!"
22

3-
if ENV['RCT_USE_PREBUILT_RNCORE'] == '1'
4-
raise "React Native Node-API cannot reliably patch JSI when React Native Core is prebuilt."
5-
end
6-
7-
def get_react_native_package
8-
if caller.any? { |frame| frame.include?("node_modules/react-native-macos/") }
9-
return "react-native-macos"
10-
elsif caller.any? { |frame| frame.include?("node_modules/react-native/") }
11-
return "react-native"
12-
else
13-
raise "Unable to determine React Native package from call stack."
3+
if ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].nil?
4+
def get_react_native_package
5+
if caller.any? { |frame| frame.include?("node_modules/react-native-macos/") }
6+
return "react-native-macos"
7+
elsif caller.any? { |frame| frame.include?("node_modules/react-native/") }
8+
return "react-native"
9+
else
10+
raise "Unable to determine React Native package from call stack."
11+
end
1412
end
15-
end
1613

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

27-
if !ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].empty?
18+
if ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'] && !ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].empty?
2819
if Dir.exist?(ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'])
2920
Pod::UI.info "[Node-API] Using overridden Hermes in #{ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].inspect}"
3021
else
31-
raise "Hermes patching failed: Expected override to exist in #{ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].inspect}"
22+
raise "Hermes setup failed: Expected override to exist in #{ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].inspect}"
3223
end
3324
end

packages/host/scripts/patch-xcode-project.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
NODE_BINARY = ENV["NODE_BINARY"] || `command -v node`.strip
66
CLI_COMMAND = "'#{NODE_BINARY}' '#{File.join(__dir__, "../dist/node/cli/run.js")}'"
77
PATCH_XCODE_PROJECT_COMMAND = "#{CLI_COMMAND} patch-xcode-project '#{Pod::Config.instance.installation_root}'"
8-
8+
99
# Using an at_exit hook to ensure the command is executed after the pod install is complete
1010
at_exit do
11-
system(PATCH_XCODE_PROJECT_COMMAND) or raise "Failed to patch the Xcode project"
11+
unless system(PATCH_XCODE_PROJECT_COMMAND)
12+
Pod::UI.warn "[Node-API] Failed to patch the Xcode project (non-fatal)"
13+
end
1214
end
1315
end

packages/host/src/node/cli/hermes.ts

Lines changed: 35 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -15,40 +15,19 @@ import {
1515
import { packageDirectory } from "pkg-dir";
1616
import { readPackage } from "read-pkg";
1717

18-
// FIXME: make this configurable with reasonable fallback before public release
19-
const HERMES_GIT_URL = "https://github.com/kraenhansen/hermes.git";
18+
const HERMES_GIT_URL = "https://github.com/facebook/hermes.git";
19+
20+
// Pinned commit on the `static_h` branch, which carries the first-party
21+
// Node-API implementation under `API/napi`. Bump deliberately: the JSI
22+
// accessor we rely on (`getVMRuntimeUnsafe`) is documented as unstable, so we
23+
// vendor a known-good commit rather than tracking a moving branch.
24+
const HERMES_GIT_SHA = "0ae42446d1ae669508368b0a18e60c789f76735d";
2025

2126
const platformOption = new Option(
2227
"--react-native-package <package-name>",
2328
"The React Native package to vendor Hermes into",
2429
).default("react-native");
2530

26-
type PatchJSIHeadersOptions = {
27-
reactNativePath: string;
28-
hermesJsiPath: string;
29-
silent: boolean;
30-
};
31-
32-
async function patchJsiHeaders({
33-
reactNativePath,
34-
hermesJsiPath,
35-
silent,
36-
}: PatchJSIHeadersOptions) {
37-
const reactNativeJsiPath = path.join(reactNativePath, "ReactCommon/jsi/jsi/");
38-
await oraPromise(
39-
fs.promises.cp(hermesJsiPath, reactNativeJsiPath, {
40-
recursive: true,
41-
}),
42-
{
43-
text: `Copying JSI from patched Hermes to React Native`,
44-
successText: "Copied JSI from patched Hermes to React Native",
45-
failText: (err) =>
46-
`Failed to copy JSI from Hermes to React Native: ${err.message}`,
47-
isEnabled: !silent,
48-
},
49-
);
50-
}
51-
5231
export const command = new Command("vendor-hermes")
5332
.argument("[from]", "Path to a file inside the app package", process.cwd())
5433
.option("--silent", "Don't print anything except the final path", false)
@@ -75,19 +54,8 @@ export const command = new Command("vendor-hermes")
7554
paths: [appPackageRoot],
7655
}),
7756
);
78-
const hermesVersionPath = path.join(
79-
reactNativePath,
80-
"sdks",
81-
".hermesversion",
82-
);
83-
assert(
84-
fs.existsSync(hermesVersionPath),
85-
`Expected a file with a Hermes version at ${prettyPath(hermesVersionPath)}`,
86-
);
87-
88-
const hermesVersion = fs.readFileSync(hermesVersionPath, "utf8").trim();
8957
if (!silent) {
90-
console.log(`Using Hermes version: ${hermesVersion}`);
58+
console.log(`Vendoring Hermes at ${HERMES_GIT_SHA}`);
9159
}
9260

9361
const hermesPath = path.join(reactNativePath, "sdks", "node-api-hermes");
@@ -104,53 +72,49 @@ export const command = new Command("vendor-hermes")
10472
);
10573
}
10674
if (!fs.existsSync(hermesPath)) {
107-
const patchedTag = `node-api-${hermesVersion}`;
10875
try {
76+
// GitHub allows fetching a reachable commit by SHA, so we can clone
77+
// the pinned commit shallowly without downloading the whole history.
10978
await oraPromise(
110-
spawn(
111-
"git",
112-
[
113-
"clone",
79+
(async () => {
80+
await fs.promises.mkdir(hermesPath, { recursive: true });
81+
const git = (args: string[]) =>
82+
spawn("git", args, {
83+
cwd: hermesPath,
84+
outputMode: "buffered",
85+
});
86+
await git(["init", "--quiet"]);
87+
await git(["remote", "add", "origin", HERMES_GIT_URL]);
88+
await git(["fetch", "--depth", "1", "origin", HERMES_GIT_SHA]);
89+
await git(["checkout", "--quiet", "FETCH_HEAD"]);
90+
await git([
91+
"submodule",
92+
"update",
93+
"--init",
11494
"--recursive",
11595
"--depth",
11696
"1",
117-
"--branch",
118-
patchedTag,
119-
HERMES_GIT_URL,
120-
hermesPath,
121-
],
122-
{
123-
outputMode: "buffered",
124-
},
125-
),
97+
]);
98+
})(),
12699
{
127-
text: `Cloning custom Hermes into ${prettyPath(hermesPath)}`,
128-
successText: "Cloned custom Hermes",
129-
failText: (err) =>
130-
`Failed to clone custom Hermes: ${err.message}`,
100+
text: `Cloning Hermes into ${prettyPath(hermesPath)}`,
101+
successText: "Cloned Hermes",
102+
failText: (err) => `Failed to clone Hermes: ${err.message}`,
131103
isEnabled: !silent,
132104
},
133105
);
134106
} catch (error) {
135-
throw new UsageError("Failed to clone custom Hermes", {
107+
// A failed clone can leave a partial checkout behind, which would
108+
// make the existence check above skip re-cloning on the next run.
109+
await fs.promises.rm(hermesPath, { recursive: true, force: true });
110+
throw new UsageError("Failed to clone Hermes", {
136111
cause: error,
137112
fix: {
138-
instructions: `Check the network connection and ensure this ${chalk.bold("react-native")} version is supported by ${chalk.bold("react-native-node-api")}.`,
113+
instructions: `Check the network connection and that the pinned Hermes commit ${chalk.bold(HERMES_GIT_SHA)} is still reachable on ${chalk.bold(HERMES_GIT_URL)}.`,
139114
},
140115
});
141116
}
142117
}
143-
const hermesJsiPath = path.join(hermesPath, "API/jsi/jsi");
144-
145-
assert(
146-
fs.existsSync(hermesJsiPath),
147-
`Hermes JSI path does not exist: ${hermesJsiPath}`,
148-
);
149-
await patchJsiHeaders({
150-
reactNativePath,
151-
hermesJsiPath,
152-
silent,
153-
});
154118
console.log(hermesPath);
155119
}),
156120
);

packages/host/src/node/podspec.test.ts

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)