@react-native/dev-middleware 0.73.0 → 0.73.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +77 -3
  2. package/dist/createDevMiddleware.d.ts +53 -0
  3. package/dist/createDevMiddleware.js +53 -6
  4. package/dist/createDevMiddleware.js.flow +44 -15
  5. package/dist/index.d.ts +12 -0
  6. package/dist/index.flow.d.ts +14 -0
  7. package/dist/index.flow.js.flow +5 -2
  8. package/dist/index.js +14 -1
  9. package/dist/index.js.flow +1 -9
  10. package/dist/inspector-proxy/Device.d.ts +90 -0
  11. package/dist/inspector-proxy/Device.js +708 -0
  12. package/dist/inspector-proxy/Device.js.flow +97 -0
  13. package/dist/inspector-proxy/DeviceEventReporter.d.ts +53 -0
  14. package/dist/inspector-proxy/DeviceEventReporter.js +162 -0
  15. package/dist/inspector-proxy/DeviceEventReporter.js.flow +65 -0
  16. package/dist/inspector-proxy/InspectorProxy.d.ts +61 -0
  17. package/dist/inspector-proxy/InspectorProxy.js +212 -0
  18. package/dist/inspector-proxy/InspectorProxy.js.flow +65 -0
  19. package/dist/inspector-proxy/types.d.ts +78 -0
  20. package/dist/inspector-proxy/types.js +1 -0
  21. package/dist/inspector-proxy/types.js.flow +135 -0
  22. package/dist/middleware/openDebuggerMiddleware.d.ts +37 -0
  23. package/dist/middleware/openDebuggerMiddleware.js +78 -24
  24. package/dist/middleware/openDebuggerMiddleware.js.flow +15 -64
  25. package/dist/types/BrowserLauncher.d.ts +27 -0
  26. package/dist/types/BrowserLauncher.js +1 -0
  27. package/dist/types/BrowserLauncher.js.flow +31 -0
  28. package/dist/types/EventReporter.d.ts +69 -0
  29. package/dist/types/EventReporter.js +1 -0
  30. package/dist/types/EventReporter.js.flow +81 -0
  31. package/dist/types/Experiments.d.ts +25 -0
  32. package/dist/types/Experiments.js +1 -0
  33. package/dist/types/Experiments.js.flow +27 -0
  34. package/dist/types/Logger.d.ts +16 -0
  35. package/dist/utils/DefaultBrowserLauncher.d.ts +18 -0
  36. package/dist/utils/DefaultBrowserLauncher.js +77 -0
  37. package/dist/utils/DefaultBrowserLauncher.js.flow +20 -0
  38. package/dist/utils/getDevToolsFrontendUrl.d.ts +21 -0
  39. package/dist/utils/getDevToolsFrontendUrl.js +48 -0
  40. package/dist/utils/getDevToolsFrontendUrl.js.flow +21 -0
  41. package/package.json +6 -1
  42. package/dist/utils/getDevServerUrl.js +0 -37
  43. package/dist/utils/getDevServerUrl.js.flow +0 -32
  44. package/dist/utils/launchChromeDevTools.js +0 -44
  45. package/dist/utils/launchChromeDevTools.js.flow +0 -38
  46. package/dist/utils/launchDebuggerAppWindow.js +0 -58
  47. package/dist/utils/launchDebuggerAppWindow.js.flow +0 -56
  48. package/dist/utils/queryInspectorTargets.js +0 -35
  49. package/dist/utils/queryInspectorTargets.js.flow +0 -40
