@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.
- package/README.md +77 -3
- package/dist/createDevMiddleware.d.ts +53 -0
- package/dist/createDevMiddleware.js +53 -6
- package/dist/createDevMiddleware.js.flow +44 -15
- package/dist/index.d.ts +12 -0
- package/dist/index.flow.d.ts +14 -0
- package/dist/index.flow.js.flow +5 -2
- package/dist/index.js +14 -1
- package/dist/index.js.flow +1 -9
- package/dist/inspector-proxy/Device.d.ts +90 -0
- package/dist/inspector-proxy/Device.js +708 -0
- package/dist/inspector-proxy/Device.js.flow +97 -0
- package/dist/inspector-proxy/DeviceEventReporter.d.ts +53 -0
- package/dist/inspector-proxy/DeviceEventReporter.js +162 -0
- package/dist/inspector-proxy/DeviceEventReporter.js.flow +65 -0
- package/dist/inspector-proxy/InspectorProxy.d.ts +61 -0
- package/dist/inspector-proxy/InspectorProxy.js +212 -0
- package/dist/inspector-proxy/InspectorProxy.js.flow +65 -0
- package/dist/inspector-proxy/types.d.ts +78 -0
- package/dist/inspector-proxy/types.js +1 -0
- package/dist/inspector-proxy/types.js.flow +135 -0
- package/dist/middleware/openDebuggerMiddleware.d.ts +37 -0
- package/dist/middleware/openDebuggerMiddleware.js +78 -24
- package/dist/middleware/openDebuggerMiddleware.js.flow +15 -64
- package/dist/types/BrowserLauncher.d.ts +27 -0
- package/dist/types/BrowserLauncher.js +1 -0
- package/dist/types/BrowserLauncher.js.flow +31 -0
- package/dist/types/EventReporter.d.ts +69 -0
- package/dist/types/EventReporter.js +1 -0
- package/dist/types/EventReporter.js.flow +81 -0
- package/dist/types/Experiments.d.ts +25 -0
- package/dist/types/Experiments.js +1 -0
- package/dist/types/Experiments.js.flow +27 -0
- package/dist/types/Logger.d.ts +16 -0
- package/dist/utils/DefaultBrowserLauncher.d.ts +18 -0
- package/dist/utils/DefaultBrowserLauncher.js +77 -0
- package/dist/utils/DefaultBrowserLauncher.js.flow +20 -0
- package/dist/utils/getDevToolsFrontendUrl.d.ts +21 -0
- package/dist/utils/getDevToolsFrontendUrl.js +48 -0
- package/dist/utils/getDevToolsFrontendUrl.js.flow +21 -0
- package/package.json +6 -1
- package/dist/utils/getDevServerUrl.js +0 -37
- package/dist/utils/getDevServerUrl.js.flow +0 -32
- package/dist/utils/launchChromeDevTools.js +0 -44
- package/dist/utils/launchChromeDevTools.js.flow +0 -38
- package/dist/utils/launchDebuggerAppWindow.js +0 -58
- package/dist/utils/launchDebuggerAppWindow.js.flow +0 -56
- package/dist/utils/queryInspectorTargets.js +0 -35
- 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;
|