From 13bb0a1c093479ec00b24df153eac1a53f963808 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Mon, 28 Sep 2026 15:08:11 +0200 Subject: [PATCH 1/3] Install globalEvalWithSourceUrl on bridgeless ReactInstance Bridge mode already exposes this helper from JSIExecutor so Metro debug loaders can evaluate fetched JS via JSI. Hermes rejects JS eval() of Metro __d(...) source, so lazy chunks fail on New Architecture. --- .../react/runtime/ReactInstance.cpp | 28 +++++++++++++++++++ .../runtime/tests/cxx/ReactInstanceTest.cpp | 10 +++++++ 2 files changed, 38 insertions(+) diff --git a/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp b/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp index d809b00d7af2..bc9fbb571e93 100644 --- a/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp +++ b/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp @@ -458,6 +458,34 @@ void ReactInstance::initializeRuntime( defineReactInstanceFlags(runtime, options); + // Bridge JSIExecutor installs this so debug loaders can evaluate Metro JS + // via JSI. Hermes does not support JS eval() of Metro `__d(...)` source. + defineReadOnlyGlobal( + runtime, + "globalEvalWithSourceUrl", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "globalEvalWithSourceUrl"), + 2, + [](jsi::Runtime& rt, + const jsi::Value& /*thisValue*/, + const jsi::Value* args, + size_t count) { + if (count != 1 && count != 2) { + throw jsi::JSError( + rt, "globalEvalWithSourceUrl arg count must be 1 or 2"); + } + + auto code = args[0].asString(rt).utf8(rt); + std::string url; + if (count > 1 && args[1].isString()) { + url = args[1].asString(rt).utf8(rt); + } + + return rt.evaluateJavaScript( + std::make_unique(std::move(code)), url); + })); + defineReadOnlyGlobal( runtime, "RN$useAlwaysAvailableJSErrorHandling", diff --git a/packages/react-native/ReactCommon/react/runtime/tests/cxx/ReactInstanceTest.cpp b/packages/react-native/ReactCommon/react/runtime/tests/cxx/ReactInstanceTest.cpp index 858ab8723153..e8f0888fa578 100644 --- a/packages/react-native/ReactCommon/react/runtime/tests/cxx/ReactInstanceTest.cpp +++ b/packages/react-native/ReactCommon/react/runtime/tests/cxx/ReactInstanceTest.cpp @@ -255,6 +255,16 @@ TEST_F(ReactInstanceTest, testBridgelessFlagIsSet) { EXPECT_EQ(val.getBool(), true); } +TEST_F(ReactInstanceTest, testGlobalEvalWithSourceUrlIsInstalled) { + auto before = tryEval("typeof globalEvalWithSourceUrl === 'function'", "false"); + EXPECT_EQ(before.getBool(), false); + initializeRuntimeWithScript(""); + auto isFn = eval("typeof globalEvalWithSourceUrl === 'function'"); + EXPECT_EQ(isFn.getBool(), true); + auto result = eval("globalEvalWithSourceUrl('1 + 2')"); + EXPECT_EQ(result.getNumber(), 3); +} + TEST_F(ReactInstanceTest, testProfilingFlag) { auto valBefore = tryEval("__RCTProfileIsProfiling === true", "false"); EXPECT_EQ(valBefore.getBool(), false); From c60a4a196a1632cb9347cd143f11356ab3f1d336 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Wed, 30 Sep 2026 12:01:03 +0200 Subject: [PATCH 2/3] Add Fantom and C++ tests for bridgeless globalEvalWithSourceUrl Document that the helper is installed on the New Architecture runtime and can evaluate Metro-shaped source, including how that compares to JS eval(). --- .../globalEvalWithSourceUrl-itest.js | 47 +++++++++++++++++++ .../runtime/tests/cxx/ReactInstanceTest.cpp | 17 +++++++ 2 files changed, 64 insertions(+) create mode 100644 packages/react-native/Libraries/Core/Devtools/__tests__/globalEvalWithSourceUrl-itest.js diff --git a/packages/react-native/Libraries/Core/Devtools/__tests__/globalEvalWithSourceUrl-itest.js b/packages/react-native/Libraries/Core/Devtools/__tests__/globalEvalWithSourceUrl-itest.js new file mode 100644 index 000000000000..49ad087f5d39 --- /dev/null +++ b/packages/react-native/Libraries/Core/Devtools/__tests__/globalEvalWithSourceUrl-itest.js @@ -0,0 +1,47 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +describe('globalEvalWithSourceUrl', () => { + it('is installed on the bridgeless runtime', () => { + // $FlowFixMe[prop-missing] + expect(typeof global.globalEvalWithSourceUrl).toBe('function'); + }); + + it('evaluates source via JSI, documenting how that differs from JS eval', () => { + // $FlowFixMe[prop-missing] + const helper = global.globalEvalWithSourceUrl; + expect(typeof helper).toBe('function'); + + // Same shape Metro serves for a lazy chunk (source, not bytecode). + const source = 'globalThis.__fantomEvalMarker = 17; 17'; + + let evalError: mixed = null; + try { + // eslint-disable-next-line no-eval + eval(source); + } catch (e) { + evalError = e; + } + + const helperResult = helper(source, 'globalEvalWithSourceUrl-itest.bundle'); + + expect(globalThis.__fantomEvalMarker).toBe(17); + expect(helperResult).toBe(17); + + if (evalError != null) { + // Lean Hermes: JS eval() is the unsupported path; the helper is JSI. + expect(String(evalError.message || evalError)).toMatch( + /Parsing source code unsupported|eval/i, + ); + } + }); +}); diff --git a/packages/react-native/ReactCommon/react/runtime/tests/cxx/ReactInstanceTest.cpp b/packages/react-native/ReactCommon/react/runtime/tests/cxx/ReactInstanceTest.cpp index e8f0888fa578..67bc397a9d39 100644 --- a/packages/react-native/ReactCommon/react/runtime/tests/cxx/ReactInstanceTest.cpp +++ b/packages/react-native/ReactCommon/react/runtime/tests/cxx/ReactInstanceTest.cpp @@ -265,6 +265,23 @@ TEST_F(ReactInstanceTest, testGlobalEvalWithSourceUrlIsInstalled) { EXPECT_EQ(result.getNumber(), 3); } +TEST_F(ReactInstanceTest, testEvalVersusGlobalEvalWithSourceUrl) { + initializeRuntimeWithScript(""); + + eval("global.__fromHelper = 0; global.__fromEval = 0; global.__evalError = '';"); + eval("globalEvalWithSourceUrl('global.__fromHelper = 1', 'chunk.js')"); + EXPECT_EQ(eval("global.__fromHelper").getNumber(), 1); + + auto evalOk = eval( + "(function(){ try { eval('global.__fromEval = 1'); return true; } catch (e) { global.__evalError = String(e); return false; } })()"); + if (evalOk.getBool()) { + EXPECT_EQ(eval("global.__fromEval").getNumber(), 1); + } else { + auto err = eval("global.__evalError"); + EXPECT_TRUE(err.isString()); + } +} + TEST_F(ReactInstanceTest, testProfilingFlag) { auto valBefore = tryEval("__RCTProfileIsProfiling === true", "false"); EXPECT_EQ(valBefore.getBool(), false); From c19158be14e1e6a00defcef43136b2a2b5df3836 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Thu, 1 Oct 2026 23:41:11 +0200 Subject: [PATCH 3/3] Assert eval() behaviour alongside globalEvalWithSourceUrl in tests The Fantom test now asserts eval()'s result and the source URL attribution difference. The C++ test builds a runtime with RuntimeConfig::EnableEval=false to show eval() throws while globalEvalWithSourceUrl still evaluates source. --- .../globalEvalWithSourceUrl-itest.js | 77 +++++++++++++------ .../react/runtime/ReactInstance.cpp | 6 +- .../runtime/tests/cxx/ReactInstanceTest.cpp | 54 +++++++++---- 3 files changed, 98 insertions(+), 39 deletions(-) diff --git a/packages/react-native/Libraries/Core/Devtools/__tests__/globalEvalWithSourceUrl-itest.js b/packages/react-native/Libraries/Core/Devtools/__tests__/globalEvalWithSourceUrl-itest.js index 49ad087f5d39..ace9b8c158ac 100644 --- a/packages/react-native/Libraries/Core/Devtools/__tests__/globalEvalWithSourceUrl-itest.js +++ b/packages/react-native/Libraries/Core/Devtools/__tests__/globalEvalWithSourceUrl-itest.js @@ -10,38 +10,71 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; +const SOURCE_URL = 'globalEvalWithSourceUrl-itest.bundle'; + +function getHelper(): (code: string, sourceUrl?: string) => mixed { + // $FlowFixMe[prop-missing] + const helper = global.globalEvalWithSourceUrl; + if (typeof helper !== 'function') { + throw new Error( + `Expected global.globalEvalWithSourceUrl to be a function, got ${typeof helper}`, + ); + } + return helper; +} + +function getStack(fn: () => mixed): string { + try { + fn(); + } catch (e) { + return String(e?.stack ?? ''); + } + throw new Error('Expected the evaluated code to throw'); +} + describe('globalEvalWithSourceUrl', () => { - it('is installed on the bridgeless runtime', () => { + afterEach(() => { // $FlowFixMe[prop-missing] - expect(typeof global.globalEvalWithSourceUrl).toBe('function'); + delete globalThis.__fantomEvalMarker; }); - it('evaluates source via JSI, documenting how that differs from JS eval', () => { + it('is installed on the bridgeless runtime', () => { // $FlowFixMe[prop-missing] - const helper = global.globalEvalWithSourceUrl; - expect(typeof helper).toBe('function'); + expect(typeof global.globalEvalWithSourceUrl).toBe('function'); + }); - // Same shape Metro serves for a lazy chunk (source, not bytecode). + it('evaluates the same source as JS eval() in the global scope', () => { const source = 'globalThis.__fantomEvalMarker = 17; 17'; - let evalError: mixed = null; - try { - // eslint-disable-next-line no-eval - eval(source); - } catch (e) { - evalError = e; - } + // eslint-disable-next-line no-eval + expect(eval(source)).toBe(17); + // $FlowFixMe[prop-missing] + expect(globalThis.__fantomEvalMarker).toBe(17); - const helperResult = helper(source, 'globalEvalWithSourceUrl-itest.bundle'); + // $FlowFixMe[prop-missing] + delete globalThis.__fantomEvalMarker; + expect(getHelper()(source, SOURCE_URL)).toBe(17); + // $FlowFixMe[prop-missing] expect(globalThis.__fantomEvalMarker).toBe(17); - expect(helperResult).toBe(17); - - if (evalError != null) { - // Lean Hermes: JS eval() is the unsupported path; the helper is JSI. - expect(String(evalError.message || evalError)).toMatch( - /Parsing source code unsupported|eval/i, - ); - } + }); + + it('attributes evaluated code to the given source URL, unlike eval()', () => { + const source = 'throw new Error("thrown from evaluated source")'; + + // eslint-disable-next-line no-eval + const evalStack = getStack(() => eval(source)); + const helperStack = getStack(() => getHelper()(source, SOURCE_URL)); + + expect(evalStack).not.toContain(SOURCE_URL); + expect(helperStack).toContain(SOURCE_URL); + }); + + it('rejects an invalid argument count', () => { + const helper = getHelper(); + // $FlowFixMe[incompatible-call] + expect(() => helper()).toThrow( + 'globalEvalWithSourceUrl arg count must be 1 or 2', + ); }); }); diff --git a/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp b/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp index bc9fbb571e93..a32a40ba70c1 100644 --- a/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp +++ b/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp @@ -458,8 +458,10 @@ void ReactInstance::initializeRuntime( defineReactInstanceFlags(runtime, options); - // Bridge JSIExecutor installs this so debug loaders can evaluate Metro JS - // via JSI. Hermes does not support JS eval() of Metro `__d(...)` source. + // Bridge JSIExecutor installs this so debug bundle loaders can evaluate + // fetched Metro source through Runtime::evaluateJavaScript, with a source + // URL for stack traces. Unlike JS eval(), that path is not disabled by + // Hermes' RuntimeConfig::EnableEval or by lean engine builds. defineReadOnlyGlobal( runtime, "globalEvalWithSourceUrl", diff --git a/packages/react-native/ReactCommon/react/runtime/tests/cxx/ReactInstanceTest.cpp b/packages/react-native/ReactCommon/react/runtime/tests/cxx/ReactInstanceTest.cpp index 67bc397a9d39..3887b7bdc857 100644 --- a/packages/react-native/ReactCommon/react/runtime/tests/cxx/ReactInstanceTest.cpp +++ b/packages/react-native/ReactCommon/react/runtime/tests/cxx/ReactInstanceTest.cpp @@ -118,9 +118,13 @@ class ReactInstanceTest : public ::testing::Test { protected: ReactInstanceTest() = default; + virtual ::hermes::vm::RuntimeConfig makeRuntimeConfig() { + return ::hermes::vm::RuntimeConfig(); + } + void SetUp() override { - auto runtime = - std::make_unique(hermes::makeHermesRuntime()); + auto runtime = std::make_unique( + hermes::makeHermesRuntime(makeRuntimeConfig())); runtime_ = &runtime->getRuntime(); messageQueueThread_ = std::make_shared(); auto mockRegistry = std::make_unique(); @@ -256,7 +260,8 @@ TEST_F(ReactInstanceTest, testBridgelessFlagIsSet) { } TEST_F(ReactInstanceTest, testGlobalEvalWithSourceUrlIsInstalled) { - auto before = tryEval("typeof globalEvalWithSourceUrl === 'function'", "false"); + auto before = + tryEval("typeof globalEvalWithSourceUrl === 'function'", "false"); EXPECT_EQ(before.getBool(), false); initializeRuntimeWithScript(""); auto isFn = eval("typeof globalEvalWithSourceUrl === 'function'"); @@ -265,21 +270,40 @@ TEST_F(ReactInstanceTest, testGlobalEvalWithSourceUrlIsInstalled) { EXPECT_EQ(result.getNumber(), 3); } -TEST_F(ReactInstanceTest, testEvalVersusGlobalEvalWithSourceUrl) { +TEST_F( + ReactInstanceTest, + testGlobalEvalWithSourceUrlMatchesEvalWhenEvalIsEnabled) { initializeRuntimeWithScript(""); - eval("global.__fromHelper = 0; global.__fromEval = 0; global.__evalError = '';"); - eval("globalEvalWithSourceUrl('global.__fromHelper = 1', 'chunk.js')"); - EXPECT_EQ(eval("global.__fromHelper").getNumber(), 1); - - auto evalOk = eval( - "(function(){ try { eval('global.__fromEval = 1'); return true; } catch (e) { global.__evalError = String(e); return false; } })()"); - if (evalOk.getBool()) { - EXPECT_EQ(eval("global.__fromEval").getNumber(), 1); - } else { - auto err = eval("global.__evalError"); - EXPECT_TRUE(err.isString()); + EXPECT_EQ(eval("eval('1 + 2')").getNumber(), 3); + EXPECT_EQ( + eval("globalEvalWithSourceUrl('1 + 2', 'chunk.js')").getNumber(), 3); +} + +// Hermes gates eval() and the Function constructor behind +// RuntimeConfig::EnableEval, but Runtime::evaluateJavaScript is not gated. +// globalEvalWithSourceUrl goes through the latter, which is why the debug +// bundle loaders prefer it over eval(). +class ReactInstanceWithoutEvalTest : public ReactInstanceTest { + protected: + ::hermes::vm::RuntimeConfig makeRuntimeConfig() override { + return ::hermes::vm::RuntimeConfig::Builder().withEnableEval(false).build(); } +}; + +TEST_F( + ReactInstanceWithoutEvalTest, + testGlobalEvalWithSourceUrlWorksWhenEvalIsDisabled) { + initializeRuntimeWithScript(""); + + auto evalOutcome = eval( + "(function() { try { eval('1 + 2'); return 'no error'; } catch (e) { return String(e.message); } })()"); + EXPECT_THAT( + evalOutcome.getString(*runtime_).utf8(*runtime_), + HasSubstr("Parsing source code unsupported")); + + EXPECT_EQ( + eval("globalEvalWithSourceUrl('1 + 2', 'chunk.js')").getNumber(), 3); } TEST_F(ReactInstanceTest, testProfilingFlag) {