@@ -0,0 +1,708 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true,
5
+ });
6
+ exports.default = void 0;
7
+ var _DeviceEventReporter = _interopRequireDefault(
8
+ require("./DeviceEventReporter")
9
+ );
10
+ var fs = _interopRequireWildcard(require("fs"));
11
+ var path = _interopRequireWildcard(require("path"));
12
+ var _nodeFetch = _interopRequireDefault(require("node-fetch"));
13
+ var _ws = _interopRequireDefault(require("ws"));
14
+ function _getRequireWildcardCache(nodeInterop) {
15
+ if (typeof WeakMap !== "function") return null;
16
+ var cacheBabelInterop = new WeakMap();
17
+ var cacheNodeInterop = new WeakMap();
18
+ return (_getRequireWildcardCache = function (nodeInterop) {
19
+ return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
20
+ })(nodeInterop);
21
+ }
22
+ function _interopRequireWildcard(obj, nodeInterop) {
23
+ if (!nodeInterop && obj && obj.__esModule) {
24
+ return obj;
25
+ }
26
+ if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
27
+ return { default: obj };
28
+ }
29
+ var cache = _getRequireWildcardCache(nodeInterop);
30
+ if (cache && cache.has(obj)) {
31
+ return cache.get(obj);
32
+ }
33
+ var newObj = {};
34
+ var hasPropertyDescriptor =
35
+ Object.defineProperty && Object.getOwnPropertyDescriptor;
36
+ for (var key in obj) {
37
+ if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
38
+ var desc = hasPropertyDescriptor
39
+ ? Object.getOwnPropertyDescriptor(obj, key)
40
+ : null;
41
+ if (desc && (desc.get || desc.set)) {
42
+ Object.defineProperty(newObj, key, desc);
43
+ } else {
44
+ newObj[key] = obj[key];
45
+ }
46
+ }
47
+ }
48
+ newObj.default = obj;
49
+ if (cache) {
50
+ cache.set(obj, newObj);
51
+ }
52
+ return newObj;
53
+ }
54
+ function _interopRequireDefault(obj) {
55
+ return obj && obj.__esModule ? obj : { default: obj };
56
+ }
57
+ /**
58
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
59
+ *
60
+ * This source code is licensed under the MIT license found in the
61
+ * LICENSE file in the root directory of this source tree.
62
+ *
63
+ *
64
+ * @format
65
+ * @oncall react_native
66
+ */
67
+
68
+ const debug = require("debug")("Metro:InspectorProxy");
69
+ const PAGES_POLLING_INTERVAL = 1000;
70
+
71
+ // Android's stock emulator and other emulators such as genymotion use a standard localhost alias.
72
+ const EMULATOR_LOCALHOST_ADDRESSES = ["10.0.2.2", "10.0.3.2"];
73
+
74
+ // Prefix for script URLs that are alphanumeric IDs. See comment in _processMessageFromDevice method for
75
+ // more details.
76
+ const FILE_PREFIX = "file://";
77
+ const REACT_NATIVE_RELOADABLE_PAGE_ID = "-1";
78
+
79
+ /**
80
+ * Device class represents single device connection to Inspector Proxy. Each device
81
+ * can have multiple inspectable pages.
82
+ */
83
+ class Device {
84
+ // ID of the device.
85
+
86
+ // Name of the device.
87
+
88
+ // Package name of the app.
89
+
90
+ // Stores socket connection between Inspector Proxy and device.
91
+
92
+ // Stores last list of device's pages.
93
+
94
+ // Stores information about currently connected debugger (if any).
95
+ _debuggerConnection = null;
96
+
97
+ // Last known Page ID of the React Native page.
98
+ // This is used by debugger connections that don't have PageID specified
99
+ // (and will interact with the latest React Native page).
100
+ _lastConnectedReactNativePage = null;
101
+
102
+ // Whether we are in the middle of a reload in the REACT_NATIVE_RELOADABLE_PAGE.
103
+ _isReloading = false;
104
+
105
+ // The previous "GetPages" message, for deduplication in debug logs.
106
+ _lastGetPagesMessage = "";
107
+
108
+ // Mapping built from scriptParsed events and used to fetch file content in `Debugger.getScriptSource`.
109
+ _scriptIdToSourcePathMapping = new Map();
110
+
111
+ // Root of the project used for relative to absolute source path conversion.
112
+
113
+ constructor(id, name, app, socket, projectRoot, eventReporter) {
114
+ this._id = id;
115
+ this._name = name;
116
+ this._app = app;
117
+ this._pages = [];
118
+ this._deviceSocket = socket;
119
+ this._projectRoot = projectRoot;
120
+ this._deviceEventReporter = eventReporter
121
+ ? new _DeviceEventReporter.default(eventReporter, {
122
+ deviceId: id,
123
+ deviceName: name,
124
+ appId: app,
125
+ })
126
+ : null;
127
+
128
+ // $FlowFixMe[incompatible-call]
129
+ this._deviceSocket.on("message", (message) => {
130
+ const parsedMessage = JSON.parse(message);
131
+ if (parsedMessage.event === "getPages") {
132
+ // There's a 'getPages' message every second, so only show them if they change
133
+ if (message !== this._lastGetPagesMessage) {
134
+ debug(
135
+ "(Debugger) (Proxy) <- (Device), getPages ping has changed: " +
136
+ message
137
+ );
138
+ this._lastGetPagesMessage = message;
139
+ }
140
+ } else {
141
+ debug("(Debugger) (Proxy) <- (Device): " + message);
142
+ }
143
+ this._handleMessageFromDevice(parsedMessage);
144
+ });
145
+ this._deviceSocket.on("close", () => {
146
+ this._deviceEventReporter?.logDisconnection("device");
147
+ // Device disconnected - close debugger connection.
148
+ if (this._debuggerConnection) {
149
+ this._debuggerConnection.socket.close();
150
+ this._debuggerConnection = null;
151
+ }
152
+ });
153
+ this._setPagesPolling();
154
+ }
155
+ getName() {
156
+ return this._name;
157
+ }
158
+ getApp() {
159
+ return this._app;
160
+ }
161
+ getPagesList() {
162
+ if (this._lastConnectedReactNativePage) {
163
+ const reactNativeReloadablePage = {
164
+ id: REACT_NATIVE_RELOADABLE_PAGE_ID,
165
+ title: "React Native Experimental (Improved Chrome Reloads)",
166
+ vm: "don't use",
167
+ app: this._app,
168
+ };
169
+ return this._pages.concat(reactNativeReloadablePage);
170
+ } else {
171
+ return this._pages;
172
+ }
173
+ }
174
+
175
+ // Handles new debugger connection to this device:
176
+ // 1. Sends connect event to device
177
+ // 2. Forwards all messages from the debugger to device as wrappedEvent
178
+ // 3. Sends disconnect event to device when debugger connection socket closes.
179
+ handleDebuggerConnection(socket, pageId, metadata) {
180
+ // Clear any commands we were waiting on.
181
+ this._deviceEventReporter?.logDisconnection("debugger");
182
+ this._deviceEventReporter?.logConnection("debugger", {
183
+ pageId,
184
+ frontendUserAgent: metadata.userAgent,
185
+ });
186
+
187
+ // Disconnect current debugger if we already have debugger connected.
188
+ if (this._debuggerConnection) {
189
+ this._debuggerConnection.socket.close();
190
+ this._debuggerConnection = null;
191
+ }
192
+ const debuggerInfo = {
193
+ socket,
194
+ prependedFilePrefix: false,
195
+ pageId,
196
+ userAgent: metadata.userAgent,
197
+ };
198
+ this._debuggerConnection = debuggerInfo;
199
+ debug(`Got new debugger connection for page ${pageId} of ${this._name}`);
200
+ this._sendMessageToDevice({
201
+ event: "connect",
202
+ payload: {
203
+ pageId: this._mapToDevicePageId(pageId),
204
+ },
205
+ });
206
+
207
+ // $FlowFixMe[incompatible-call]
208
+ socket.on("message", (message) => {
209
+ debug("(Debugger) -> (Proxy) (Device): " + message);
210
+ const debuggerRequest = JSON.parse(message);
211
+ this._deviceEventReporter?.logRequest(debuggerRequest, "debugger", {
212
+ pageId: this._debuggerConnection?.pageId ?? null,
213
+ frontendUserAgent: metadata.userAgent,
214
+ });
215
+ const handled = this._interceptMessageFromDebugger(
216
+ debuggerRequest,
217
+ debuggerInfo,
218
+ socket
219
+ );
220
+ if (!handled) {
221
+ this._sendMessageToDevice({
222
+ event: "wrappedEvent",
223
+ payload: {
224
+ pageId: this._mapToDevicePageId(pageId),
225
+ wrappedEvent: JSON.stringify(debuggerRequest),
226
+ },
227
+ });
228
+ }
229
+ });
230
+ socket.on("close", () => {
231
+ debug(`Debugger for page ${pageId} and ${this._name} disconnected.`);
232
+ this._deviceEventReporter?.logDisconnection("debugger");
233
+ this._sendMessageToDevice({
234
+ event: "disconnect",
235
+ payload: {
236
+ pageId: this._mapToDevicePageId(pageId),
237
+ },
238
+ });
239
+ this._debuggerConnection = null;
240
+ });
241
+
242
+ // $FlowFixMe[method-unbinding]
243
+ const sendFunc = socket.send;
244
+ // $FlowFixMe[cannot-write]
245
+ socket.send = function (message) {
246
+ debug("(Debugger) <- (Proxy) (Device): " + message);
247
+ return sendFunc.call(socket, message);
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Handles cleaning up a duplicate device connection, by client-side device ID.
253
+ * 1. Checks if the same device is attempting to reconnect for the same app.
254
+ * 2. If not, close both the device and debugger socket.
255
+ * 3. If the debugger connection can be reused, close the device socket only.
256
+ *
257
+ * This allows users to reload the app, either as result of a crash, or manually
258
+ * reloading, without having to restart the debugger.
259
+ */
260
+ handleDuplicateDeviceConnection(newDevice) {
261
+ if (
262
+ this._app !== newDevice.getApp() ||
263
+ this._name !== newDevice.getName()
264
+ ) {
265
+ this._deviceSocket.close();
266
+ this._debuggerConnection?.socket.close();
267
+ }
268
+ const oldDebugger = this._debuggerConnection;
269
+ this._debuggerConnection = null;
270
+ if (oldDebugger) {
271
+ oldDebugger.socket.removeAllListeners();
272
+ this._deviceSocket.close();
273
+ newDevice.handleDebuggerConnection(
274
+ oldDebugger.socket,
275
+ oldDebugger.pageId,
276
+ {
277
+ userAgent: oldDebugger.userAgent,
278
+ }
279
+ );
280
+ }
281
+ }
282
+
283
+ // Handles messages received from device:
284
+ // 1. For getPages responses updates local _pages list.
285
+ // 2. All other messages are forwarded to debugger as wrappedEvent.
286
+ //
287
+ // In the future more logic will be added to this method for modifying
288
+ // some of the messages (like updating messages with source maps and file
289
+ // locations).
290
+ _handleMessageFromDevice(message) {
291
+ if (message.event === "getPages") {
292
+ this._pages = message.payload;
293
+
294
+ // Check if device have new React Native page.
295
+ // There is usually no more than 2-3 pages per device so this operation
296
+ // is not expensive.
297
+ // TODO(hypuk): It is better for VM to send update event when new page is
298
+ // created instead of manually checking this on every getPages result.
299
+ for (let i = 0; i < this._pages.length; ++i) {
300
+ if (this._pages[i].title.indexOf("React") >= 0) {
301
+ if (this._pages[i].id !== this._lastConnectedReactNativePage?.id) {
302
+ this._newReactNativePage(this._pages[i]);
303
+ break;
304
+ }
305
+ }
306
+ }
307
+ } else if (message.event === "disconnect") {
308
+ // Device sends disconnect events only when page is reloaded or
309
+ // if debugger socket was disconnected.
310
+ const pageId = message.payload.pageId;
311
+ const debuggerSocket = this._debuggerConnection
312
+ ? this._debuggerConnection.socket
313
+ : null;
314
+ if (debuggerSocket && debuggerSocket.readyState === _ws.default.OPEN) {
315
+ if (
316
+ this._debuggerConnection != null &&
317
+ this._debuggerConnection.pageId !== REACT_NATIVE_RELOADABLE_PAGE_ID
318
+ ) {
319
+ debug(`Page ${pageId} is reloading.`);
320
+ debuggerSocket.send(
321
+ JSON.stringify({
322
+ method: "reload",
323
+ })
324
+ );
325
+ }
326
+ }
327
+ } else if (message.event === "wrappedEvent") {
328
+ if (this._debuggerConnection == null) {
329
+ return;
330
+ }
331
+
332
+ // FIXME: Is it possible that we received message for pageID that does not
333
+ // correspond to current debugger connection?
334
+
335
+ const debuggerSocket = this._debuggerConnection.socket;
336
+ if (
337
+ debuggerSocket == null ||
338
+ debuggerSocket.readyState !== _ws.default.OPEN
339
+ ) {
340
+ // TODO(hypuk): Send error back to device?
341
+ return;
342
+ }
343
+ const parsedPayload = JSON.parse(message.payload.wrappedEvent);
344
+ if ("id" in parsedPayload) {
345
+ this._deviceEventReporter?.logResponse(parsedPayload, "device", {
346
+ pageId: this._debuggerConnection?.pageId ?? null,
347
+ frontendUserAgent: this._debuggerConnection?.userAgent ?? null,
348
+ });
349
+ }
350
+ if (this._debuggerConnection) {
351
+ // Wrapping just to make flow happy :)
352
+ // $FlowFixMe[unused-promise]
353
+ this._processMessageFromDevice(
354
+ parsedPayload,
355
+ this._debuggerConnection
356
+ ).then(() => {
357
+ const messageToSend = JSON.stringify(parsedPayload);
358
+ debuggerSocket.send(messageToSend);
359
+ });
360
+ }
361
+ }
362
+ }
363
+
364
+ // Sends single message to device.
365
+ _sendMessageToDevice(message) {
366
+ try {
367
+ if (message.event !== "getPages") {
368
+ debug("(Debugger) (Proxy) -> (Device): " + JSON.stringify(message));
369
+ }
370
+ this._deviceSocket.send(JSON.stringify(message));
371
+ } catch (error) {}
372
+ }
373
+
374
+ // Sends 'getPages' request to device every PAGES_POLLING_INTERVAL milliseconds.
375
+ _setPagesPolling() {
376
+ setInterval(
377
+ () =>
378
+ this._sendMessageToDevice({
379
+ event: "getPages",
380
+ }),
381
+ PAGES_POLLING_INTERVAL
382
+ );
383
+ }
384
+
385
+ // We received new React Native Page ID.
386
+ _newReactNativePage(page) {
387
+ debug(`React Native page updated to ${page.id}`);
388
+ if (
389
+ this._debuggerConnection == null ||
390
+ this._debuggerConnection.pageId !== REACT_NATIVE_RELOADABLE_PAGE_ID
391
+ ) {
392
+ // We can just remember new page ID without any further actions if no
393
+ // debugger is currently attached or attached debugger is not
394
+ // "Reloadable React Native" connection.
395
+ this._lastConnectedReactNativePage = page;
396
+ return;
397
+ }
398
+ const oldPageId = this._lastConnectedReactNativePage?.id;
399
+ this._lastConnectedReactNativePage = page;
400
+ this._isReloading = true;
401
+
402
+ // We already had a debugger connected to React Native page and a
403
+ // new one appeared - in this case we need to emulate execution context
404
+ // detroy and resend Debugger.enable and Runtime.enable commands to new
405
+ // page.
406
+
407
+ if (oldPageId != null) {
408
+ this._sendMessageToDevice({
409
+ event: "disconnect",
410
+ payload: {
411
+ pageId: oldPageId,
412
+ },
413
+ });
414
+ }
415
+ this._sendMessageToDevice({
416
+ event: "connect",
417
+ payload: {
418
+ pageId: page.id,
419
+ },
420
+ });
421
+ const toSend = [
422
+ {
423
+ method: "Runtime.enable",
424
+ id: 1e9,
425
+ },
426
+ {
427
+ method: "Debugger.enable",
428
+ id: 1e9,
429
+ },
430
+ ];
431
+ for (const message of toSend) {
432
+ this._deviceEventReporter?.logRequest(message, "proxy", {
433
+ pageId: this._debuggerConnection?.pageId ?? null,
434
+ frontendUserAgent: this._debuggerConnection?.userAgent ?? null,
435
+ });
436
+ this._sendMessageToDevice({
437
+ event: "wrappedEvent",
438
+ payload: {
439
+ pageId: this._mapToDevicePageId(page.id),
440
+ wrappedEvent: JSON.stringify(message),
441
+ },
442
+ });
443
+ }
444
+ }
445
+
446
+ // Allows to make changes in incoming message from device.
447
+ async _processMessageFromDevice(payload, debuggerInfo) {
448
+ // Replace Android addresses for scriptParsed event.
449
+ if (payload.method === "Debugger.scriptParsed") {
450
+ const params = payload.params || {};
451
+ if ("sourceMapURL" in params) {
452
+ for (let i = 0; i < EMULATOR_LOCALHOST_ADDRESSES.length; ++i) {
453
+ const address = EMULATOR_LOCALHOST_ADDRESSES[i];
454
+ if (params.sourceMapURL.indexOf(address) >= 0) {
455
+ payload.params.sourceMapURL = params.sourceMapURL.replace(
456
+ address,
457
+ "localhost"
458
+ );
459
+ debuggerInfo.originalSourceURLAddress = address;
460
+ }
461
+ }
462
+ const sourceMapURL = this._tryParseHTTPURL(params.sourceMapURL);
463
+ if (sourceMapURL) {
464
+ // Some debug clients do not support fetching HTTP URLs. If the
465
+ // message headed to the debug client identifies the source map with
466
+ // an HTTP URL, fetch the content here and convert the content to a
467
+ // Data URL (which is more widely supported) before passing the
468
+ // message to the debug client.
469
+ try {
470
+ const sourceMap = await this._fetchText(sourceMapURL);
471
+ payload.params.sourceMapURL =
472
+ "data:application/json;charset=utf-8;base64," +
473
+ new Buffer(sourceMap).toString("base64");
474
+ } catch (exception) {
475
+ this._sendErrorToDebugger(
476
+ `Failed to fetch source map ${params.sourceMapURL}: ${exception.message}`
477
+ );
478
+ }
479
+ }
480
+ }
481
+ if ("url" in params) {
482
+ for (let i = 0; i < EMULATOR_LOCALHOST_ADDRESSES.length; ++i) {
483
+ const address = EMULATOR_LOCALHOST_ADDRESSES[i];
484
+ if (params.url.indexOf(address) >= 0) {
485
+ payload.params.url = params.url.replace(address, "localhost");
486
+ debuggerInfo.originalSourceURLAddress = address;
487
+ }
488
+ }
489
+
490
+ // Chrome doesn't download source maps if URL param is not a valid
491
+ // URL. Some frameworks pass alphanumeric script ID instead of URL which causes
492
+ // Chrome to not download source maps. In this case we want to prepend script ID
493
+ // with 'file://' prefix.
494
+ if (payload.params.url.match(/^[0-9a-z]+$/)) {
495
+ payload.params.url = FILE_PREFIX + payload.params.url;
496
+ debuggerInfo.prependedFilePrefix = true;
497
+ }
498
+
499
+ // $FlowFixMe[prop-missing]
500
+ if (params.scriptId != null) {
501
+ this._scriptIdToSourcePathMapping.set(params.scriptId, params.url);
502
+ }
503
+ }
504
+ }
505
+ if (
506
+ payload.method === "Runtime.executionContextCreated" &&
507
+ this._isReloading
508
+ ) {
509
+ // The new context is ready. First notify Chrome that we've reloaded so
510
+ // it'll resend its breakpoints. If we do this earlier, we may not be
511
+ // ready to receive them.
512
+ debuggerInfo.socket.send(
513
+ JSON.stringify({
514
+ method: "Runtime.executionContextsCleared",
515
+ })
516
+ );
517
+
518
+ // The VM starts in a paused mode. Ask it to resume.
519
+ // Note that if setting breakpoints in early initialization functions,
520
+ // there's a currently race condition between these functions executing
521
+ // and Chrome re-applying the breakpoints due to the message above.
522
+ //
523
+ // This is not an issue in VSCode/Nuclide where the IDE knows to resume
524
+ // at its convenience.
525
+ const resumeMessage = {
526
+ method: "Debugger.resume",
527
+ id: 0,
528
+ };
529
+ this._deviceEventReporter?.logRequest(resumeMessage, "proxy", {
530
+ pageId: this._debuggerConnection?.pageId ?? null,
531
+ frontendUserAgent: this._debuggerConnection?.userAgent ?? null,
532
+ });
533
+ this._sendMessageToDevice({
534
+ event: "wrappedEvent",
535
+ payload: {
536
+ pageId: this._mapToDevicePageId(debuggerInfo.pageId),
537
+ wrappedEvent: JSON.stringify(resumeMessage),
538
+ },
539
+ });
540
+ this._isReloading = false;
541
+ }
542
+ }
543
+
544
+ // Allows to make changes in incoming messages from debugger. Returns a boolean
545
+ // indicating whether the message has been handled locally (i.e. does not need
546
+ // to be forwarded to the target).
547
+ _interceptMessageFromDebugger(req, debuggerInfo, socket) {
548
+ if (req.method === "Debugger.setBreakpointByUrl") {
549
+ this._processDebuggerSetBreakpointByUrl(req, debuggerInfo);
550
+ } else if (req.method === "Debugger.getScriptSource") {
551
+ this._processDebuggerGetScriptSource(req, socket);
552
+ return true;
553
+ }
554
+ return false;
555
+ }
556
+ _processDebuggerSetBreakpointByUrl(req, debuggerInfo) {
557
+ // If we replaced Android emulator's address to localhost we need to change it back.
558
+ if (debuggerInfo.originalSourceURLAddress != null) {
559
+ if (req.params.url != null) {
560
+ req.params.url = req.params.url.replace(
561
+ "localhost",
562
+ debuggerInfo.originalSourceURLAddress
563
+ );
564
+ if (
565
+ req.params.url &&
566
+ req.params.url.startsWith(FILE_PREFIX) &&
567
+ debuggerInfo.prependedFilePrefix
568
+ ) {
569
+ // Remove fake URL prefix if we modified URL in _processMessageFromDevice.
570
+ // $FlowFixMe[incompatible-use]
571
+ req.params.url = req.params.url.slice(FILE_PREFIX.length);
572
+ }
573
+ }
574
+ if (req.params.urlRegex != null) {
575
+ req.params.urlRegex = req.params.urlRegex.replace(
576
+ /localhost/g,
577
+ // $FlowFixMe[incompatible-call]
578
+ debuggerInfo.originalSourceURLAddress
579
+ );
580
+ }
581
+ }
582
+ }
583
+ _processDebuggerGetScriptSource(req, socket) {
584
+ const sendSuccessResponse = (scriptSource) => {
585
+ const response = {
586
+ id: req.id,
587
+ result: {
588
+ scriptSource,
589
+ },
590
+ };
591
+ socket.send(JSON.stringify(response));
592
+ this._deviceEventReporter?.logResponse(response, "proxy", {
593
+ pageId: this._debuggerConnection?.pageId ?? null,
594
+ frontendUserAgent: this._debuggerConnection?.userAgent ?? null,
595
+ });
596
+ };
597
+ const sendErrorResponse = (error) => {
598
+ // Tell the client that the request failed
599
+
600
+ const response = {
601
+ id: req.id,
602
+ result: {
603
+ error: {
604
+ message: error,
605
+ },
606
+ },
607
+ };
608
+ socket.send(JSON.stringify(response));
609
+
610
+ // Send to the console as well, so the user can see it
611
+ this._sendErrorToDebugger(error);
612
+ this._deviceEventReporter?.logResponse(response, "proxy", {
613
+ pageId: this._debuggerConnection?.pageId ?? null,
614
+ frontendUserAgent: this._debuggerConnection?.userAgent ?? null,
615
+ });
616
+ };
617
+ const pathToSource = this._scriptIdToSourcePathMapping.get(
618
+ req.params.scriptId
619
+ );
620
+ if (pathToSource != null) {
621
+ const httpURL = this._tryParseHTTPURL(pathToSource);
622
+ if (httpURL) {
623
+ this._fetchText(httpURL).then(
624
+ (text) => sendSuccessResponse(text),
625
+ (err) =>
626
+ sendErrorResponse(
627
+ `Failed to fetch source url ${pathToSource}: ${err.message}`
628
+ )
629
+ );
630
+ } else {
631
+ let file;
632
+ try {
633
+ file = fs.readFileSync(
634
+ path.resolve(this._projectRoot, pathToSource),
635
+ "utf8"
636
+ );
637
+ } catch (err) {
638
+ sendErrorResponse(
639
+ `Failed to fetch source file ${pathToSource}: ${err.message}`
640
+ );
641
+ }
642
+ if (file != null) {
643
+ sendSuccessResponse(file);
644
+ }
645
+ }
646
+ }
647
+ }
648
+ _mapToDevicePageId(pageId) {
649
+ if (
650
+ pageId === REACT_NATIVE_RELOADABLE_PAGE_ID &&
651
+ this._lastConnectedReactNativePage != null
652
+ ) {
653
+ return this._lastConnectedReactNativePage.id;
654
+ } else {
655
+ return pageId;
656
+ }
657
+ }
658
+ _tryParseHTTPURL(url) {
659
+ let parsedURL;
660
+ try {
661
+ parsedURL = new URL(url);
662
+ } catch {}
663
+ const protocol = parsedURL?.protocol;
664
+ if (protocol !== "http:" && protocol !== "https:") {
665
+ parsedURL = undefined;
666
+ }
667
+ return parsedURL;
668
+ }
669
+
670
+ // Fetch text, raising an exception if the text could not be fetched,
671
+ // or is too large.
672
+ async _fetchText(url) {
673
+ if (url.hostname !== "localhost") {
674
+ throw new Error("remote fetches not permitted");
675
+ }
676
+
677
+ // $FlowFixMe[incompatible-call] Suppress arvr node-fetch flow error
678
+ const response = await (0, _nodeFetch.default)(url);
679
+ const text = await response.text();
680
+ // Restrict the length to well below the 500MB limit for nodejs (leaving
681
+ // room some some later manipulation, e.g. base64 or wrapping in JSON)
682
+ if (text.length > 350000000) {
683
+ throw new Error("file too large to fetch via HTTP");
684
+ }
685
+ return text;
686
+ }
687
+ _sendErrorToDebugger(message) {
688
+ const debuggerSocket = this._debuggerConnection?.socket;
689
+ if (debuggerSocket && debuggerSocket.readyState === _ws.default.OPEN) {
690
+ debuggerSocket.send(
691
+ JSON.stringify({
692
+ method: "Runtime.consoleAPICalled",
693
+ params: {
694
+ args: [
695
+ {
696
+ type: "string",
697
+ value: message,
698
+ },
699
+ ],
700
+ executionContextId: 0,
701
+ type: "error",
702
+ },
703
+ })
704
+ );
705
+ }
706
+ }
707
+ }
708
+ exports.default = Device;