Skip to content
Open
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
@@ -0,0 +1,80 @@
/**
* 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';

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', () => {
afterEach(() => {
// $FlowFixMe[prop-missing]
delete globalThis.__fantomEvalMarker;
});

it('is installed on the bridgeless runtime', () => {
// $FlowFixMe[prop-missing]
expect(typeof global.globalEvalWithSourceUrl).toBe('function');
});

it('evaluates the same source as JS eval() in the global scope', () => {
const source = 'globalThis.__fantomEvalMarker = 17; 17';

// eslint-disable-next-line no-eval
expect(eval(source)).toBe(17);
// $FlowFixMe[prop-missing]
expect(globalThis.__fantomEvalMarker).toBe(17);

// $FlowFixMe[prop-missing]
delete globalThis.__fantomEvalMarker;

expect(getHelper()(source, SOURCE_URL)).toBe(17);
// $FlowFixMe[prop-missing]
expect(globalThis.__fantomEvalMarker).toBe(17);
});

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',
);
});
});
30 changes: 30 additions & 0 deletions packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,36 @@ void ReactInstance::initializeRuntime(

defineReactInstanceFlags(runtime, options);

// 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",
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<jsi::StringBuffer>(std::move(code)), url);
}));

defineReadOnlyGlobal(
runtime,
"RN$useAlwaysAvailableJSErrorHandling",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<JSIRuntimeHolder>(hermes::makeHermesRuntime());
auto runtime = std::make_unique<JSIRuntimeHolder>(
hermes::makeHermesRuntime(makeRuntimeConfig()));
runtime_ = &runtime->getRuntime();
messageQueueThread_ = std::make_shared<MockMessageQueueThread>();
auto mockRegistry = std::make_unique<MockTimerRegistry>();
Expand Down Expand Up @@ -255,6 +259,53 @@ 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,
testGlobalEvalWithSourceUrlMatchesEvalWhenEvalIsEnabled) {
initializeRuntimeWithScript("");

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) {
auto valBefore = tryEval("__RCTProfileIsProfiling === true", "false");
EXPECT_EQ(valBefore.getBool(), false);
Expand Down
Loading