diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp index 9046456fb985..daef4edc43e2 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp @@ -21,8 +21,10 @@ #include #include +#include #include #include +#include #include using namespace std::chrono; @@ -236,6 +238,53 @@ class HostAgent::Impl final { }; } } + if (req.method == "Page.addScriptToEvaluateOnNewDocument") { + // @cdp Page.addScriptToEvaluateOnNewDocument registers a script that + // will be evaluated in every new JS runtime created for this Host + // (e.g. after a reload), BEFORE the app's main bundle runs. We store + // it as session state and let each new RuntimeAgent replay it onto its + // runtime, mirroring the handling of @cdp Runtime.addBinding. Per CDP + // semantics the script does NOT run in the runtime that is current + // when it is registered; the client must reload to apply it. + std::string source = + req.params.isObject() && (req.params.count("source") != 0u) + ? req.params.at("source").asString() + : std::string(); + std::string identifier = + std::to_string(sessionState_.nextScriptToEvaluateOnNewDocumentId++); + sessionState_.scriptsToEvaluateOnNewDocument.push_back( + {.identifier = identifier, .source = std::move(source)}); + + frontendChannel_( + cdp::jsonResult( + req.id, folly::dynamic::object("identifier", identifier))); + + return { + .isFinishedHandlingRequest = true, + .shouldSendOKResponse = false, + }; + } + if (req.method == "Page.removeScriptToEvaluateOnNewDocument") { + std::string identifier = + req.params.isObject() && (req.params.count("identifier") != 0u) + ? req.params.at("identifier").asString() + : std::string(); + auto& scripts = sessionState_.scriptsToEvaluateOnNewDocument; + scripts.erase( + std::remove_if( + scripts.begin(), + scripts.end(), + [&identifier]( + const SessionState::ScriptToEvaluateOnNewDocument& script) { + return script.identifier == identifier; + }), + scripts.end()); + + return { + .isFinishedHandlingRequest = true, + .shouldSendOKResponse = true, + }; + } if (req.method == "Overlay.setPausedInDebuggerMessage") { auto message = req.params.isObject() && (req.params.count("message") != 0u) @@ -397,6 +446,11 @@ class HostAgent::Impl final { return; } + if (requestState.isFinishedHandlingRequest) { + // The handler already sent its own response via frontendChannel_. + return; + } + throw NotImplementedException(req.method); } diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp index 0f3120518ac9..009214586791 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp @@ -33,6 +33,13 @@ RuntimeAgent::RuntimeAgent( } } + // Replay any scripts registered via @cdp + // Page.addScriptToEvaluateOnNewDocument onto this newly created runtime, in + // registration order, so they evaluate before the app's main bundle. + for (const auto& script : sessionState_.scriptsToEvaluateOnNewDocument) { + targetController_.installScriptToEvaluateOnNewDocument(script.source); + } + if (sessionState_.isRuntimeDomainEnabled) { targetController_.notifyDomainStateChanged( RuntimeTargetController::Domain::Runtime, true, *this); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp index 946983d928de..811d0ea143eb 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp @@ -130,6 +130,21 @@ void RuntimeTarget::installBindingHandler(const std::string& bindingName) { }); } +void RuntimeTarget::installScriptToEvaluateOnNewDocument( + const std::string& source) { + jsExecutor_([source](jsi::Runtime& runtime) { + try { + runtime.evaluateJavaScript( + std::make_shared(source), + ""); + } catch (jsi::JSIException&) { + // Swallow exceptions thrown while evaluating the injected script so a + // faulty script cannot break the app. This mirrors how + // installBindingHandler isolates binding-setup failures. + } + }); +} + void RuntimeTarget::installFastRefreshHandler() { jsExecutor_([selfExecutor = executorFromThis()](jsi::Runtime& runtime) { auto globalObj = runtime.global(); @@ -313,6 +328,11 @@ void RuntimeTargetController::installBindingHandler( target_.installBindingHandler(bindingName); } +void RuntimeTargetController::installScriptToEvaluateOnNewDocument( + const std::string& source) { + target_.installScriptToEvaluateOnNewDocument(source); +} + void RuntimeTargetController::enableSamplingProfiler() { target_.enableSamplingProfiler(); } diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h index 75eb87410c33..0136ba376eed 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h @@ -129,6 +129,14 @@ class RuntimeTargetController { */ void installBindingHandler(const std::string &bindingName); + /** + * Evaluates the given JavaScript source on the runtime's thread before any + * user code runs. Used to replay @cdp + * Page.addScriptToEvaluateOnNewDocument scripts onto a freshly created + * runtime. + */ + void installScriptToEvaluateOnNewDocument(const std::string &source); + /** * Notifies the target that an agent has received an enable or disable * message for the given domain. @@ -289,6 +297,14 @@ class JSINSPECTOR_EXPORT RuntimeTarget : public EnableExecutorFromThis #include #include +#include namespace facebook::react::jsinspector_modern { @@ -43,6 +44,36 @@ struct SessionState { */ std::unordered_map subscribedBindings; + /** + * A single script registered during this session using @cdp + * Page.addScriptToEvaluateOnNewDocument. + */ + struct ScriptToEvaluateOnNewDocument { + /** Opaque identifier returned to the frontend, used by @cdp + * Page.removeScriptToEvaluateOnNewDocument. */ + std::string identifier; + /** The JavaScript source to evaluate. */ + std::string source; + }; + + /** + * Scripts registered during this session using @cdp + * Page.addScriptToEvaluateOnNewDocument, in registration order. + * + * Like subscribedBindings, these are treated as session state: each new + * RuntimeAgent replays them onto its runtime so they evaluate before any + * user code (i.e. before the app's main bundle). Per CDP semantics they do + * NOT run in the runtime that is current when they are registered - the + * client must trigger a reload (@cdp Page.reload) for them to take effect. + */ + std::vector scriptsToEvaluateOnNewDocument; + + /** + * Monotonic counter for generating the identifiers returned from @cdp + * Page.addScriptToEvaluateOnNewDocument. + */ + unsigned int nextScriptToEvaluateOnNewDocumentId{1}; + /** * Messages logged through the HostAgent::sendConsoleMessage and * InstanceAgent::sendConsoleMessage utilities that have not yet been sent to diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp index a4bca34e82b7..e42d4b3cbfec 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp @@ -217,6 +217,59 @@ TEST_F(HostTargetProtocolTest, PageReloadMethod) { })"); } +TEST_F(HostTargetProtocolTest, PageAddAndRemoveScriptToEvaluateOnNewDocument) { + InSequence s; + + // The first registered script gets identifier "1". + EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({ + "id": 1, + "result": {"identifier": "1"} + })"))) + .RetiresOnSaturation(); + toPage_->sendMessage(R"({ + "id": 1, + "method": "Page.addScriptToEvaluateOnNewDocument", + "params": {"source": "globalThis.__a = 1;"} + })"); + + // The second registration gets a distinct, monotonically increasing id "2". + EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({ + "id": 2, + "result": {"identifier": "2"} + })"))) + .RetiresOnSaturation(); + toPage_->sendMessage(R"({ + "id": 2, + "method": "Page.addScriptToEvaluateOnNewDocument", + "params": {"source": "globalThis.__b = 2;"} + })"); + + // Removing a registered script succeeds with an empty result. + EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({ + "id": 3, + "result": {} + })"))) + .RetiresOnSaturation(); + toPage_->sendMessage(R"({ + "id": 3, + "method": "Page.removeScriptToEvaluateOnNewDocument", + "params": {"identifier": "1"} + })"); + + // Removing an unknown identifier is a lenient no-op that still succeeds + // (matching Chrome's behaviour). + EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({ + "id": 4, + "result": {} + })"))) + .RetiresOnSaturation(); + toPage_->sendMessage(R"({ + "id": 4, + "method": "Page.removeScriptToEvaluateOnNewDocument", + "params": {"identifier": "999"} + })"); +} + TEST_F(HostTargetProtocolTest, OverlaySetPausedInDebuggerMessageMethod) { InSequence s; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index beaf187d8273..0a97c58f422f 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -10866,6 +10866,7 @@ class facebook::react::jsinspector_modern::RuntimeTargetController { public void emitTracingStateChange(bool isTracing); public void enableSamplingProfiler(); public void installBindingHandler(const std::string& bindingName); + public void installScriptToEvaluateOnNewDocument(const std::string& source); public void notifyDomainStateChanged(facebook::react::jsinspector_modern::RuntimeTargetController::Domain domain, bool enabled, const facebook::react::jsinspector_modern::RuntimeAgent& notifyingAgent); } @@ -11040,7 +11041,14 @@ struct facebook::react::jsinspector_modern::SessionState { public bool isRuntimeDomainEnabled; public facebook::react::jsinspector_modern::RuntimeAgent::ExportedState lastRuntimeAgentExportedState; public std::unordered_map subscribedBindings; + public std::vector scriptsToEvaluateOnNewDocument; public std::vector pendingSimpleConsoleMessages; + public unsigned int nextScriptToEvaluateOnNewDocumentId; +} + +struct facebook::react::jsinspector_modern::SessionState::ScriptToEvaluateOnNewDocument { + public std::string identifier; + public std::string source; } struct facebook::react::jsinspector_modern::SimpleConsoleMessage { diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index cd0a69f6db16..298d3ec47f70 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -10492,6 +10492,7 @@ class facebook::react::jsinspector_modern::RuntimeTargetController { public void emitTracingStateChange(bool isTracing); public void enableSamplingProfiler(); public void installBindingHandler(const std::string& bindingName); + public void installScriptToEvaluateOnNewDocument(const std::string& source); public void notifyDomainStateChanged(facebook::react::jsinspector_modern::RuntimeTargetController::Domain domain, bool enabled, const facebook::react::jsinspector_modern::RuntimeAgent& notifyingAgent); } @@ -10666,7 +10667,14 @@ struct facebook::react::jsinspector_modern::SessionState { public bool isRuntimeDomainEnabled; public facebook::react::jsinspector_modern::RuntimeAgent::ExportedState lastRuntimeAgentExportedState; public std::unordered_map subscribedBindings; + public std::vector scriptsToEvaluateOnNewDocument; public std::vector pendingSimpleConsoleMessages; + public unsigned int nextScriptToEvaluateOnNewDocumentId; +} + +struct facebook::react::jsinspector_modern::SessionState::ScriptToEvaluateOnNewDocument { + public std::string identifier; + public std::string source; } struct facebook::react::jsinspector_modern::SimpleConsoleMessage { diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index bdc9fdc54f0f..66f5284af735 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -10719,6 +10719,7 @@ class facebook::react::jsinspector_modern::RuntimeTargetController { public void emitTracingStateChange(bool isTracing); public void enableSamplingProfiler(); public void installBindingHandler(const std::string& bindingName); + public void installScriptToEvaluateOnNewDocument(const std::string& source); public void notifyDomainStateChanged(facebook::react::jsinspector_modern::RuntimeTargetController::Domain domain, bool enabled, const facebook::react::jsinspector_modern::RuntimeAgent& notifyingAgent); } @@ -10893,7 +10894,14 @@ struct facebook::react::jsinspector_modern::SessionState { public bool isRuntimeDomainEnabled; public facebook::react::jsinspector_modern::RuntimeAgent::ExportedState lastRuntimeAgentExportedState; public std::unordered_map subscribedBindings; + public std::vector scriptsToEvaluateOnNewDocument; public std::vector pendingSimpleConsoleMessages; + public unsigned int nextScriptToEvaluateOnNewDocumentId; +} + +struct facebook::react::jsinspector_modern::SessionState::ScriptToEvaluateOnNewDocument { + public std::string identifier; + public std::string source; } struct facebook::react::jsinspector_modern::SimpleConsoleMessage { diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 323f80e5161f..f86e2ab5e56e 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -12708,6 +12708,7 @@ class facebook::react::jsinspector_modern::RuntimeTargetController { public void emitTracingStateChange(bool isTracing); public void enableSamplingProfiler(); public void installBindingHandler(const std::string& bindingName); + public void installScriptToEvaluateOnNewDocument(const std::string& source); public void notifyDomainStateChanged(facebook::react::jsinspector_modern::RuntimeTargetController::Domain domain, bool enabled, const facebook::react::jsinspector_modern::RuntimeAgent& notifyingAgent); } @@ -12869,7 +12870,14 @@ struct facebook::react::jsinspector_modern::SessionState { public bool isRuntimeDomainEnabled; public facebook::react::jsinspector_modern::RuntimeAgent::ExportedState lastRuntimeAgentExportedState; public std::unordered_map subscribedBindings; + public std::vector scriptsToEvaluateOnNewDocument; public std::vector pendingSimpleConsoleMessages; + public unsigned int nextScriptToEvaluateOnNewDocumentId; +} + +struct facebook::react::jsinspector_modern::SessionState::ScriptToEvaluateOnNewDocument { + public std::string identifier; + public std::string source; } struct facebook::react::jsinspector_modern::SimpleConsoleMessage { diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 0e9f7bf4394e..30a90e298b44 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -12396,6 +12396,7 @@ class facebook::react::jsinspector_modern::RuntimeTargetController { public void emitTracingStateChange(bool isTracing); public void enableSamplingProfiler(); public void installBindingHandler(const std::string& bindingName); + public void installScriptToEvaluateOnNewDocument(const std::string& source); public void notifyDomainStateChanged(facebook::react::jsinspector_modern::RuntimeTargetController::Domain domain, bool enabled, const facebook::react::jsinspector_modern::RuntimeAgent& notifyingAgent); } @@ -12557,7 +12558,14 @@ struct facebook::react::jsinspector_modern::SessionState { public bool isRuntimeDomainEnabled; public facebook::react::jsinspector_modern::RuntimeAgent::ExportedState lastRuntimeAgentExportedState; public std::unordered_map subscribedBindings; + public std::vector scriptsToEvaluateOnNewDocument; public std::vector pendingSimpleConsoleMessages; + public unsigned int nextScriptToEvaluateOnNewDocumentId; +} + +struct facebook::react::jsinspector_modern::SessionState::ScriptToEvaluateOnNewDocument { + public std::string identifier; + public std::string source; } struct facebook::react::jsinspector_modern::SimpleConsoleMessage { diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 270a0c797f5e..7321d50f1f7f 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -12571,6 +12571,7 @@ class facebook::react::jsinspector_modern::RuntimeTargetController { public void emitTracingStateChange(bool isTracing); public void enableSamplingProfiler(); public void installBindingHandler(const std::string& bindingName); + public void installScriptToEvaluateOnNewDocument(const std::string& source); public void notifyDomainStateChanged(facebook::react::jsinspector_modern::RuntimeTargetController::Domain domain, bool enabled, const facebook::react::jsinspector_modern::RuntimeAgent& notifyingAgent); } @@ -12732,7 +12733,14 @@ struct facebook::react::jsinspector_modern::SessionState { public bool isRuntimeDomainEnabled; public facebook::react::jsinspector_modern::RuntimeAgent::ExportedState lastRuntimeAgentExportedState; public std::unordered_map subscribedBindings; + public std::vector scriptsToEvaluateOnNewDocument; public std::vector pendingSimpleConsoleMessages; + public unsigned int nextScriptToEvaluateOnNewDocumentId; +} + +struct facebook::react::jsinspector_modern::SessionState::ScriptToEvaluateOnNewDocument { + public std::string identifier; + public std::string source; } struct facebook::react::jsinspector_modern::SimpleConsoleMessage { diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 39c05b7ffbe5..457bfe50f04e 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -7869,6 +7869,7 @@ class facebook::react::jsinspector_modern::RuntimeTargetController { public void emitTracingStateChange(bool isTracing); public void enableSamplingProfiler(); public void installBindingHandler(const std::string& bindingName); + public void installScriptToEvaluateOnNewDocument(const std::string& source); public void notifyDomainStateChanged(facebook::react::jsinspector_modern::RuntimeTargetController::Domain domain, bool enabled, const facebook::react::jsinspector_modern::RuntimeAgent& notifyingAgent); } @@ -8030,7 +8031,14 @@ struct facebook::react::jsinspector_modern::SessionState { public bool isRuntimeDomainEnabled; public facebook::react::jsinspector_modern::RuntimeAgent::ExportedState lastRuntimeAgentExportedState; public std::unordered_map subscribedBindings; + public std::vector scriptsToEvaluateOnNewDocument; public std::vector pendingSimpleConsoleMessages; + public unsigned int nextScriptToEvaluateOnNewDocumentId; +} + +struct facebook::react::jsinspector_modern::SessionState::ScriptToEvaluateOnNewDocument { + public std::string identifier; + public std::string source; } struct facebook::react::jsinspector_modern::SimpleConsoleMessage { diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index e0293739574a..9856033cd526 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -7697,6 +7697,7 @@ class facebook::react::jsinspector_modern::RuntimeTargetController { public void emitTracingStateChange(bool isTracing); public void enableSamplingProfiler(); public void installBindingHandler(const std::string& bindingName); + public void installScriptToEvaluateOnNewDocument(const std::string& source); public void notifyDomainStateChanged(facebook::react::jsinspector_modern::RuntimeTargetController::Domain domain, bool enabled, const facebook::react::jsinspector_modern::RuntimeAgent& notifyingAgent); } @@ -7858,7 +7859,14 @@ struct facebook::react::jsinspector_modern::SessionState { public bool isRuntimeDomainEnabled; public facebook::react::jsinspector_modern::RuntimeAgent::ExportedState lastRuntimeAgentExportedState; public std::unordered_map subscribedBindings; + public std::vector scriptsToEvaluateOnNewDocument; public std::vector pendingSimpleConsoleMessages; + public unsigned int nextScriptToEvaluateOnNewDocumentId; +} + +struct facebook::react::jsinspector_modern::SessionState::ScriptToEvaluateOnNewDocument { + public std::string identifier; + public std::string source; } struct facebook::react::jsinspector_modern::SimpleConsoleMessage { diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 0ccad701dc85..4b2cf34b7bc3 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -7860,6 +7860,7 @@ class facebook::react::jsinspector_modern::RuntimeTargetController { public void emitTracingStateChange(bool isTracing); public void enableSamplingProfiler(); public void installBindingHandler(const std::string& bindingName); + public void installScriptToEvaluateOnNewDocument(const std::string& source); public void notifyDomainStateChanged(facebook::react::jsinspector_modern::RuntimeTargetController::Domain domain, bool enabled, const facebook::react::jsinspector_modern::RuntimeAgent& notifyingAgent); } @@ -8021,7 +8022,14 @@ struct facebook::react::jsinspector_modern::SessionState { public bool isRuntimeDomainEnabled; public facebook::react::jsinspector_modern::RuntimeAgent::ExportedState lastRuntimeAgentExportedState; public std::unordered_map subscribedBindings; + public std::vector scriptsToEvaluateOnNewDocument; public std::vector pendingSimpleConsoleMessages; + public unsigned int nextScriptToEvaluateOnNewDocumentId; +} + +struct facebook::react::jsinspector_modern::SessionState::ScriptToEvaluateOnNewDocument { + public std::string identifier; + public std::string source; } struct facebook::react::jsinspector_modern::SimpleConsoleMessage {