Skip to content

Commit bc20ec8

Browse files
GijsWeteringsmeta-codesync[bot]
authored andcommitted
Implement Page.addScriptToEvaluateOnNewDocument CDP handler (#57248)
Summary: Pull Request resolved: #57248 Implement the CDP `Page.addScriptToEvaluateOnNewDocument` and `Page.removeScriptToEvaluateOnNewDocument` methods in the modern JS inspector (`jsinspector-modern`). `Page.addScriptToEvaluateOnNewDocument` registers a JavaScript snippet that is evaluated in every new JS runtime created for the Host (for example, after a reload), before the application's main bundle runs, matching the standard Chrome DevTools Protocol semantics. This is useful for debugger frontends and tooling that need to install instrumentation ahead of application code. The registered scripts are stored as session state (alongside `Runtime.addBinding` subscriptions in `SessionState`) and replayed onto each new runtime by `RuntimeAgent` via the runtime executor, so they run before any user code and survive reloads. Per CDP semantics the script does not run in the runtime that is current when it is registered; the client triggers `Page.reload` to apply it. `HostAgent` handles both methods, returning the generated script `identifier` from add and removing by `identifier` on remove. Changelog: [General][Added] - Implement the `Page.addScriptToEvaluateOnNewDocument` and `Page.removeScriptToEvaluateOnNewDocument` CDP methods in the modern inspector Reviewed By: hoxyq Differential Revision: D107084044 fbshipit-source-id: 7951028f81f89fbf36418cf8da8a03a7191d228a
1 parent 79adce3 commit bc20ec8

15 files changed

Lines changed: 253 additions & 0 deletions

packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
#include <folly/json.h>
2222
#include <jsinspector-modern/cdp/CdpJson.h>
2323

24+
#include <algorithm>
2425
#include <chrono>
2526
#include <functional>
27+
#include <string>
2628
#include <string_view>
2729

2830
using namespace std::chrono;
@@ -236,6 +238,53 @@ class HostAgent::Impl final {
236238
};
237239
}
238240
}
241+
if (req.method == "Page.addScriptToEvaluateOnNewDocument") {
242+
// @cdp Page.addScriptToEvaluateOnNewDocument registers a script that
243+
// will be evaluated in every new JS runtime created for this Host
244+
// (e.g. after a reload), BEFORE the app's main bundle runs. We store
245+
// it as session state and let each new RuntimeAgent replay it onto its
246+
// runtime, mirroring the handling of @cdp Runtime.addBinding. Per CDP
247+
// semantics the script does NOT run in the runtime that is current
248+
// when it is registered; the client must reload to apply it.
249+
std::string source =
250+
req.params.isObject() && (req.params.count("source") != 0u)
251+
? req.params.at("source").asString()
252+
: std::string();
253+
std::string identifier =
254+
std::to_string(sessionState_.nextScriptToEvaluateOnNewDocumentId++);
255+
sessionState_.scriptsToEvaluateOnNewDocument.push_back(
256+
{.identifier = identifier, .source = std::move(source)});
257+
258+
frontendChannel_(
259+
cdp::jsonResult(
260+
req.id, folly::dynamic::object("identifier", identifier)));
261+
262+
return {
263+
.isFinishedHandlingRequest = true,
264+
.shouldSendOKResponse = false,
265+
};
266+
}
267+
if (req.method == "Page.removeScriptToEvaluateOnNewDocument") {
268+
std::string identifier =
269+
req.params.isObject() && (req.params.count("identifier") != 0u)
270+
? req.params.at("identifier").asString()
271+
: std::string();
272+
auto& scripts = sessionState_.scriptsToEvaluateOnNewDocument;
273+
scripts.erase(
274+
std::remove_if(
275+
scripts.begin(),
276+
scripts.end(),
277+
[&identifier](
278+
const SessionState::ScriptToEvaluateOnNewDocument& script) {
279+
return script.identifier == identifier;
280+
}),
281+
scripts.end());
282+
283+
return {
284+
.isFinishedHandlingRequest = true,
285+
.shouldSendOKResponse = true,
286+
};
287+
}
239288
if (req.method == "Overlay.setPausedInDebuggerMessage") {
240289
auto message =
241290
req.params.isObject() && (req.params.count("message") != 0u)
@@ -397,6 +446,11 @@ class HostAgent::Impl final {
397446
return;
398447
}
399448

449+
if (requestState.isFinishedHandlingRequest) {
450+
// The handler already sent its own response via frontendChannel_.
451+
return;
452+
}
453+
400454
throw NotImplementedException(req.method);
401455
}
402456

packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ RuntimeAgent::RuntimeAgent(
3333
}
3434
}
3535

