Skip to content

Commit 2a2ccaa

Browse files
abzokhattabmeta-codesync[bot]
authored andcommitted
Install globalEvalWithSourceUrl on bridgeless ReactInstance (#58718)
Summary: **Problem:** On the New Architecture (bridgeless), fetching and evaluating a Metro split bundle in development (`lazy=true`, `import()`, `React.lazy()`) can fail with: ``` SyntaxError: Parsing source code unsupported: __d(function (global, require, _ ``` The main bundle still loads. **Root cause:** `global.globalEvalWithSourceUrl` is never installed on bridgeless. Bridge mode installs it in `JSIExecutor::initializeRuntime`. ```js typeof global.globalEvalWithSourceUrl // bridgeless: "undefined" // bridge: "function" ``` The debug loader (`Libraries/Core/Devtools/loadBundleFromServer.js`) prefers that helper and only falls back to JS `eval()` when it is missing. The two paths are not equivalent in Hermes: - `eval()` / `Function` go through `evalInEnvironment`, which raises `Parsing source code unsupported` when `RuntimeConfig::EnableEval` is false (and, on legacy Hermes, when the engine is built lean). - `globalEvalWithSourceUrl` goes through `Runtime::evaluateJavaScript`, the same entry point used to run the main bundle. It is not gated by `EnableEval`, and it attaches the given source URL to stack traces. So on a runtime where `EnableEval` is off (or the engine is lean), the main bundle loads but every split bundle falls into `eval()` and throws. Bridgeless has no way to reach the JSI path from JS today. **Fix:** Port the existing `JSIExecutor` binding into `ReactInstance::initializeRuntime` so bridgeless exposes the same helper. ## Changelog: [General] [Fixed] - Install globalEvalWithSourceUrl on bridgeless ReactInstance so debug bundle loaders can evaluate split bundles via JSI instead of eval() Pull Request resolved: #58718 Test Plan: 1. `ReactInstanceTest.testGlobalEvalWithSourceUrlIsInstalled`: helper is absent before `initializeRuntime` and present after. 2. `ReactInstanceTest.testGlobalEvalWithSourceUrlMatchesEvalWhenEvalIsEnabled`: with the default `RuntimeConfig`, `eval('1 + 2')` and `globalEvalWithSourceUrl('1 + 2', 'chunk.js')` both return `3`. 3. `ReactInstanceWithoutEvalTest.testGlobalEvalWithSourceUrlWorksWhenEvalIsDisabled`: runtime built with `RuntimeConfig::Builder().withEnableEval(false)`; `eval('1 + 2')` throws `Parsing source code unsupported`, `globalEvalWithSourceUrl('1 + 2', 'chunk.js')` returns `3`. 4. Fantom `Libraries/Core/Devtools/__tests__/globalEvalWithSourceUrl-itest.js`: helper is installed on bridgeless; `eval()` and the helper evaluate the same source to the same result; an error thrown from the helper carries the given source URL in its stack while the `eval()` one does not; wrong arity throws. 5. In a bridgeless debug app, `typeof global.globalEvalWithSourceUrl` is `'function'` and a `React.lazy(() => import('./SomeModule'))` split bundle loads without `Parsing source code unsupported`. Reviewed By: fabriziocucci Differential Revision: D122329977 Pulled By: javache fbshipit-source-id: 789294c7806a331828effd407588208a55e1d9a3
1 parent 44ffdba commit 2a2ccaa

3 files changed

Lines changed: 168 additions & 2 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict
8+
* @format
9+
*/
10+
11+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
12+
13+
// Must not be a substring of this test's own bundle URL, which appears in
14+
// every stack trace.
15+
const SOURCE_URL = 'split-chunk-under-test.bundle';
16+
17+
function getHelper(): (code: string, sourceUrl?: string) => unknown {
18+
// $FlowFixMe[prop-missing]
19+
const helper = global.globalEvalWithSourceUrl;
20+
if (typeof helper !== 'function') {
21+
throw new Error(
22+
`Expected global.globalEvalWithSourceUrl to be a function, got ${typeof helper}`,
23+
);
24+
}
25+
return helper;
26+
}
27+
28+
function getStack(fn: () => unknown): string {
29+
try {
30+
fn();
31+
} catch (e) {
32+
return String(e?.stack ?? '');
33+
}
34+
throw new Error('Expected the evaluated code to throw');
35+
}
36+
37+
describe('globalEvalWithSourceUrl', () => {
38+
afterEach(() => {
39+
// $FlowFixMe[prop-missing]
40+
delete globalThis.__fantomEvalMarker;
41+
});
42+
43+
it('is installed on the bridgeless runtime', () => {
44+
// $FlowFixMe[prop-missing]
45+
expect(typeof global.globalEvalWithSourceUrl).toBe('function');
46+
});
47+
48+
it('evaluates the same source as JS eval() in the global scope', () => {
49+
const source = 'globalThis.__fantomEvalMarker = 17; 17';
50+
51+
// eslint-disable-next-line no-eval
52+
expect(eval(source)).toBe(17);
53+
// $FlowFixMe[prop-missing]
54+
expect(globalThis.__fantomEvalMarker).toBe(17);
55+
56+
// $FlowFixMe[prop-missing]
57+
delete globalThis.__fantomEvalMarker;
58+
59+
expect(getHelper()(source, SOURCE_URL)).toBe(17);
60+
// $FlowFixMe[prop-missing]
61+
expect(globalThis.__fantomEvalMarker).toBe(17);
62+
});
63+
64+
it('attributes evaluated code to the given source URL, unlike eval()', () => {
65+
const source = 'throw new Error("thrown from evaluated source")';
66+
67+
// eslint-disable-next-line no-eval
68+
const evalStack = getStack(() => eval(source));
69+
const helperStack = getStack(() => getHelper()(source, SOURCE_URL));
70+
71+
expect(evalStack).not.toContain(SOURCE_URL);
72+
expect(helperStack).toContain(SOURCE_URL);
73+
});
74+
75+
it('rejects an invalid argument count', () => {
76+
const helper = getHelper();
77+
// $FlowFixMe[incompatible-type]
78+
expect(() => helper()).toThrow(
79+
'globalEvalWithSourceUrl arg count must be 1 or 2',
80+
);
81+
});
82+
});

‎packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,39 @@ void ReactInstance::initializeRuntime(
458458

459459
defineReactInstanceFlags(runtime, options);
460460

461+
// Bridge JSIExecutor installs this so debug bundle loaders can evaluate
462+
// fetched Metro source through Runtime::evaluateJavaScript, with a source
463+
// URL for stack traces. Unlike JS eval(), that path is not disabled by
464+
// Hermes' RuntimeConfig::EnableEval or by lean engine builds. Left
465+
// writable, like in JSIExecutor, so hosts can override or wrap it.
466+
runtime.global().setProperty(
467+
runtime,
468+
"globalEvalWithSourceUrl",
469+
jsi::Function::createFromHostFunction(
470+
runtime,
471+
jsi::PropNameID::forAscii(runtime, "globalEvalWithSourceUrl"),
472+
2,
473+
[](jsi::Runtime& rt,
474+
const jsi::Value& /*thisValue*/,
475+
const jsi::Value* args,
476+
size_t count) {
477+
if (count != 1 && count != 2) {
478+
throw jsi::JSError(
479+
rt, "globalEvalWithSourceUrl arg count must be 1 or 2");
480+
}
481+
482+
// NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic)
483+
auto code = args[0].asString(rt).utf8(rt);
484+
std::string url;
485+
if (count > 1 && args[1].isString()) {
486+
url = args[1].asString(rt).utf8(rt);
487+
}
488+
// NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic)
489+
490+
return rt.evaluateJavaScript(
491+
std::make_unique<jsi::StringBuffer>(std::move(code)), url);
492+
}));
493+
461494
defineReadOnlyGlobal(
462495
runtime,
463496
"RN$useAlwaysAvailableJSErrorHandling",

‎packages/react-native/ReactCommon/react/runtime/tests/cxx/ReactInstanceTest.cpp‎

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,13 @@ class ReactInstanceTest : public ::testing::Test {
118118
protected:
119119
ReactInstanceTest() = default;
120120

121+
virtual ::hermes::vm::RuntimeConfig makeRuntimeConfig() {
122+
return {};
123+
}
124+
121125
void SetUp() override {
122-
auto runtime =
123-
std::make_unique<JSIRuntimeHolder>(hermes::makeHermesRuntime());
126+
auto runtime = std::make_unique<JSIRuntimeHolder>(
127+
hermes::makeHermesRuntime(makeRuntimeConfig()));
124128
runtime_ = &runtime->getRuntime();
125129
messageQueueThread_ = std::make_shared<MockMessageQueueThread>();
126130
auto mockRegistry = std::make_unique<MockTimerRegistry>();
@@ -255,6 +259,53 @@ TEST_F(ReactInstanceTest, testBridgelessFlagIsSet) {
255259
EXPECT_EQ(val.getBool(), true);
256260
}
257261

262+
TEST_F(ReactInstanceTest, testGlobalEvalWithSourceUrlIsInstalled) {
263+
auto before =
264+
tryEval("typeof globalEvalWithSourceUrl === 'function'", "false");
265+
EXPECT_EQ(before.getBool(), false);
266+
initializeRuntimeWithScript("");
267+
auto isFn = eval("typeof globalEvalWithSourceUrl === 'function'");
268+
EXPECT_EQ(isFn.getBool(), true);
269+
auto result = eval("globalEvalWithSourceUrl('1 + 2')");
270+
EXPECT_EQ(result.getNumber(), 3);
271+
}
272+
273+
TEST_F(
274+
ReactInstanceTest,
275+
testGlobalEvalWithSourceUrlMatchesEvalWhenEvalIsEnabled) {
276+
initializeRuntimeWithScript("");
277+
278+
EXPECT_EQ(eval("eval('1 + 2')").getNumber(), 3);
279+
EXPECT_EQ(
280+
eval("globalEvalWithSourceUrl('1 + 2', 'chunk.js')").getNumber(), 3);
281+
}
282+
283+
// Hermes gates eval() and the Function constructor behind
284+
// RuntimeConfig::EnableEval, but Runtime::evaluateJavaScript is not gated.
285+
// globalEvalWithSourceUrl goes through the latter, which is why the debug
286+
// bundle loaders prefer it over eval().
287+
class ReactInstanceWithoutEvalTest : public ReactInstanceTest {
288+
protected:
289+
::hermes::vm::RuntimeConfig makeRuntimeConfig() override {
290+
return ::hermes::vm::RuntimeConfig::Builder().withEnableEval(false).build();
291+
}
292+
};
293+
294+
TEST_F(
295+
ReactInstanceWithoutEvalTest,
296+
testGlobalEvalWithSourceUrlWorksWhenEvalIsDisabled) {
297+
initializeRuntimeWithScript("");
298+
299+
auto evalOutcome = eval(
300+
"(function() { try { eval('1 + 2'); return 'no error'; } catch (e) { return String(e.message); } })()");
301+
EXPECT_THAT(
302+
evalOutcome.getString(*runtime_).utf8(*runtime_),
303+
HasSubstr("Parsing source code unsupported"));
304+
305+
EXPECT_EQ(
306+
eval("globalEvalWithSourceUrl('1 + 2', 'chunk.js')").getNumber(), 3);
307+
}
308+
258309
TEST_F(ReactInstanceTest, testProfilingFlag) {
259310
auto valBefore = tryEval("__RCTProfileIsProfiling === true", "false");
260311
EXPECT_EQ(valBefore.getBool(), false);

0 commit comments

Comments
 (0)