From 71c84cf6bebba701bb23b135e079aa68c05f4441 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Thu, 25 Jul 2019 10:19:44 -0700 Subject: [PATCH] Implement globalEvalWithSourceUrl Summary: Implements a new host function on the global object in debug builds, called `globalEvalWithSourceUrl`. This performs a global `eval()` and attaches a URL/filename to the evaluated script (in stack traces, debuggers, etc). It serves a similar purpose to the `//# sourceURL=` directive (which most JS engines support, but JSC doesn't) and to the old `nativeInjectHMRUpdate` function which was dropped in the JSC->JSI migration. Reviewed By: cpojer Differential Revision: D16491506 fbshipit-source-id: bd9a89311dcbb1d0baece77ead16b9ecfb13bfe3 --- .../GlobalEvalWithSourceUrlTest.js | 80 +++++++++++++++++++ IntegrationTests/IntegrationTestsApp.js | 1 + .../RNTesterIntegrationTests.m | 2 +- .../jsiexecutor/jsireact/JSIExecutor.cpp | 33 ++++++++ .../jsiexecutor/jsireact/JSIExecutor.h | 3 + 5 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 IntegrationTests/GlobalEvalWithSourceUrlTest.js diff --git a/IntegrationTests/GlobalEvalWithSourceUrlTest.js b/IntegrationTests/GlobalEvalWithSourceUrlTest.js new file mode 100644 index 00000000000..3b633e9acc2 --- /dev/null +++ b/IntegrationTests/GlobalEvalWithSourceUrlTest.js @@ -0,0 +1,80 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const parseErrorStack = require('react-native/Libraries/Core/Devtools/parseErrorStack'); +const {View} = ReactNative; + +const {TestModule} = ReactNative.NativeModules; + +class GlobalEvalWithSourceUrlTest extends React.Component<{}> { + componentDidMount() { + if (typeof global.globalEvalWithSourceUrl !== 'function') { + throw new Error( + 'Expected to find globalEvalWithSourceUrl function on global object but found ' + + typeof global.globalEvalWithSourceUrl, + ); + } + const value = global.globalEvalWithSourceUrl('42'); + if (value !== 42) { + throw new Error( + 'Expected globalEvalWithSourceUrl(expression) to return a value', + ); + } + let syntaxError; + try { + global.globalEvalWithSourceUrl('{'); + } catch (e) { + syntaxError = e; + } + if (!syntaxError) { + throw new Error( + 'Expected globalEvalWithSourceUrl to throw on a syntax error', + ); + } + if (!(syntaxError instanceof SyntaxError)) { + throw new Error( + 'Expected globalEvalWithSourceUrl to throw SyntaxError on a syntax error', + ); + } + const url = 'http://example.com/foo.js'; + let error; + try { + global.globalEvalWithSourceUrl('throw new Error()', url); + } catch (e) { + error = e; + } + if (!error) { + throw new Error( + 'Expected globalEvalWithSourceUrl to throw an Error object', + ); + } + const parsedStack = parseErrorStack(error); + if (parsedStack[0].file !== url) { + throw new Error( + `Expected first eval stack frame to be in ${url} but found ${ + parsedStack[0].file + }`, + ); + } + TestModule.markTestCompleted(); + } + + render(): React.Node { + return ; + } +} + +GlobalEvalWithSourceUrlTest.displayName = 'GlobalEvalWithSourceUrlTest'; + +module.exports = GlobalEvalWithSourceUrlTest; diff --git a/IntegrationTests/IntegrationTestsApp.js b/IntegrationTests/IntegrationTestsApp.js index d1f736bd789..c8af81bf126 100644 --- a/IntegrationTests/IntegrationTestsApp.js +++ b/IntegrationTests/IntegrationTestsApp.js @@ -36,6 +36,7 @@ const TESTS = [ require('./SyncMethodTest'), require('./WebSocketTest'), require('./AccessibilityManagerTest'), + require('./GlobalEvalWithSourceUrlTest'), ]; TESTS.forEach( diff --git a/RNTester/RNTesterIntegrationTests/RNTesterIntegrationTests.m b/RNTester/RNTesterIntegrationTests/RNTesterIntegrationTests.m index de28e9a7425..b8a55652aa8 100644 --- a/RNTester/RNTesterIntegrationTests/RNTesterIntegrationTests.m +++ b/RNTester/RNTesterIntegrationTests/RNTesterIntegrationTests.m @@ -74,6 +74,6 @@ RCT_TEST(SyncMethodTest) RCT_TEST(PromiseTest) RCT_TEST_ONLY_WITH_PACKAGER(WebSocketTest) // Requires a WebSocket test server, see scripts/objc-test.sh RCT_TEST(AccessibilityManagerTest) +RCT_TEST(GlobalEvalWithSourceUrlTest) @end - diff --git a/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp b/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp index e261dc574b0..c31a740509b 100644 --- a/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp +++ b/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp @@ -113,6 +113,21 @@ void JSIExecutor::loadApplicationScript( const jsi::Value *args, size_t count) { return nativeCallSyncHook(args, count); })); +#if DEBUG + runtime_->global().setProperty( + *runtime_, + "globalEvalWithSourceUrl", + Function::createFromHostFunction( + *runtime_, + PropNameID::forAscii(*runtime_, "globalEvalWithSourceUrl"), + 1, + [this]( + jsi::Runtime &, + const jsi::Value &, + const jsi::Value *args, + size_t count) { return globalEvalWithSourceUrl(args, count); })); +#endif + if (runtimeInstaller_) { runtimeInstaller_(*runtime_); } @@ -358,6 +373,24 @@ Value JSIExecutor::nativeCallSyncHook(const Value *args, size_t count) { return valueFromDynamic(*runtime_, result.value()); } +#if DEBUG +Value JSIExecutor::globalEvalWithSourceUrl(const Value *args, size_t count) { + if (count != 1 && count != 2) { + throw std::invalid_argument( + "globalEvalWithSourceUrl arg count must be 1 or 2"); + } + + auto code = args[0].asString(*runtime_).utf8(*runtime_); + std::string url; + if (count > 1 && args[1].isString()) { + url = args[1].asString(*runtime_).utf8(*runtime_); + } + + return runtime_->evaluateJavaScript( + std::make_unique(std::move(code)), url); +} +#endif + void bindNativeLogger(Runtime &runtime, Logger logger) { runtime.global().setProperty( runtime, diff --git a/ReactCommon/jsiexecutor/jsireact/JSIExecutor.h b/ReactCommon/jsiexecutor/jsireact/JSIExecutor.h index a290c409b8d..9e932964252 100644 --- a/ReactCommon/jsiexecutor/jsireact/JSIExecutor.h +++ b/ReactCommon/jsiexecutor/jsireact/JSIExecutor.h @@ -112,6 +112,9 @@ class JSIExecutor : public JSExecutor { void callNativeModules(const jsi::Value &queue, bool isEndOfBatch); jsi::Value nativeCallSyncHook(const jsi::Value *args, size_t count); jsi::Value nativeRequire(const jsi::Value *args, size_t count); +#ifdef DEBUG + jsi::Value globalEvalWithSourceUrl(const jsi::Value *args, size_t count); +#endif std::shared_ptr runtime_; std::shared_ptr delegate_;