36+
// Replay any scripts registered via @cdp
37+
// Page.addScriptToEvaluateOnNewDocument onto this newly created runtime, in
38+
// registration order, so they evaluate before the app's main bundle.
39+
for (const auto& script : sessionState_.scriptsToEvaluateOnNewDocument) {
40+
targetController_.installScriptToEvaluateOnNewDocument(script.source);
41+
}
42+
3643
if (sessionState_.isRuntimeDomainEnabled) {
3744
targetController_.notifyDomainStateChanged(
3845
RuntimeTargetController::Domain::Runtime, true, *this);

packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,21 @@ void RuntimeTarget::installBindingHandler(const std::string& bindingName) {
130130
});
131131
}
132132

133+
void RuntimeTarget::installScriptToEvaluateOnNewDocument(
134+
const std::string& source) {
135+
jsExecutor_([source](jsi::Runtime& runtime) {
136+
try {
137+
runtime.evaluateJavaScript(
138+
std::make_shared<jsi::StringBuffer>(source),
139+
"<addScriptToEvaluateOnNewDocument>");
140+
} catch (jsi::JSIException&) {
141+
// Swallow exceptions thrown while evaluating the injected script so a
142+
// faulty script cannot break the app. This mirrors how
143+
// installBindingHandler isolates binding-setup failures.
144+
}
145+
});
146+
}
147+
133148
void RuntimeTarget::installFastRefreshHandler() {
134149
jsExecutor_([selfExecutor = executorFromThis()](jsi::Runtime& runtime) {
135150
auto globalObj = runtime.global();
@@ -313,6 +328,11 @@ void RuntimeTargetController::installBindingHandler(
313328
target_.installBindingHandler(bindingName);
314329
}
315330

331+
void RuntimeTargetController::installScriptToEvaluateOnNewDocument(
332+
const std::string& source) {
333+
target_.installScriptToEvaluateOnNewDocument(source);
334+
}
335+
316336
void RuntimeTargetController::enableSamplingProfiler() {
317337
target_.enableSamplingProfiler();
318338
}

packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ class RuntimeTargetController {
129129
*/
130130
void installBindingHandler(const std::string &bindingName);
131131

132+
/**
133+
* Evaluates the given JavaScript source on the runtime's thread before any
134+
* user code runs. Used to replay @cdp
135+
* Page.addScriptToEvaluateOnNewDocument scripts onto a freshly created
136+
* runtime.
137+
*/
138+
void installScriptToEvaluateOnNewDocument(const std::string &source);
139+
132140
/**
133141
* Notifies the target that an agent has received an enable or disable
134142
* message for the given domain.
@@ -289,6 +297,14 @@ class JSINSPECTOR_EXPORT RuntimeTarget : public EnableExecutorFromThis<RuntimeTa
289297
*/
290298
void installBindingHandler(const std::string &bindingName);
291299

300+
/**
301+
* Evaluates the given JavaScript source on the runtime's thread before any
302+
* user code runs. Used to replay @cdp
303+
* Page.addScriptToEvaluateOnNewDocument scripts onto a freshly created
304+
* runtime.
305+
*/
306+
void installScriptToEvaluateOnNewDocument(const std::string &source);
307+
292308
/**
293309
* Installs any global values we want to expose to framework/user JavaScript
294310
* code.

packages/react-native/ReactCommon/jsinspector-modern/SessionState.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <string_view>
1515
#include <unordered_map>
1616
#include <unordered_set>
17+
#include <vector>
1718

1819
namespace facebook::react::jsinspector_modern {
1920

@@ -43,6 +44,36 @@ struct SessionState {
4344
*/
4445
std::unordered_map<std::string, ExecutionContextSelectorSet> subscribedBindings;
4546

47+
/**
48+
* A single script registered during this session using @cdp
49+
* Page.addScriptToEvaluateOnNewDocument.
50+
*/
51+
struct ScriptToEvaluateOnNewDocument {
52+
/** Opaque identifier returned to the frontend, used by @cdp
53+
* Page.removeScriptToEvaluateOnNewDocument. */
54+
std::string identifier;
55+
/** The JavaScript source to evaluate. */
56+
std::string source;
57+
};
58+
59+
/**
60+
* Scripts registered during this session using @cdp
61+
* Page.addScriptToEvaluateOnNewDocument, in registration order.
62+
*
63+
* Like subscribedBindings, these are treated as session state: each new
64+
* RuntimeAgent replays them onto its runtime so they evaluate before any
65+
* user code (i.e. before the app's main bundle). Per CDP semantics they do
66+
* NOT run in the runtime that is current when they are registered - the
67+
* client must trigger a reload (@cdp Page.reload) for them to take effect.
68+
*/
69+
std::vector<ScriptToEvaluateOnNewDocument> scriptsToEvaluateOnNewDocument;
70+
71+
/**
72+
* Monotonic counter for generating the identifiers returned from @cdp
73+
* Page.addScriptToEvaluateOnNewDocument.
74+
*/
75+
unsigned int nextScriptToEvaluateOnNewDocumentId{1};
76+
4677
/**
4778
* Messages logged through the HostAgent::sendConsoleMessage and
4879
* InstanceAgent::sendConsoleMessage utilities that have not yet been sent to

packages/react-native/ReactCommon/jsinspector-modern/tests/HostTargetTest.cpp

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,59 @@ TEST_F(HostTargetProtocolTest, PageReloadMethod) {
217217
})");
218218
}
219219

220+
TEST_F(HostTargetProtocolTest, PageAddAndRemoveScriptToEvaluateOnNewDocument) {
221+
InSequence s;
222+
223+
// The first registered script gets identifier "1".
224+
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
225+
"id": 1,
226+
"result": {"identifier": "1"}
227+
})")))
228+
.RetiresOnSaturation();
229+
toPage_->sendMessage(R"({
230+
"id": 1,
231+
"method": "Page.addScriptToEvaluateOnNewDocument",
232+
"params": {"source": "globalThis.__a = 1;"}
233+
})");
234+
235+
// The second registration gets a distinct, monotonically increasing id "2".
236+
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
237+
"id": 2,
238+
"result": {"identifier": "2"}
239+
})")))
240+
.RetiresOnSaturation();
241+
toPage_->sendMessage(R"({
242+
"id": 2,
243+
"method": "Page.addScriptToEvaluateOnNewDocument",
244+
"params": {"source": "globalThis.__b = 2;"}
245+
})");
246+
247+
// Removing a registered script succeeds with an empty result.
248+
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
249+
"id": 3,
250+
"result": {}
251+
})")))
252+
.RetiresOnSaturation();
253+
toPage_->sendMessage(R"({
254+
"id": 3,
255+
"method": "Page.removeScriptToEvaluateOnNewDocument",
256+
"params": {"identifier": "1"}
257+
})");
258+
259+
// Removing an unknown identifier is a lenient no-op that still succeeds
260+
// (matching Chrome's behaviour).
261+
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
262+
"id": 4,
263+
"result": {}
264+
})")))
265+
.RetiresOnSaturation();
266+
toPage_->sendMessage(R"({
267+
"id": 4,
268+
"method": "Page.removeScriptToEvaluateOnNewDocument",
269+
"params": {"identifier": "999"}
270+
})");
271+
}
272+
220273
TEST_F(HostTargetProtocolTest, OverlaySetPausedInDebuggerMessageMethod) {
221274
InSequence s;
222275

scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10860,6 +10860,7 @@ class facebook::react::jsinspector_modern::RuntimeTargetController {
1086010860
public void emitTracingStateChange(bool isTracing);
1086110861
public void enableSamplingProfiler();
1086210862
public void installBindingHandler(const std::string& bindingName);
10863+
public void installScriptToEvaluateOnNewDocument(const std::string& source);
1086310864
public void notifyDomainStateChanged(facebook::react::jsinspector_modern::RuntimeTargetController::Domain domain, bool enabled, const facebook::react::jsinspector_modern::RuntimeAgent& notifyingAgent);
1086410865
}
1086510866

@@ -11034,7 +11035,14 @@ struct facebook::react::jsinspector_modern::SessionState {
1103411035
public bool isRuntimeDomainEnabled;
1103511036
public facebook::react::jsinspector_modern::RuntimeAgent::ExportedState lastRuntimeAgentExportedState;
1103611037
public std::unordered_map<std::string, facebook::react::jsinspector_modern::ExecutionContextSelectorSet> subscribedBindings;
11038+
public std::vector<facebook::react::jsinspector_modern::SessionState::ScriptToEvaluateOnNewDocument> scriptsToEvaluateOnNewDocument;
1103711039
public std::vector<facebook::react::jsinspector_modern::SimpleConsoleMessage> pendingSimpleConsoleMessages;
11040+
public unsigned int nextScriptToEvaluateOnNewDocumentId;
11041+
}
11042+
11043+
struct facebook::react::jsinspector_modern::SessionState::ScriptToEvaluateOnNewDocument {
11044+
public std::string identifier;
11045+
public std::string source;
1103811046
}
1103911047

1104011048
struct facebook::react::jsinspector_modern::SimpleConsoleMessage {

scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10486,6 +10486,7 @@ class facebook::react::jsinspector_modern::RuntimeTargetController {
1048610486
public void emitTracingStateChange(bool isTracing);
1048710487
public void enableSamplingProfiler();
1048810488
public void installBindingHandler(const std::string& bindingName);
10489+
public void installScriptToEvaluateOnNewDocument(const std::string& source);
1048910490
public void notifyDomainStateChanged(facebook::react::jsinspector_modern::RuntimeTargetController::Domain domain, bool enabled, const facebook::react::jsinspector_modern::RuntimeAgent& notifyingAgent);
1049010491
}
1049110492

@@ -10660,7 +10661,14 @@ struct facebook::react::jsinspector_modern::SessionState {
1066010661
public bool isRuntimeDomainEnabled;
1066110662
public facebook::react::jsinspector_modern::RuntimeAgent::ExportedState lastRuntimeAgentExportedState;
1066210663
public std::unordered_map<std::string, facebook::react::jsinspector_modern::ExecutionContextSelectorSet> subscribedBindings;
10664+
public std::vector<facebook::react::jsinspector_modern::SessionState::ScriptToEvaluateOnNewDocument> scriptsToEvaluateOnNewDocument;
1066310665
public std::vector<facebook::react::jsinspector_modern::SimpleConsoleMessage> pendingSimpleConsoleMessages;
10666+
public unsigned int nextScriptToEvaluateOnNewDocumentId;
10667+
}
10668+
10669+
struct facebook::react::jsinspector_modern::SessionState::ScriptToEvaluateOnNewDocument {
10670+
public std::string identifier;
10671+
public std::string source;
1066410672
}
1066510673

1066610674
struct facebook::react::jsinspector_modern::SimpleConsoleMessage {

scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10713,6 +10713,7 @@ class facebook::react::jsinspector_modern::RuntimeTargetController {
1071310713
public void emitTracingStateChange(bool isTracing);
1071410714
public void enableSamplingProfiler();
1071510715
public void installBindingHandler(const std::string& bindingName);
10716+
public void installScriptToEvaluateOnNewDocument(const std::string& source);
1071610717
public void notifyDomainStateChanged(facebook::react::jsinspector_modern::RuntimeTargetController::Domain domain, bool enabled, const facebook::react::jsinspector_modern::RuntimeAgent& notifyingAgent);
1071710718
}
1071810719

@@ -10887,7 +10888,14 @@ struct facebook::react::jsinspector_modern::SessionState {
1088710888
public bool isRuntimeDomainEnabled;
1088810889
public facebook::react::jsinspector_modern::RuntimeAgent::ExportedState lastRuntimeAgentExportedState;
1088910890
public std::unordered_map<std::string, facebook::react::jsinspector_modern::ExecutionContextSelectorSet> subscribedBindings;
10891+
public std::vector<facebook::react::jsinspector_modern::SessionState::ScriptToEvaluateOnNewDocument> scriptsToEvaluateOnNewDocument;
1089010892
public std::vector<facebook::react::jsinspector_modern::SimpleConsoleMessage> pendingSimpleConsoleMessages;
10893+
public unsigned int nextScriptToEvaluateOnNewDocumentId;
10894+
}
10895+
10896+
struct facebook::react::jsinspector_modern::SessionState::ScriptToEvaluateOnNewDocument {
10897+
public std::string identifier;
10898+
public std::string source;
1089110899
}
1089210900

1089310901
struct facebook::react::jsinspector_modern::SimpleConsoleMessage {

scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12702,6 +12702,7 @@ class facebook::react::jsinspector_modern::RuntimeTargetController {
1270212702
public void emitTracingStateChange(bool isTracing);
1270312703
public void enableSamplingProfiler();
1270412704
public void installBindingHandler(const std::string& bindingName);
12705+
public void installScriptToEvaluateOnNewDocument(const std::string& source);
1270512706
public void notifyDomainStateChanged(facebook::react::jsinspector_modern::RuntimeTargetController::Domain domain, bool enabled, const facebook::react::jsinspector_modern::RuntimeAgent& notifyingAgent);
1270612707
}
1270712708

@@ -12863,7 +12864,14 @@ struct facebook::react::jsinspector_modern::SessionState {
1286312864
public bool isRuntimeDomainEnabled;
1286412865
public facebook::react::jsinspector_modern::RuntimeAgent::ExportedState lastRuntimeAgentExportedState;
1286512866
public std::unordered_map<std::string, facebook::react::jsinspector_modern::ExecutionContextSelectorSet> subscribedBindings;
12867+
public std::vector<facebook::react::jsinspector_modern::SessionState::ScriptToEvaluateOnNewDocument> scriptsToEvaluateOnNewDocument;
1286612868
public std::vector<facebook::react::jsinspector_modern::SimpleConsoleMessage> pendingSimpleConsoleMessages;
12869+
public unsigned int nextScriptToEvaluateOnNewDocumentId;
12870+
}
12871+
12872+
struct facebook::react::jsinspector_modern::SessionState::ScriptToEvaluateOnNewDocument {
12873+
public std::string identifier;
12874+
public std::string source;
1286712875
}
1286812876

1286912877
struct facebook::react::jsinspector_modern::SimpleConsoleMessage {

0 commit comments

Comments
 (0)