first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
* @format
*/
export type CDPMessageDestination =
| "DebuggerToProxy"
| "ProxyToDebugger"
| "DeviceToProxy"
| "ProxyToDevice";
declare class CdpDebugLogging {
constructor();
log(destination: CDPMessageDestination, message: string): void;
}
export default CdpDebugLogging;

View File

@@ -0,0 +1,117 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
var _timers = require("timers");
var _util = _interopRequireDefault(require("util"));
function _interopRequireDefault(e) {
return e && e.__esModule ? e : { default: e };
}
const debug = require("debug")("Metro:InspectorProxy");
const debugCDPMessages = require("debug")("Metro:InspectorProxyCDPMessages");
const CDP_MESSAGES_BATCH_DEBUGGING_THROTTLE_MS = 5000;
function getCDPLogPrefix(destination) {
return _util.default.format(
"[(Debugger) %s (Proxy) %s (Device)]",
destination === "DebuggerToProxy"
? "->"
: destination === "ProxyToDebugger"
? "<-"
: " ",
destination === "ProxyToDevice"
? "->"
: destination === "DeviceToProxy"
? "<-"
: " ",
);
}
class CdpDebugLogging {
#cdpMessagesLoggingBatchingFn = {
DebuggerToProxy: () => {},
ProxyToDebugger: () => {},
DeviceToProxy: () => {},
ProxyToDevice: () => {},
};
constructor() {
if (debug.enabled) {
this.#initializeThrottledCDPMessageLogging();
}
}
#initializeThrottledCDPMessageLogging() {
const batchingCounters = {
DebuggerToProxy: {
count: 0,
size: 0,
},
ProxyToDebugger: {
count: 0,
size: 0,
},
DeviceToProxy: {
count: 0,
size: 0,
},
ProxyToDevice: {
count: 0,
size: 0,
},
};
Object.keys(batchingCounters).forEach((destination) => {
let timeout = null;
this.#cdpMessagesLoggingBatchingFn[destination] = (message) => {
if (message.length > 1024 * 100) {
const messagePreview = JSON.stringify(
JSON.parse(message, (key, value) => {
if (Array.isArray(value)) {
return "[ARRAY]";
}
if (typeof value === "string" && value.length > 50) {
return value.slice(0, 50) + "...";
}
return value;
}),
null,
2,
);
debug(
"%s A large message (%s MB) was %s- %s",
getCDPLogPrefix(destination),
(message.length / (1024 * 1024)).toFixed(2),
destination.startsWith("Proxy") ? " sent " : "received",
messagePreview,
);
}
if (timeout == null) {
timeout = (0, _timers.setTimeout)(() => {
debug(
"%s %s CDP messages of size %s MB %s in the last %ss.",
getCDPLogPrefix(destination),
String(batchingCounters[destination].count).padStart(4),
String(
(batchingCounters[destination].size / (1024 * 1024)).toFixed(2),
).padStart(6),
destination.startsWith("Proxy") ? " sent " : "received",
CDP_MESSAGES_BATCH_DEBUGGING_THROTTLE_MS / 1000,
);
batchingCounters[destination].count = 0;
batchingCounters[destination].size = 0;
timeout = null;
}, CDP_MESSAGES_BATCH_DEBUGGING_THROTTLE_MS).unref();
}
batchingCounters[destination].count++;
batchingCounters[destination].size += message.length;
};
});
}
log(destination, message) {
if (debugCDPMessages.enabled) {
debugCDPMessages("%s message: %s", getCDPLogPrefix(destination), message);
}
if (debug.enabled) {
this.#cdpMessagesLoggingBatchingFn[destination](message);
}
}
}
exports.default = CdpDebugLogging;

View File

@@ -0,0 +1,20 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
export type CDPMessageDestination =
| "DebuggerToProxy"
| "ProxyToDebugger"
| "DeviceToProxy"
| "ProxyToDevice";
declare export default class CdpDebugLogging {
constructor(): void;
log(destination: CDPMessageDestination, message: string): void;
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
* @format
*/
import type { JSONSerializable, Page } from "./types";
type ExposedDevice = Readonly<{
appId: string;
id: string;
name: string;
sendMessage: (message: JSONSerializable) => void;
}>;
type ExposedDebugger = Readonly<{
userAgent: string | null;
sendMessage: (message: JSONSerializable) => void;
}>;
export type CustomMessageHandlerConnection = Readonly<{
page: Page;
device: ExposedDevice;
debugger: ExposedDebugger;
}>;
export type CreateCustomMessageHandlerFn = (
connection: CustomMessageHandlerConnection,
) => null | undefined | CustomMessageHandler;
/**
* The device message middleware allows implementers to handle unsupported CDP messages.
* It is instantiated per device and may contain state that is specific to that device.
* The middleware can also mark messages from the device or debugger as handled, which stops propagating.
*/
export interface CustomMessageHandler {
/**
* Handle a CDP message coming from the device.
* This is invoked before the message is sent to the debugger.
* When returning true, the message is considered handled and will not be sent to the debugger.
*/
handleDeviceMessage(message: JSONSerializable): true | void;
/**
* Handle a CDP message coming from the debugger.
* This is invoked before the message is sent to the device.
* When returning true, the message is considered handled and will not be sent to the device.
*/
handleDebuggerMessage(message: JSONSerializable): true | void;
}

View File

@@ -0,0 +1 @@
"use strict";

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type { JSONSerializable, Page } from "./types";
type ExposedDevice = $ReadOnly<{
appId: string,
id: string,
name: string,
sendMessage: (message: JSONSerializable) => void,
}>;
type ExposedDebugger = $ReadOnly<{
userAgent: string | null,
sendMessage: (message: JSONSerializable) => void,
}>;
export type CustomMessageHandlerConnection = $ReadOnly<{
page: Page,
device: ExposedDevice,
debugger: ExposedDebugger,
}>;
export type CreateCustomMessageHandlerFn = (
connection: CustomMessageHandlerConnection,
) => ?CustomMessageHandler;
/**
* The device message middleware allows implementers to handle unsupported CDP messages.
* It is instantiated per device and may contain state that is specific to that device.
* The middleware can also mark messages from the device or debugger as handled, which stops propagating.
*/
export interface CustomMessageHandler {
/**
* Handle a CDP message coming from the device.
* This is invoked before the message is sent to the debugger.
* When returning true, the message is considered handled and will not be sent to the debugger.
*/
handleDeviceMessage(message: JSONSerializable): true | void;
/**
* Handle a CDP message coming from the debugger.
* This is invoked before the message is sent to the device.
* When returning true, the message is considered handled and will not be sent to the device.
*/
handleDebuggerMessage(message: JSONSerializable): true | void;
}

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
* @format
*/
import type { EventReporter } from "../types/EventReporter";
import type { CreateCustomMessageHandlerFn } from "./CustomMessageHandler";
import type { Page } from "./types";
import WS from "ws";
export declare const WS_CLOSE_REASON: {
PAGE_NOT_FOUND: "[PAGE_NOT_FOUND] Debugger page not found";
CONNECTION_LOST: "[CONNECTION_LOST] Connection lost to corresponding device";
RECREATING_DEVICE: "[RECREATING_DEVICE] Recreating device connection";
NEW_DEBUGGER_OPENED: "[NEW_DEBUGGER_OPENED] New debugger opened for the same app instance";
};
export declare type WS_CLOSE_REASON = typeof WS_CLOSE_REASON;
export type DeviceOptions = Readonly<{
id: string;
name: string;
app: string;
socket: WS;
eventReporter: null | undefined | EventReporter;
createMessageMiddleware: null | undefined | CreateCustomMessageHandlerFn;
deviceRelativeBaseUrl: URL;
serverRelativeBaseUrl: URL;
isProfilingBuild: boolean;
}>;
/**
* Device class represents single device connection to Inspector Proxy. Each device
* can have multiple inspectable pages.
*/
declare class Device {
constructor(deviceOptions: DeviceOptions);
/**
* Used to recreate the device connection if there is a device ID collision.
* 1. Checks if the same device is attempting to reconnect for the same app.
* 2. If not, close both the device and debugger socket.
* 3. If the debugger connection can be reused, close the device socket only.
*
* This hack attempts to allow users to reload the app, either as result of a
* crash, or manually reloading, without having to restart the debugger.
*/
dangerouslyRecreateDevice(deviceOptions: DeviceOptions): void;
getName(): string;
getApp(): string;
getPagesList(): ReadonlyArray<Page>;
handleDebuggerConnection(
socket: WS,
pageId: string,
$$PARAM_2$$: Readonly<{
debuggerRelativeBaseUrl: URL;
userAgent: string | null;
}>,
): void;
dangerouslyGetSocket(): WS;
}
export default Device;

View File

@@ -0,0 +1,810 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = exports.WS_CLOSE_REASON = void 0;
var _CdpDebugLogging = _interopRequireDefault(require("./CdpDebugLogging"));
var _DeviceEventReporter = _interopRequireDefault(
require("./DeviceEventReporter"),
);
var _invariant = _interopRequireDefault(require("invariant"));
var _ws = _interopRequireDefault(require("ws"));
function _interopRequireDefault(e) {
return e && e.__esModule ? e : { default: e };
}
const debug = require("debug")("Metro:InspectorProxy");
const PAGES_POLLING_INTERVAL = 1000;
const WS_CLOSURE_CODE = {
NORMAL: 1000,
INTERNAL_ERROR: 1011,
};
const WS_CLOSE_REASON = (exports.WS_CLOSE_REASON = {
PAGE_NOT_FOUND: "[PAGE_NOT_FOUND] Debugger page not found",
CONNECTION_LOST: "[CONNECTION_LOST] Connection lost to corresponding device",
RECREATING_DEVICE: "[RECREATING_DEVICE] Recreating device connection",
NEW_DEBUGGER_OPENED:
"[NEW_DEBUGGER_OPENED] New debugger opened for the same app instance",
});
const FILE_PREFIX = "file://";
let fuseboxConsoleNoticeLogged = false;
const REACT_NATIVE_RELOADABLE_PAGE_ID = "-1";
class Device {
#id;
#name;
#app;
#deviceSocket;
#pages = new Map();
#debuggerConnection = null;
#lastConnectedLegacyReactNativePage = null;
#isLegacyPageReloading = false;
#lastGetPagesMessage = "";
#scriptIdToSourcePathMapping = new Map();
#deviceEventReporter;
#pagesPollingIntervalId;
#createCustomMessageHandler;
#connectedPageIds = new Set();
#deviceRelativeBaseUrl;
#serverRelativeBaseUrl;
#cdpDebugLogging;
constructor(deviceOptions) {
this.#dangerouslyConstruct(deviceOptions);
}
#dangerouslyConstruct({
id,
name,
app,
socket,
eventReporter,
createMessageMiddleware,
serverRelativeBaseUrl,
deviceRelativeBaseUrl,
isProfilingBuild,
}) {
this.#cdpDebugLogging = new _CdpDebugLogging.default();
this.#id = id;
this.#name = name;
this.#app = app;
this.#deviceSocket = socket;
this.#serverRelativeBaseUrl = serverRelativeBaseUrl;
this.#deviceRelativeBaseUrl = deviceRelativeBaseUrl;
this.#deviceEventReporter = eventReporter
? new _DeviceEventReporter.default(eventReporter, {
deviceId: id,
deviceName: name,
appId: app,
})
: null;
this.#createCustomMessageHandler = createMessageMiddleware;
if (isProfilingBuild) {
this.#deviceEventReporter?.logProfilingTargetRegistered();
}
this.#deviceSocket.on("message", (message) => {
try {
const parsedMessage = JSON.parse(message);
if (parsedMessage.event === "getPages") {
if (message !== this.#lastGetPagesMessage) {
debug("Device getPages ping has changed: %s", message);
this.#lastGetPagesMessage = message;
}
} else {
this.#cdpDebugLogging.log("DeviceToProxy", message);
}
this.#handleMessageFromDevice(parsedMessage);
} catch (error) {
debug("%O\nHandling device message: %s", error, message);
try {
this.#deviceEventReporter?.logProxyMessageHandlingError(
"device",
error,
message,
);
} catch (loggingError) {
debug(
"Error logging message handling error to reporter: %O",
loggingError,
);
}
}
});
this.#pagesPollingIntervalId = setInterval(
() =>
this.#sendMessageToDevice({
event: "getPages",
}),
PAGES_POLLING_INTERVAL,
);
this.#deviceSocket.on("close", () => {
if (socket === this.#deviceSocket) {
this.#deviceEventReporter?.logDisconnection("device");
this.#terminateDebuggerConnection(
WS_CLOSURE_CODE.NORMAL,
WS_CLOSE_REASON.CONNECTION_LOST,
);
clearInterval(this.#pagesPollingIntervalId);
}
});
}
#terminateDebuggerConnection(code, reason) {
const debuggerConnection = this.#debuggerConnection;
if (debuggerConnection) {
this.#sendDisconnectEventToDevice(
this.#mapToDevicePageId(debuggerConnection.pageId),
);
debuggerConnection.socket.close(code, reason);
this.#debuggerConnection = null;
}
}
dangerouslyRecreateDevice(deviceOptions) {
(0, _invariant.default)(
deviceOptions.id === this.#id,
"dangerouslyRecreateDevice() can only be used for the same device ID",
);
const oldDebugger = this.#debuggerConnection;
if (this.#app !== deviceOptions.app || this.#name !== deviceOptions.name) {
this.#deviceSocket.close(
WS_CLOSURE_CODE.NORMAL,
WS_CLOSE_REASON.RECREATING_DEVICE,
);
this.#terminateDebuggerConnection(
WS_CLOSURE_CODE.NORMAL,
WS_CLOSE_REASON.RECREATING_DEVICE,
);
}
this.#debuggerConnection = null;
if (oldDebugger) {
oldDebugger.socket.removeAllListeners();
this.#deviceSocket.close(
WS_CLOSURE_CODE.NORMAL,
WS_CLOSE_REASON.RECREATING_DEVICE,
);
this.handleDebuggerConnection(oldDebugger.socket, oldDebugger.pageId, {
debuggerRelativeBaseUrl: oldDebugger.debuggerRelativeBaseUrl,
userAgent: oldDebugger.userAgent,
});
}
this.#dangerouslyConstruct(deviceOptions);
}
getName() {
return this.#name;
}
getApp() {
return this.#app;
}
getPagesList() {
if (this.#lastConnectedLegacyReactNativePage) {
return [...this.#pages.values(), this.#createSyntheticPage()];
} else {
return [...this.#pages.values()];
}
}
handleDebuggerConnection(
socket,
pageId,
{ debuggerRelativeBaseUrl, userAgent },
) {
const page =
pageId === REACT_NATIVE_RELOADABLE_PAGE_ID
? this.#createSyntheticPage()
: this.#pages.get(pageId);
if (!page) {
debug(
`Got new debugger connection via ${debuggerRelativeBaseUrl.href} for ` +
`page ${pageId} of ${this.#name}, but no such page exists`,
);
socket.close(
WS_CLOSURE_CODE.INTERNAL_ERROR,
WS_CLOSE_REASON.PAGE_NOT_FOUND,
);
return;
}
this.#deviceEventReporter?.logDisconnection("debugger");
this.#terminateDebuggerConnection(
WS_CLOSURE_CODE.NORMAL,
WS_CLOSE_REASON.NEW_DEBUGGER_OPENED,
);
this.#deviceEventReporter?.logConnection("debugger", {
pageId,
frontendUserAgent: userAgent,
});
const debuggerInfo = {
socket,
prependedFilePrefix: false,
pageId,
userAgent: userAgent,
customHandler: null,
debuggerRelativeBaseUrl,
};
this.#debuggerConnection = debuggerInfo;
debug(
`Got new debugger connection via ${debuggerRelativeBaseUrl.href} for ` +
`page ${pageId} of ${this.#name}`,
);
if (this.#debuggerConnection && this.#createCustomMessageHandler) {
this.#debuggerConnection.customHandler = this.#createCustomMessageHandler(
{
page,
debugger: {
userAgent: debuggerInfo.userAgent,
sendMessage: (message) => {
try {
const payload = JSON.stringify(message);
this.#cdpDebugLogging.log("ProxyToDebugger", payload);
socket.send(payload);
} catch {}
},
},
device: {
appId: this.#app,
id: this.#id,
name: this.#name,
sendMessage: (message) => {
try {
const payload = JSON.stringify({
event: "wrappedEvent",
payload: {
pageId: this.#mapToDevicePageId(pageId),
wrappedEvent: JSON.stringify(message),
},
});
this.#cdpDebugLogging.log("DebuggerToProxy", payload);
this.#deviceSocket.send(payload);
} catch {}
},
},
},
);
if (this.#debuggerConnection.customHandler) {
debug("Created new custom message handler for debugger connection");
} else {
debug(
"Skipping new custom message handler for debugger connection, factory function returned null",
);
}
}
this.#sendConnectEventToDevice(this.#mapToDevicePageId(pageId));
socket.on("message", (message) => {
this.#cdpDebugLogging.log("DebuggerToProxy", message);
const debuggerRequest = JSON.parse(message);
this.#deviceEventReporter?.logRequest(debuggerRequest, "debugger", {
pageId: this.#debuggerConnection?.pageId ?? null,
frontendUserAgent: userAgent,
prefersFuseboxFrontend: this.#isPageFuseboxFrontend(
this.#debuggerConnection?.pageId,
),
});
let processedReq = debuggerRequest;
if (
this.#debuggerConnection?.customHandler?.handleDebuggerMessage(
debuggerRequest,
) === true
) {
return;
}
if (!this.#pageHasCapability(page, "nativeSourceCodeFetching")) {
processedReq = this.#interceptClientMessageForSourceFetching(
debuggerRequest,
debuggerInfo,
socket,
);
}
if (processedReq) {
this.#sendMessageToDevice({
event: "wrappedEvent",
payload: {
pageId: this.#mapToDevicePageId(pageId),
wrappedEvent: JSON.stringify(processedReq),
},
});
}
});
socket.on("close", () => {
debug(`Debugger for page ${pageId} and ${this.#name} disconnected.`);
this.#deviceEventReporter?.logDisconnection("debugger");
if (this.#debuggerConnection?.socket === socket) {
this.#terminateDebuggerConnection();
}
});
const cdpDebugLogging = this.#cdpDebugLogging;
const sendFunc = socket.send;
socket.send = function (message) {
cdpDebugLogging.log("ProxyToDebugger", message);
return sendFunc.call(socket, message);
};
}
#sendConnectEventToDevice(devicePageId) {
if (this.#connectedPageIds.has(devicePageId)) {
return;
}
this.#connectedPageIds.add(devicePageId);
this.#sendMessageToDevice({
event: "connect",
payload: {
pageId: devicePageId,
},
});
}
#sendDisconnectEventToDevice(devicePageId) {
if (!this.#connectedPageIds.has(devicePageId)) {
return;
}
this.#connectedPageIds.delete(devicePageId);
this.#sendMessageToDevice({
event: "disconnect",
payload: {
pageId: devicePageId,
},
});
}
#pageHasCapability(page, flag) {
return page.capabilities[flag] === true;
}
#createSyntheticPage() {
return {
id: REACT_NATIVE_RELOADABLE_PAGE_ID,
title: "React Native Experimental (Improved Chrome Reloads)",
vm: "don't use",
app: this.#app,
capabilities: {},
};
}
#handleMessageFromDevice(message) {
if (message.event === "getPages") {
this.#pages = new Map(
message.payload.map(({ capabilities, ...page }) => [
page.id,
{
...page,
capabilities: capabilities ?? {},
},
]),
);
if (message.payload.length !== this.#pages.size) {
const duplicateIds = new Set();
const idsSeen = new Set();
for (const page of message.payload) {
if (!idsSeen.has(page.id)) {
idsSeen.add(page.id);
} else {
duplicateIds.add(page.id);
}
}
debug(
`Received duplicate page IDs from device: ${[...duplicateIds].join(", ")}`,
);
}
for (const page of this.#pages.values()) {
if (this.#pageHasCapability(page, "nativePageReloads")) {
this.#logFuseboxConsoleNotice();
continue;
}
if (page.title.includes("React")) {
if (page.id !== this.#lastConnectedLegacyReactNativePage?.id) {
this.#newLegacyReactNativePage(page);
break;
}
}
}
} else if (message.event === "disconnect") {
const pageId = message.payload.pageId;
const page = this.#pages.get(pageId);
if (page != null && this.#pageHasCapability(page, "nativePageReloads")) {
return;
}
const debuggerSocket = this.#debuggerConnection
? this.#debuggerConnection.socket
: null;
if (debuggerSocket && debuggerSocket.readyState === _ws.default.OPEN) {
if (
this.#debuggerConnection != null &&
this.#debuggerConnection.pageId !== REACT_NATIVE_RELOADABLE_PAGE_ID
) {
debug(`Legacy page ${pageId} is reloading.`);
debuggerSocket.send(
JSON.stringify({
method: "reload",
}),
);
}
}
} else if (message.event === "wrappedEvent") {
if (this.#debuggerConnection == null) {
return;
}
const debuggerSocket = this.#debuggerConnection.socket;
if (
debuggerSocket == null ||
debuggerSocket.readyState !== _ws.default.OPEN
) {
return;
}
const parsedPayload = JSON.parse(message.payload.wrappedEvent);
const pageId = this.#debuggerConnection?.pageId ?? null;
if ("id" in parsedPayload) {
this.#deviceEventReporter?.logResponse(parsedPayload, "device", {
pageId,
frontendUserAgent: this.#debuggerConnection?.userAgent ?? null,
prefersFuseboxFrontend: this.#isPageFuseboxFrontend(pageId),
});
}
const debuggerConnection = this.#debuggerConnection;
if (debuggerConnection != null) {
if (
debuggerConnection.customHandler?.handleDeviceMessage(
parsedPayload,
) === true
) {
return;
}
this.#processMessageFromDeviceLegacy(
parsedPayload,
debuggerConnection,
pageId,
);
const messageToSend = JSON.stringify(parsedPayload);
debuggerSocket.send(messageToSend);
} else {
debuggerSocket.send(message.payload.wrappedEvent);
}
}
}
#sendMessageToDevice(message) {
try {
const messageToSend = JSON.stringify(message);
if (message.event !== "getPages") {
this.#cdpDebugLogging.log("ProxyToDevice", messageToSend);
}
this.#deviceSocket.send(messageToSend);
} catch (error) {}
}
#newLegacyReactNativePage(page) {
debug(`React Native page updated to ${page.id}`);
if (
this.#debuggerConnection == null ||
this.#debuggerConnection.pageId !== REACT_NATIVE_RELOADABLE_PAGE_ID
) {
this.#lastConnectedLegacyReactNativePage = page;
return;
}
const oldPageId = this.#lastConnectedLegacyReactNativePage?.id;
this.#lastConnectedLegacyReactNativePage = page;
this.#isLegacyPageReloading = true;
if (oldPageId != null) {
this.#sendDisconnectEventToDevice(oldPageId);
}
this.#sendConnectEventToDevice(page.id);
const toSend = [
{
method: "Runtime.enable",
id: 1e9,
},
{
method: "Debugger.enable",
id: 1e9,
},
];
for (const message of toSend) {
const pageId = this.#debuggerConnection?.pageId ?? null;
this.#deviceEventReporter?.logRequest(message, "proxy", {
pageId,
frontendUserAgent: this.#debuggerConnection?.userAgent ?? null,
prefersFuseboxFrontend: this.#isPageFuseboxFrontend(pageId),
});
this.#sendMessageToDevice({
event: "wrappedEvent",
payload: {
pageId: this.#mapToDevicePageId(page.id),
wrappedEvent: JSON.stringify(message),
},
});
}
}
#debuggerRelativeToDeviceRelativeUrl(
debuggerRelativeUrl,
{ debuggerRelativeBaseUrl },
) {
const deviceRelativeUrl = new URL(debuggerRelativeUrl.href);
if (debuggerRelativeUrl.origin === debuggerRelativeBaseUrl.origin) {
deviceRelativeUrl.hostname = this.#deviceRelativeBaseUrl.hostname;
deviceRelativeUrl.port = this.#deviceRelativeBaseUrl.port;
deviceRelativeUrl.protocol = this.#deviceRelativeBaseUrl.protocol;
}
return deviceRelativeUrl;
}
#deviceRelativeUrlToDebuggerRelativeUrl(
deviceRelativeUrl,
{ debuggerRelativeBaseUrl },
) {
const debuggerRelativeUrl = new URL(deviceRelativeUrl.href);
if (deviceRelativeUrl.origin === this.#deviceRelativeBaseUrl.origin) {
debuggerRelativeUrl.hostname = debuggerRelativeBaseUrl.hostname;
debuggerRelativeUrl.port = debuggerRelativeBaseUrl.port;
debuggerRelativeUrl.protocol = debuggerRelativeUrl.protocol;
}
return debuggerRelativeUrl;
}
#deviceRelativeUrlToServerRelativeUrl(deviceRelativeUrl) {
const debuggerRelativeUrl = new URL(deviceRelativeUrl.href);
if (deviceRelativeUrl.origin === this.#deviceRelativeBaseUrl.origin) {
debuggerRelativeUrl.hostname = this.#serverRelativeBaseUrl.hostname;
debuggerRelativeUrl.port = this.#serverRelativeBaseUrl.port;
debuggerRelativeUrl.protocol = this.#serverRelativeBaseUrl.protocol;
}
return debuggerRelativeUrl;
}
#processMessageFromDeviceLegacy(payload, debuggerInfo, pageId) {
const page = pageId != null ? this.#pages.get(pageId) : null;
if (
(!page || !this.#pageHasCapability(page, "nativeSourceCodeFetching")) &&
payload.method === "Debugger.scriptParsed" &&
payload.params != null
) {
const params = payload.params;
if ("sourceMapURL" in params) {
const sourceMapURL = this.#tryParseHTTPURL(params.sourceMapURL);
if (sourceMapURL) {
payload.params.sourceMapURL =
this.#deviceRelativeUrlToDebuggerRelativeUrl(
sourceMapURL,
debuggerInfo,
).href;
}
}
if ("url" in params) {
let serverRelativeUrl = params.url;
const parsedUrl = this.#tryParseHTTPURL(params.url);
if (parsedUrl) {
payload.params.url = this.#deviceRelativeUrlToDebuggerRelativeUrl(
parsedUrl,
debuggerInfo,
).href;
serverRelativeUrl =
this.#deviceRelativeUrlToServerRelativeUrl(parsedUrl).href;
}
if (payload.params.url.match(/^[0-9a-z]+$/)) {
payload.params.url = FILE_PREFIX + payload.params.url;
debuggerInfo.prependedFilePrefix = true;
}
if ("scriptId" in params && params.scriptId != null) {
this.#scriptIdToSourcePathMapping.set(
params.scriptId,
serverRelativeUrl,
);
}
}
}
if (
payload.method === "Runtime.executionContextCreated" &&
this.#isLegacyPageReloading
) {
debuggerInfo.socket.send(
JSON.stringify({
method: "Runtime.executionContextsCleared",
}),
);
const resumeMessage = {
method: "Debugger.resume",
id: 0,
};
this.#deviceEventReporter?.logRequest(resumeMessage, "proxy", {
pageId: this.#debuggerConnection?.pageId ?? null,
frontendUserAgent: this.#debuggerConnection?.userAgent ?? null,
prefersFuseboxFrontend: this.#isPageFuseboxFrontend(
this.#debuggerConnection?.pageId,
),
});
this.#sendMessageToDevice({
event: "wrappedEvent",
payload: {
pageId: this.#mapToDevicePageId(debuggerInfo.pageId),
wrappedEvent: JSON.stringify(resumeMessage),
},
});
this.#isLegacyPageReloading = false;
}
if (payload.method === "Runtime.consoleAPICalled") {
const callFrames = payload.params?.stackTrace?.callFrames ?? [];
for (const callFrame of callFrames) {
if (callFrame.url) {
const parsedUrl = this.#tryParseHTTPURL(callFrame.url);
if (parsedUrl) {
callFrame.url = this.#deviceRelativeUrlToDebuggerRelativeUrl(
parsedUrl,
debuggerInfo,
).href;
}
}
}
}
}
#interceptClientMessageForSourceFetching(req, debuggerInfo, socket) {
switch (req.method) {
case "Debugger.setBreakpointByUrl":
return this.#processDebuggerSetBreakpointByUrl(req, debuggerInfo);
case "Debugger.getScriptSource":
void this.#processDebuggerGetScriptSource(req, socket);
return null;
case "Network.loadNetworkResource":
const response = {
id: req.id,
result: {
error: {
code: -32601,
message:
"[inspector-proxy]: Page lacks nativeSourceCodeFetching capability.",
},
},
};
socket.send(JSON.stringify(response));
const pageId = this.#debuggerConnection?.pageId ?? null;
this.#deviceEventReporter?.logResponse(response, "proxy", {
pageId,
frontendUserAgent: this.#debuggerConnection?.userAgent ?? null,
prefersFuseboxFrontend: this.#isPageFuseboxFrontend(pageId),
});
return null;
default:
return req;
}
}
#processDebuggerSetBreakpointByUrl(req, debuggerInfo) {
const { debuggerRelativeBaseUrl, prependedFilePrefix } = debuggerInfo;
const processedReq = {
...req,
params: {
...req.params,
},
};
if (processedReq.params.url != null) {
const originalUrlParam = processedReq.params.url;
const httpUrl = this.#tryParseHTTPURL(originalUrlParam);
if (httpUrl) {
processedReq.params.url = this.#debuggerRelativeToDeviceRelativeUrl(
httpUrl,
debuggerInfo,
).href;
} else if (
originalUrlParam.startsWith(FILE_PREFIX) &&
prependedFilePrefix
) {
processedReq.params.url = originalUrlParam.slice(FILE_PREFIX.length);
}
}
if (
new Set(["10.0.2.2", "10.0.3.2"]).has(
this.#deviceRelativeBaseUrl.hostname,
) &&
debuggerRelativeBaseUrl.hostname === "localhost" &&
processedReq.params.urlRegex != null
) {
processedReq.params.urlRegex = processedReq.params.urlRegex.replaceAll(
"localhost",
this.#deviceRelativeBaseUrl.hostname.replaceAll(".", "\\."),
);
}
return processedReq;
}
async #processDebuggerGetScriptSource(req, socket) {
const sendSuccessResponse = (scriptSource) => {
const response = {
id: req.id,
result: {
scriptSource,
},
};
socket.send(JSON.stringify(response));
const pageId = this.#debuggerConnection?.pageId ?? null;
this.#deviceEventReporter?.logResponse(response, "proxy", {
pageId,
frontendUserAgent: this.#debuggerConnection?.userAgent ?? null,
prefersFuseboxFrontend: this.#isPageFuseboxFrontend(pageId),
});
};
const sendErrorResponse = (error) => {
const response = {
id: req.id,
result: {
error: {
message: error,
},
},
};
socket.send(JSON.stringify(response));
this.#sendErrorToDebugger(error);
const pageId = this.#debuggerConnection?.pageId ?? null;
this.#deviceEventReporter?.logResponse(response, "proxy", {
pageId,
frontendUserAgent: this.#debuggerConnection?.userAgent ?? null,
prefersFuseboxFrontend: this.#isPageFuseboxFrontend(pageId),
});
};
const pathToSource = this.#scriptIdToSourcePathMapping.get(
req.params.scriptId,
);
try {
const httpURL =
pathToSource == null ? null : this.#tryParseHTTPURL(pathToSource);
if (!httpURL) {
throw new Error(
`Can't parse requested URL ${pathToSource === undefined ? "undefined" : JSON.stringify(pathToSource)}`,
);
}
const text = await this.#fetchText(httpURL);
sendSuccessResponse(text);
} catch (err) {
sendErrorResponse(
`Failed to fetch source url ${pathToSource === undefined ? "undefined" : JSON.stringify(pathToSource)} for scriptId ${req.params.scriptId}: ${err.message}`,
);
}
}
#mapToDevicePageId(pageId) {
if (
pageId === REACT_NATIVE_RELOADABLE_PAGE_ID &&
this.#lastConnectedLegacyReactNativePage != null
) {
return this.#lastConnectedLegacyReactNativePage.id;
} else {
return pageId;
}
}
#tryParseHTTPURL(url) {
let parsedURL;
try {
parsedURL = new URL(url);
} catch {}
const protocol = parsedURL?.protocol;
if (protocol !== "http:" && protocol !== "https:") {
parsedURL = undefined;
}
return parsedURL;
}
async #fetchText(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error("HTTP " + response.status + " " + response.statusText);
}
const text = await response.text();
if (text.length > 350000000) {
throw new Error("file too large to fetch via HTTP");
}
return text;
}
#sendErrorToDebugger(message) {
const debuggerSocket = this.#debuggerConnection?.socket;
if (debuggerSocket && debuggerSocket.readyState === _ws.default.OPEN) {
debuggerSocket.send(
JSON.stringify({
method: "Runtime.consoleAPICalled",
params: {
args: [
{
type: "string",
value: message,
},
],
executionContextId: 0,
type: "error",
},
}),
);
}
}
#isPageFuseboxFrontend(pageId) {
const page = pageId == null ? null : this.#pages.get(pageId);
if (page == null) {
return null;
}
return this.#pageHasCapability(page, "prefersFuseboxFrontend");
}
dangerouslyGetSocket() {
return this.#deviceSocket;
}
#logFuseboxConsoleNotice() {
if (fuseboxConsoleNoticeLogged) {
return;
}
this.#deviceEventReporter?.logFuseboxConsoleNotice();
fuseboxConsoleNoticeLogged = true;
}
}
exports.default = Device;

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type { EventReporter } from "../types/EventReporter";
import type { CreateCustomMessageHandlerFn } from "./CustomMessageHandler";
import type { Page } from "./types";
import WS from "ws";
// should be aligned with
// https://github.com/facebook/react-native-devtools-frontend/blob/3d17e0fd462dc698db34586697cce2371b25e0d3/front_end/ui/legacy/components/utils/TargetDetachedDialog.ts#L50-L64
declare export const WS_CLOSE_REASON: {
PAGE_NOT_FOUND: "[PAGE_NOT_FOUND] Debugger page not found",
CONNECTION_LOST: "[CONNECTION_LOST] Connection lost to corresponding device",
RECREATING_DEVICE: "[RECREATING_DEVICE] Recreating device connection",
NEW_DEBUGGER_OPENED: "[NEW_DEBUGGER_OPENED] New debugger opened for the same app instance",
};
export type DeviceOptions = $ReadOnly<{
id: string,
name: string,
app: string,
socket: WS,
eventReporter: ?EventReporter,
createMessageMiddleware: ?CreateCustomMessageHandlerFn,
deviceRelativeBaseUrl: URL,
serverRelativeBaseUrl: URL,
isProfilingBuild: boolean,
}>;
/**
* Device class represents single device connection to Inspector Proxy. Each device
* can have multiple inspectable pages.
*/
declare export default class Device {
constructor(deviceOptions: DeviceOptions): void;
/**
* Used to recreate the device connection if there is a device ID collision.
* 1. Checks if the same device is attempting to reconnect for the same app.
* 2. If not, close both the device and debugger socket.
* 3. If the debugger connection can be reused, close the device socket only.
*
* This hack attempts to allow users to reload the app, either as result of a
* crash, or manually reloading, without having to restart the debugger.
*/
dangerouslyRecreateDevice(deviceOptions: DeviceOptions): void;
getName(): string;
getApp(): string;
getPagesList(): $ReadOnlyArray<Page>;
// Handles new debugger connection to this device:
// 1. Sends connect event to device
// 2. Forwards all messages from the debugger to device as wrappedEvent
// 3. Sends disconnect event to device when debugger connection socket closes.
handleDebuggerConnection(
socket: WS,
pageId: string,
$$PARAM_2$$: $ReadOnly<{
debuggerRelativeBaseUrl: URL,
userAgent: string | null,
}>,
): void;
dangerouslyGetSocket(): WS;
}

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
* @format
*/
import type { EventReporter } from "../types/EventReporter";
import type { CDPResponse } from "./cdp-types/messages";
import type { DeepReadOnly } from "./types";
type DeviceMetadata = Readonly<{
appId: string;
deviceId: string;
deviceName: string;
}>;
type RequestMetadata = Readonly<{
pageId: string | null;
frontendUserAgent: string | null;
prefersFuseboxFrontend: boolean | null;
}>;
type ResponseMetadata = Readonly<{
pageId: string | null;
frontendUserAgent: string | null;
prefersFuseboxFrontend: boolean | null;
}>;
declare class DeviceEventReporter {
constructor(eventReporter: EventReporter, metadata: DeviceMetadata);
logRequest(
req: Readonly<{ id: number; method: string }>,
origin: "debugger" | "proxy",
metadata: RequestMetadata,
): void;
logResponse(
res: DeepReadOnly<CDPResponse>,
origin: "device" | "proxy",
metadata: ResponseMetadata,
): void;
logProfilingTargetRegistered(): void;
logConnection(
connectedEntity: "debugger",
metadata: Readonly<{ pageId: string; frontendUserAgent: string | null }>,
): void;
logDisconnection(disconnectedEntity: "device" | "debugger"): void;
logProxyMessageHandlingError(
messageOrigin: "device" | "debugger",
error: Error,
message: string,
): void;
logFuseboxConsoleNotice(): void;
}
export default DeviceEventReporter;

View File

@@ -0,0 +1,194 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
var _ttlcache = _interopRequireDefault(require("@isaacs/ttlcache"));
function _interopRequireDefault(e) {
return e && e.__esModule ? e : { default: e };
}
class DeviceEventReporter {
#eventReporter;
#pendingCommands = new _ttlcache.default({
ttl: 10000,
dispose: (command, id, reason) => {
if (reason === "delete" || reason === "set") {
return;
}
this.#logExpiredCommand(command);
},
});
#metadata;
#deviceConnectedTimestamp;
constructor(eventReporter, metadata) {
this.#eventReporter = eventReporter;
this.#metadata = metadata;
this.#deviceConnectedTimestamp = Date.now();
}
logRequest(req, origin, metadata) {
this.#pendingCommands.set(req.id, {
method: req.method,
requestOrigin: origin,
requestTime: Date.now(),
metadata,
});
}
logResponse(res, origin, metadata) {
const pendingCommand = this.#pendingCommands.get(res.id);
if (!pendingCommand) {
this.#eventReporter.logEvent({
type: "debugger_command",
protocol: "CDP",
requestOrigin: null,
method: null,
status: "coded_error",
errorCode: "UNMATCHED_REQUEST_ID",
responseOrigin: "proxy",
timeSinceStart: null,
appId: this.#metadata.appId,
deviceId: this.#metadata.deviceId,
deviceName: this.#metadata.deviceName,
pageId: metadata.pageId,
frontendUserAgent: metadata.frontendUserAgent,
prefersFuseboxFrontend: metadata.prefersFuseboxFrontend,
connectionUptime: this.#deviceConnectedTimestamp - Date.now(),
});
return;
}
const timeSinceStart = Date.now() - pendingCommand.requestTime;
this.#pendingCommands.delete(res.id);
if (res.error) {
let { message } = res.error;
if ("data" in res.error) {
message += ` (${String(res.error.data)})`;
}
this.#eventReporter.logEvent({
type: "debugger_command",
requestOrigin: pendingCommand.requestOrigin,
method: pendingCommand.method,
protocol: "CDP",
status: "coded_error",
errorCode: "PROTOCOL_ERROR",
errorDetails: message,
responseOrigin: origin,
timeSinceStart,
appId: this.#metadata.appId,
deviceId: this.#metadata.deviceId,
deviceName: this.#metadata.deviceName,
pageId: pendingCommand.metadata.pageId,
frontendUserAgent: pendingCommand.metadata.frontendUserAgent,
prefersFuseboxFrontend: metadata.prefersFuseboxFrontend,
connectionUptime: this.#deviceConnectedTimestamp - Date.now(),
});
return;
}
this.#eventReporter.logEvent({
type: "debugger_command",
protocol: "CDP",
requestOrigin: pendingCommand.requestOrigin,
method: pendingCommand.method,
status: "success",
responseOrigin: origin,
timeSinceStart,
appId: this.#metadata.appId,
deviceId: this.#metadata.deviceId,
deviceName: this.#metadata.deviceName,
pageId: pendingCommand.metadata.pageId,
frontendUserAgent: pendingCommand.metadata.frontendUserAgent,
prefersFuseboxFrontend: metadata.prefersFuseboxFrontend,
connectionUptime: this.#deviceConnectedTimestamp - Date.now(),
});
}
logProfilingTargetRegistered() {
this.#eventReporter.logEvent({
type: "profiling_target_registered",
status: "success",
appId: this.#metadata.appId,
deviceName: this.#metadata.deviceName,
deviceId: this.#metadata.deviceId,
pageId: null,
});
}
logConnection(connectedEntity, metadata) {
this.#eventReporter.logEvent({
type: "connect_debugger_frontend",
status: "success",
appId: this.#metadata.appId,
deviceName: this.#metadata.deviceName,
deviceId: this.#metadata.deviceId,
pageId: metadata.pageId,
frontendUserAgent: metadata.frontendUserAgent,
});
}
logDisconnection(disconnectedEntity) {
const eventReporter = this.#eventReporter;
if (!eventReporter) {
return;
}
const errorCode =
disconnectedEntity === "device"
? "DEVICE_DISCONNECTED"
: "DEBUGGER_DISCONNECTED";
for (const pendingCommand of this.#pendingCommands.values()) {
this.#eventReporter.logEvent({
type: "debugger_command",
protocol: "CDP",
requestOrigin: pendingCommand.requestOrigin,
method: pendingCommand.method,
status: "coded_error",
errorCode,
responseOrigin: "proxy",
timeSinceStart: Date.now() - pendingCommand.requestTime,
appId: this.#metadata.appId,
deviceId: this.#metadata.deviceId,
deviceName: this.#metadata.deviceName,
pageId: pendingCommand.metadata.pageId,
frontendUserAgent: pendingCommand.metadata.frontendUserAgent,
prefersFuseboxFrontend: pendingCommand.metadata.prefersFuseboxFrontend,
connectionUptime: this.#deviceConnectedTimestamp - Date.now(),
});
}
this.#pendingCommands.clear();
}
logProxyMessageHandlingError(messageOrigin, error, message) {
this.#eventReporter.logEvent({
type: "proxy_error",
status: "error",
messageOrigin,
message,
error: error.message,
errorStack: error.stack,
appId: this.#metadata.appId,
deviceId: this.#metadata.deviceId,
deviceName: this.#metadata.deviceName,
pageId: null,
connectionUptime: this.#deviceConnectedTimestamp - Date.now(),
});
}
logFuseboxConsoleNotice() {
this.#eventReporter.logEvent({
type: "fusebox_console_notice",
});
}
#logExpiredCommand(pendingCommand) {
this.#eventReporter.logEvent({
type: "debugger_command",
protocol: "CDP",
requestOrigin: pendingCommand.requestOrigin,
method: pendingCommand.method,
status: "coded_error",
errorCode: "TIMED_OUT",
responseOrigin: "proxy",
timeSinceStart: Date.now() - pendingCommand.requestTime,
appId: this.#metadata.appId,
deviceId: this.#metadata.deviceId,
deviceName: this.#metadata.deviceName,
pageId: pendingCommand.metadata.pageId,
frontendUserAgent: pendingCommand.metadata.frontendUserAgent,
prefersFuseboxFrontend: pendingCommand.metadata.prefersFuseboxFrontend,
connectionUptime: this.#deviceConnectedTimestamp - Date.now(),
});
}
}
var _default = (exports.default = DeviceEventReporter);

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type { EventReporter } from "../types/EventReporter";
import type { CDPResponse } from "./cdp-types/messages";
import type { DeepReadOnly } from "./types";
type DeviceMetadata = $ReadOnly<{
appId: string,
deviceId: string,
deviceName: string,
}>;
type RequestMetadata = $ReadOnly<{
pageId: string | null,
frontendUserAgent: string | null,
prefersFuseboxFrontend: boolean | null,
}>;
type ResponseMetadata = $ReadOnly<{
pageId: string | null,
frontendUserAgent: string | null,
prefersFuseboxFrontend: boolean | null,
}>;
declare class DeviceEventReporter {
constructor(eventReporter: EventReporter, metadata: DeviceMetadata): void;
logRequest(
req: $ReadOnly<{ id: number, method: string, ... }>,
origin: "debugger" | "proxy",
metadata: RequestMetadata,
): void;
logResponse(
res: DeepReadOnly<CDPResponse<>>,
origin: "device" | "proxy",
metadata: ResponseMetadata,
): void;
logProfilingTargetRegistered(): void;
logConnection(
connectedEntity: "debugger",
metadata: $ReadOnly<{
pageId: string,
frontendUserAgent: string | null,
}>,
): void;
logDisconnection(disconnectedEntity: "device" | "debugger"): void;
logProxyMessageHandlingError(
messageOrigin: "device" | "debugger",
error: Error,
message: string,
): void;
logFuseboxConsoleNotice(): void;
}
declare export default typeof DeviceEventReporter;

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
* @format
*/
import type { DebuggerSessionIDs } from "../types/EventReporter";
export type EventLoopPerfTrackerArgs = {
perfMeasurementDuration: number;
minDelayPercentToReport: number;
onHighDelay: (args: OnHighDelayArgs) => void;
};
export type OnHighDelayArgs = {
eventLoopUtilization: number;
maxEventLoopDelayPercent: number;
duration: number;
debuggerSessionIDs: DebuggerSessionIDs;
connectionUptime: number;
};
declare class EventLoopPerfTracker {
constructor(args: EventLoopPerfTrackerArgs);
trackPerfThrottled(
debuggerSessionIDs: DebuggerSessionIDs,
connectionUptime: number,
): void;
}
export default EventLoopPerfTracker;

View File

@@ -0,0 +1,50 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
var _perf_hooks = require("perf_hooks");
var _timers = require("timers");
class EventLoopPerfTracker {
#perfMeasurementDuration;
#minDelayPercentToReport;
#onHighDelay;
#eventLoopPerfMeasurementOngoing;
constructor(args) {
this.#perfMeasurementDuration = args.perfMeasurementDuration;
this.#minDelayPercentToReport = args.minDelayPercentToReport;
this.#onHighDelay = args.onHighDelay;
this.#eventLoopPerfMeasurementOngoing = false;
}
trackPerfThrottled(debuggerSessionIDs, connectionUptime) {
if (this.#eventLoopPerfMeasurementOngoing) {
return;
}
this.#eventLoopPerfMeasurementOngoing = true;
const eluStart = _perf_hooks.performance.eventLoopUtilization();
const h = (0, _perf_hooks.monitorEventLoopDelay)({
resolution: 20,
});
h.enable();
(0, _timers.setTimeout)(() => {
const eluEnd = _perf_hooks.performance.eventLoopUtilization(eluStart);
h.disable();
const eventLoopUtilization = Math.floor(eluEnd.utilization * 100);
const maxEventLoopDelayPercent = Math.floor(
(h.max / 1e6 / this.#perfMeasurementDuration) * 100,
);
if (maxEventLoopDelayPercent >= this.#minDelayPercentToReport) {
this.#onHighDelay({
eventLoopUtilization,
maxEventLoopDelayPercent,
duration: this.#perfMeasurementDuration,
debuggerSessionIDs,
connectionUptime,
});
}
this.#eventLoopPerfMeasurementOngoing = false;
}, this.#perfMeasurementDuration).unref();
}
}
exports.default = EventLoopPerfTracker;

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
// $FlowFixMe[cannot-resolve-module] libdef missing in RN OSS
import type { DebuggerSessionIDs } from "../types/EventReporter";
export type EventLoopPerfTrackerArgs = {
perfMeasurementDuration: number,
minDelayPercentToReport: number,
onHighDelay: (args: OnHighDelayArgs) => void,
};
export type OnHighDelayArgs = {
eventLoopUtilization: number,
maxEventLoopDelayPercent: number,
duration: number,
debuggerSessionIDs: DebuggerSessionIDs,
connectionUptime: number,
};
declare export default class EventLoopPerfTracker {
constructor(args: EventLoopPerfTrackerArgs): void;
trackPerfThrottled(
debuggerSessionIDs: DebuggerSessionIDs,
connectionUptime: number,
): void;
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
* @format
*/
import type { EventReporter } from "../types/EventReporter";
import type { Experiments } from "../types/Experiments";
import type { Logger } from "../types/Logger";
import type { CreateCustomMessageHandlerFn } from "./CustomMessageHandler";
import type { PageDescription } from "./types";
import type { IncomingMessage, ServerResponse } from "http";
import WS from "ws";
export type GetPageDescriptionsConfig = {
requestorRelativeBaseUrl: URL;
logNoPagesForConnectedDevice?: boolean;
};
export interface InspectorProxyQueries {
/**
* Returns list of page descriptions ordered by device connection order, then
* page addition order.
*/
getPageDescriptions(
config: GetPageDescriptionsConfig,
): Array<PageDescription>;
}
/**
* Main Inspector Proxy class that connects JavaScript VM inside Android/iOS apps and JS debugger.
*/
declare class InspectorProxy implements InspectorProxyQueries {
constructor(
serverBaseUrl: string,
eventReporter: null | undefined | EventReporter,
experiments: Experiments,
logger?: Logger,
customMessageHandler: null | undefined | CreateCustomMessageHandlerFn,
trackEventLoopPerf?: boolean,
);
getPageDescriptions(
$$PARAM_0$$: GetPageDescriptionsConfig,
): Array<PageDescription>;
processRequest(
request: IncomingMessage,
response: ServerResponse,
next: ($$PARAM_0$$: null | undefined | Error) => unknown,
): void;
createWebSocketListeners(): { [path: string]: WS.Server };
}
export default InspectorProxy;

View File

@@ -0,0 +1,477 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
var _getBaseUrlFromRequest = _interopRequireDefault(
require("../utils/getBaseUrlFromRequest"),
);
var _getDevToolsFrontendUrl = _interopRequireDefault(
require("../utils/getDevToolsFrontendUrl"),
);
var _Device = _interopRequireDefault(require("./Device"));
var _EventLoopPerfTracker = _interopRequireDefault(
require("./EventLoopPerfTracker"),
);
var _InspectorProxyHeartbeat = _interopRequireDefault(
require("./InspectorProxyHeartbeat"),
);
var _nullthrows = _interopRequireDefault(require("nullthrows"));
var _url = _interopRequireDefault(require("url"));
var _ws = _interopRequireDefault(require("ws"));
function _interopRequireDefault(e) {
return e && e.__esModule ? e : { default: e };
}
const debug = require("debug")("Metro:InspectorProxy");
const WS_DEVICE_URL = "/inspector/device";
const WS_DEBUGGER_URL = "/inspector/debug";
const PAGES_LIST_JSON_URL = "/json";
const PAGES_LIST_JSON_URL_2 = "/json/list";
const PAGES_LIST_JSON_VERSION_URL = "/json/version";
const HEARTBEAT_TIME_BETWEEN_PINGS_MS = 5000;
const HEARTBEAT_TIMEOUT_MS = 60000;
const MIN_PING_TO_REPORT = 500;
const EVENT_LOOP_PERF_MEASUREMENT_MS = 5000;
const MIN_EVENT_LOOP_DELAY_PERCENT_TO_REPORT = 20;
const INTERNAL_ERROR_CODE = 1011;
const INTERNAL_ERROR_MESSAGES = {
UNREGISTERED_DEVICE:
"[UNREGISTERED_DEVICE] Debugger connection attempted for a device that was not registered",
INCORRECT_URL:
"[INCORRECT_URL] Incorrect URL - device and page IDs must be provided",
};
class InspectorProxy {
#serverBaseUrl;
#devices;
#deviceCounter = 0;
#eventReporter;
#experiments;
#customMessageHandler;
#logger;
#lastMessageTimestamp = null;
#eventLoopPerfTracker;
constructor(
serverBaseUrl,
eventReporter,
experiments,
logger,
customMessageHandler,
trackEventLoopPerf = false,
) {
this.#serverBaseUrl = new URL(serverBaseUrl);
this.#devices = new Map();
this.#eventReporter = eventReporter;
this.#experiments = experiments;
this.#logger = logger;
this.#customMessageHandler = customMessageHandler;
if (trackEventLoopPerf) {
this.#eventLoopPerfTracker = new _EventLoopPerfTracker.default({
perfMeasurementDuration: EVENT_LOOP_PERF_MEASUREMENT_MS,
minDelayPercentToReport: MIN_EVENT_LOOP_DELAY_PERCENT_TO_REPORT,
onHighDelay: ({
eventLoopUtilization,
maxEventLoopDelayPercent,
duration,
debuggerSessionIDs,
connectionUptime,
}) => {
debug(
"[perf] high event loop delay in the last %ds- event loop utilization='%d%' max event loop delay percent='%d%'",
duration / 1000,
eventLoopUtilization,
maxEventLoopDelayPercent,
);
this.#eventReporter?.logEvent({
type: "high_event_loop_delay",
eventLoopUtilization,
maxEventLoopDelayPercent,
duration,
connectionUptime,
...debuggerSessionIDs,
});
},
});
}
}
getPageDescriptions({
requestorRelativeBaseUrl,
logNoPagesForConnectedDevice = false,
}) {
let result = [];
Array.from(this.#devices.entries()).forEach(([deviceId, device]) => {
const devicePages = device
.getPagesList()
.map((page) =>
this.#buildPageDescription(
deviceId,
device,
page,
requestorRelativeBaseUrl,
),
);
if (
logNoPagesForConnectedDevice &&
devicePages.length === 0 &&
device.dangerouslyGetSocket()?.readyState === _ws.default.OPEN
) {
this.#logger?.warn(
`Waiting for a DevTools connection to app='%s' on device='%s'.
Try again when the main bundle for the app is built and connection is established.
If no connection occurs, try to:
- Restart the app. For Android, force stopping the app first might be required.
- Ensure a stable connection to the device.
- Ensure that the app is built in a mode that supports debugging.
- Take the app out of running in the background.`,
device.getApp(),
device.getName(),
);
this.#eventReporter?.logEvent({
type: "no_debug_pages_for_device",
appId: device.getApp(),
deviceName: device.getName(),
deviceId: deviceId,
pageId: null,
});
}
result = result.concat(devicePages);
});
return result;
}
processRequest(request, response, next) {
const pathname = _url.default.parse(request.url).pathname;
if (
pathname === PAGES_LIST_JSON_URL ||
pathname === PAGES_LIST_JSON_URL_2
) {
this.#sendJsonResponse(
response,
this.getPageDescriptions({
requestorRelativeBaseUrl:
(0, _getBaseUrlFromRequest.default)(request) ?? this.#serverBaseUrl,
logNoPagesForConnectedDevice: true,
}),
);
} else if (pathname === PAGES_LIST_JSON_VERSION_URL) {
this.#sendJsonResponse(response, {
Browser: "Mobile JavaScript",
"Protocol-Version": "1.1",
});
} else {
next();
}
}
createWebSocketListeners() {
return {
[WS_DEVICE_URL]: this.#createDeviceConnectionWSServer(),
[WS_DEBUGGER_URL]: this.#createDebuggerConnectionWSServer(),
};
}
#buildPageDescription(deviceId, device, page, requestorRelativeBaseUrl) {
const { host, protocol } = requestorRelativeBaseUrl;
const webSocketScheme = protocol === "https:" ? "wss" : "ws";
const webSocketUrlWithoutProtocol = `${host}${WS_DEBUGGER_URL}?device=${deviceId}&page=${page.id}`;
const webSocketDebuggerUrl = `${webSocketScheme}://${webSocketUrlWithoutProtocol}`;
const devtoolsFrontendUrl = (0, _getDevToolsFrontendUrl.default)(
this.#experiments,
webSocketDebuggerUrl,
this.#serverBaseUrl.origin,
{
relative: true,
useFuseboxEntryPoint: page.capabilities.prefersFuseboxFrontend,
},
);
return {
id: `${deviceId}-${page.id}`,
title: page.title,
description: page.description ?? page.app,
appId: page.app,
type: "node",
devtoolsFrontendUrl,
webSocketDebuggerUrl,
...(page.vm != null
? {
vm: page.vm,
}
: null),
deviceName: device.getName(),
reactNative: {
logicalDeviceId: deviceId,
capabilities: (0, _nullthrows.default)(page.capabilities),
},
};
}
#sendJsonResponse(response, object) {
const data = JSON.stringify(object, null, 2);
response.writeHead(200, {
"Content-Type": "application/json; charset=UTF-8",
"Cache-Control": "no-cache",
"Content-Length": Buffer.byteLength(data).toString(),
Connection: "close",
});
response.end(data);
}
#getTimeSinceLastCommunication() {
const timestamp = this.#lastMessageTimestamp;
return timestamp == null ? null : Date.now() - timestamp;
}
#onMessageFromDeviceOrDebugger(
message,
debuggerSessionIDs,
connectionUptime,
) {
if (message.includes('"event":"getPages"')) {
return;
}
this.#lastMessageTimestamp = Date.now();
this.#eventLoopPerfTracker?.trackPerfThrottled(
debuggerSessionIDs,
connectionUptime,
);
}
#createDeviceConnectionWSServer() {
const wss = new _ws.default.Server({
noServer: true,
perMessageDeflate: true,
maxPayload: 0,
});
wss.on("connection", async (socket, req) => {
const wssTimestamp = Date.now();
const fallbackDeviceId = String(this.#deviceCounter++);
const query = _url.default.parse(req.url || "", true).query || {};
const deviceId = query.device || fallbackDeviceId;
const deviceName = query.name || "Unknown";
const appName = query.app || "Unknown";
const isProfilingBuild = query.profiling === "true";
try {
const deviceRelativeBaseUrl =
(0, _getBaseUrlFromRequest.default)(req) ?? this.#serverBaseUrl;
const oldDevice = this.#devices.get(deviceId);
let newDevice;
const deviceOptions = {
id: deviceId,
name: deviceName,
app: appName,
socket,
eventReporter: this.#eventReporter,
createMessageMiddleware: this.#customMessageHandler,
deviceRelativeBaseUrl,
serverRelativeBaseUrl: this.#serverBaseUrl,
isProfilingBuild,
};
if (oldDevice) {
oldDevice.dangerouslyRecreateDevice(deviceOptions);
newDevice = oldDevice;
} else {
newDevice = new _Device.default(deviceOptions);
}
this.#devices.set(deviceId, newDevice);
debug(
"Got new device connection: name='%s', app=%s, device=%s, via=%s",
deviceName,
appName,
deviceId,
deviceRelativeBaseUrl.origin,
);
const debuggerSessionIDs = {
appId: newDevice?.getApp() || null,
deviceId,
deviceName: newDevice?.getName() || null,
pageId: null,
};
const heartbeat = new _InspectorProxyHeartbeat.default({
socket,
timeBetweenPings: HEARTBEAT_TIME_BETWEEN_PINGS_MS,
minHighPingToReport: MIN_PING_TO_REPORT,
timeoutMs: HEARTBEAT_TIMEOUT_MS,
onHighPing: (roundtripDuration) => {
debug(
"[high ping] [ Device ] %sms for app='%s' on device='%s'",
String(roundtripDuration).padStart(5),
debuggerSessionIDs.appId,
debuggerSessionIDs.deviceName,
);
this.#eventReporter?.logEvent({
type: "device_high_ping",
duration: roundtripDuration,
timeSinceLastCommunication: this.#getTimeSinceLastCommunication(),
connectionUptime: Date.now() - wssTimestamp,
...debuggerSessionIDs,
});
},
onTimeout: (roundtripDuration) => {
socket.terminate();
this.#logger?.error(
"[timeout] connection terminated with Device for app='%s' on device='%s' after not responding for %s seconds.",
debuggerSessionIDs.appId ?? "unknown",
debuggerSessionIDs.deviceName ?? "unknown",
String(roundtripDuration / 1000),
);
this.#eventReporter?.logEvent({
type: "device_timeout",
duration: roundtripDuration,
timeSinceLastCommunication: this.#getTimeSinceLastCommunication(),
connectionUptime: Date.now() - wssTimestamp,
...debuggerSessionIDs,
});
},
});
heartbeat.start();
socket.on("message", (message) =>
this.#onMessageFromDeviceOrDebugger(
message.toString(),
debuggerSessionIDs,
Date.now() - wssTimestamp,
),
);
socket.on("close", (code, reason) => {
debug(
"Connection closed to device='%s' for app='%s' with code='%s' and reason='%s'.",
deviceName,
appName,
String(code),
reason,
);
this.#eventReporter?.logEvent({
type: "device_connection_closed",
code,
reason,
timeSinceLastCommunication: this.#getTimeSinceLastCommunication(),
connectionUptime: Date.now() - wssTimestamp,
...debuggerSessionIDs,
});
if (this.#devices.get(deviceId)?.dangerouslyGetSocket() === socket) {
this.#devices.delete(deviceId);
}
});
} catch (error) {
this.#logger?.error(
"Connection failed to be established with app='%s' on device='%s' with error:",
appName,
deviceName,
error,
);
socket.close(INTERNAL_ERROR_CODE, error?.toString() ?? "Unknown error");
}
});
return wss;
}
#createDebuggerConnectionWSServer() {
const wss = new _ws.default.Server({
noServer: true,
perMessageDeflate: false,
maxPayload: 0,
});
wss.on("connection", async (socket, req) => {
const wssTimestamp = Date.now();
const query = _url.default.parse(req.url || "", true).query || {};
const deviceId = query.device;
const pageId = query.page;
const debuggerRelativeBaseUrl =
(0, _getBaseUrlFromRequest.default)(req) ?? this.#serverBaseUrl;
const device = deviceId ? this.#devices.get(deviceId) : undefined;
const debuggerSessionIDs = {
appId: device?.getApp() || null,
deviceId,
deviceName: device?.getName() || null,
pageId,
};
try {
if (deviceId == null || pageId == null) {
throw new Error(INTERNAL_ERROR_MESSAGES.INCORRECT_URL);
}
if (device == null) {
throw new Error(INTERNAL_ERROR_MESSAGES.UNREGISTERED_DEVICE);
}
debug(
"Connection established to DevTools for app='%s' on device='%s'.",
device.getApp() || "unknown",
device.getName() || "unknown",
);
const heartbeat = new _InspectorProxyHeartbeat.default({
socket,
timeBetweenPings: HEARTBEAT_TIME_BETWEEN_PINGS_MS,
minHighPingToReport: MIN_PING_TO_REPORT,
timeoutMs: HEARTBEAT_TIMEOUT_MS,
onHighPing: (roundtripDuration) => {
debug(
"[high ping] [DevTools] %sms for app='%s' on device='%s'",
String(roundtripDuration).padStart(5),
debuggerSessionIDs.appId,
debuggerSessionIDs.deviceName,
);
this.#eventReporter?.logEvent({
type: "debugger_high_ping",
duration: roundtripDuration,
timeSinceLastCommunication: this.#getTimeSinceLastCommunication(),
connectionUptime: Date.now() - wssTimestamp,
...debuggerSessionIDs,
});
},
onTimeout: (roundtripDuration) => {
socket.terminate();
this.#logger?.error(
"[timeout] connection terminated with DevTools for app='%s' on device='%s' after not responding for %s seconds.",
debuggerSessionIDs.appId ?? "unknown",
debuggerSessionIDs.deviceName ?? "unknown",
String(roundtripDuration / 1000),
);
this.#eventReporter?.logEvent({
type: "debugger_timeout",
duration: roundtripDuration,
timeSinceLastCommunication: this.#getTimeSinceLastCommunication(),
connectionUptime: Date.now() - wssTimestamp,
...debuggerSessionIDs,
});
},
});
heartbeat.start();
socket.on("message", (message) =>
this.#onMessageFromDeviceOrDebugger(
message.toString(),
debuggerSessionIDs,
Date.now() - wssTimestamp,
),
);
device.handleDebuggerConnection(socket, pageId, {
debuggerRelativeBaseUrl,
userAgent: req.headers["user-agent"] ?? query.userAgent ?? null,
});
socket.on("close", (code, reason) => {
debug(
"Connection closed to DevTools for app='%s' on device='%s' with code='%s' and reason='%s'.",
device.getApp() || "unknown",
device.getName() || "unknown",
String(code),
reason,
);
this.#eventReporter?.logEvent({
type: "debugger_connection_closed",
code,
reason,
timeSinceLastCommunication: this.#getTimeSinceLastCommunication(),
connectionUptime: Date.now() - wssTimestamp,
...debuggerSessionIDs,
});
});
} catch (error) {
this.#logger?.error(
"Connection failed to be established with DevTools for app='%s' on device='%s' and device id='%s' with error:",
device?.getApp() || "unknown",
device?.getName() || "unknown",
deviceId,
error,
);
socket.close(INTERNAL_ERROR_CODE, error?.toString() ?? "Unknown error");
this.#eventReporter?.logEvent({
type: "connect_debugger_frontend",
status: "error",
error,
...debuggerSessionIDs,
});
}
});
return wss;
}
}
exports.default = InspectorProxy;

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type { EventReporter } from "../types/EventReporter";
import type { Experiments } from "../types/Experiments";
import type { Logger } from "../types/Logger";
import type { CreateCustomMessageHandlerFn } from "./CustomMessageHandler";
import type { PageDescription } from "./types";
import type { IncomingMessage, ServerResponse } from "http";
import WS from "ws";
export type GetPageDescriptionsConfig = {
requestorRelativeBaseUrl: URL,
logNoPagesForConnectedDevice?: boolean,
};
export interface InspectorProxyQueries {
/**
* Returns list of page descriptions ordered by device connection order, then
* page addition order.
*/
getPageDescriptions(
config: GetPageDescriptionsConfig,
): Array<PageDescription>;
}
/**
* Main Inspector Proxy class that connects JavaScript VM inside Android/iOS apps and JS debugger.
*/
declare export default class InspectorProxy implements InspectorProxyQueries {
constructor(
serverBaseUrl: string,
eventReporter: ?EventReporter,
experiments: Experiments,
logger?: Logger,
customMessageHandler: ?CreateCustomMessageHandlerFn,
trackEventLoopPerf?: boolean,
): void;
getPageDescriptions(
$$PARAM_0$$: GetPageDescriptionsConfig,
): Array<PageDescription>;
// Process HTTP request sent to server. We only respond to 2 HTTP requests:
// 1. /json/version returns Chrome debugger protocol version that we use
// 2. /json and /json/list returns list of page descriptions (list of inspectable apps).
// This list is combined from all the connected devices.
processRequest(
request: IncomingMessage,
response: ServerResponse,
next: (?Error) => mixed,
): void;
createWebSocketListeners(): {
[path: string]: WS.Server,
};
}

View File

@@ -0,0 +1,24 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
* @format
*/
import WS from "ws";
export type HeartbeatTrackerArgs = {
socket: WS;
timeBetweenPings: number;
minHighPingToReport: number;
timeoutMs: number;
onTimeout: (roundtripDuration: number) => void;
onHighPing: (roundtripDuration: number) => void;
};
declare class InspectorProxyHeartbeat {
constructor(args: HeartbeatTrackerArgs);
start(): void;
}
export default InspectorProxyHeartbeat;

View File

@@ -0,0 +1,64 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
var _timers = require("timers");
var _ws = _interopRequireDefault(require("ws"));
function _interopRequireDefault(e) {
return e && e.__esModule ? e : { default: e };
}
class InspectorProxyHeartbeat {
#socket;
#timeBetweenPings;
#minHighPingToReport;
#timeoutMs;
#onTimeout;
#onHighPing;
constructor(args) {
this.#socket = args.socket;
this.#timeBetweenPings = args.timeBetweenPings;
this.#minHighPingToReport = args.minHighPingToReport;
this.#timeoutMs = args.timeoutMs;
this.#onTimeout = args.onTimeout;
this.#onHighPing = args.onHighPing;
}
start() {
let latestPingMs = Date.now();
let terminateTimeout;
const pingTimeout = (0, _timers.setTimeout)(() => {
if (this.#socket.readyState !== _ws.default.OPEN) {
pingTimeout.refresh();
return;
}
if (!terminateTimeout) {
terminateTimeout = (0, _timers.setTimeout)(() => {
if (this.#socket.readyState !== _ws.default.OPEN) {
terminateTimeout?.refresh();
return;
}
this.#onTimeout(this.#timeoutMs);
}, this.#timeoutMs).unref();
}
latestPingMs = Date.now();
this.#socket.ping();
}, this.#timeBetweenPings).unref();
this.#socket.on("pong", () => {
const roundtripDuration = Date.now() - latestPingMs;
if (roundtripDuration >= this.#minHighPingToReport) {
this.#onHighPing(roundtripDuration);
}
terminateTimeout?.refresh();
pingTimeout.refresh();
});
this.#socket.on("message", () => {
terminateTimeout?.refresh();
});
this.#socket.on("close", (code, reason) => {
terminateTimeout && (0, _timers.clearTimeout)(terminateTimeout);
(0, _timers.clearTimeout)(pingTimeout);
});
}
}
exports.default = InspectorProxyHeartbeat;

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import WS from "ws";
export type HeartbeatTrackerArgs = {
socket: WS,
timeBetweenPings: number,
minHighPingToReport: number,
timeoutMs: number,
onTimeout: (roundtripDuration: number) => void,
onHighPing: (roundtripDuration: number) => void,
};
declare export default class InspectorProxyHeartbeat {
constructor(args: HeartbeatTrackerArgs): void;
start(): void;
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
* @format
*/
import type { JSONSerializable } from "../types";
import type { Commands, Events } from "./protocol";
export type CDPEvent<TEvent extends keyof Events = "unknown"> = {
method: TEvent;
params: Events[TEvent];
};
export type CDPRequest<TCommand extends keyof Commands = "unknown"> = {
method: TCommand;
params: Commands[TCommand]["paramsType"];
id: number;
};
export type CDPResponse<TCommand extends keyof Commands = "unknown"> =
| { result: Commands[TCommand]["resultType"]; id: number }
| { error: CDPRequestError; id: number };
export type CDPRequestError = {
code: number;
message: string;
data?: JSONSerializable;
};
export type CDPClientMessage =
| CDPRequest<"Debugger.getScriptSource">
| CDPRequest<"Debugger.scriptParsed">
| CDPRequest<"Debugger.setBreakpointByUrl">
| CDPRequest<"Network.loadNetworkResource">
| CDPRequest;
export type CDPServerMessage =
| CDPEvent<"Debugger.scriptParsed">
| CDPEvent<"Runtime.consoleAPICalled">
| CDPEvent
| CDPResponse<"Debugger.getScriptSource">
| CDPResponse;

View File

@@ -0,0 +1 @@
"use strict";

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type { JSONSerializable } from "../types";
import type { Commands, Events } from "./protocol";
// Note: A CDP event is a JSON-RPC notification with no `id` member.
export type CDPEvent<TEvent: $Keys<Events> = "unknown"> = {
method: TEvent,
params: Events[TEvent],
};
export type CDPRequest<TCommand: $Keys<Commands> = "unknown"> = {
method: TCommand,
params: Commands[TCommand]["paramsType"],
id: number,
};
export type CDPResponse<TCommand: $Keys<Commands> = "unknown"> =
| {
result: Commands[TCommand]["resultType"],
id: number,
}
| {
error: CDPRequestError,
id: number,
};
export type CDPRequestError = {
code: number,
message: string,
data?: JSONSerializable,
};
export type CDPClientMessage =
| CDPRequest<"Debugger.getScriptSource">
| CDPRequest<"Debugger.scriptParsed">
| CDPRequest<"Debugger.setBreakpointByUrl">
| CDPRequest<"Network.loadNetworkResource">
| CDPRequest<>;
export type CDPServerMessage =
| CDPEvent<"Debugger.scriptParsed">
| CDPEvent<"Runtime.consoleAPICalled">
| CDPEvent<>
| CDPResponse<"Debugger.getScriptSource">
| CDPResponse<>;

View File

@@ -0,0 +1,106 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
* @format
*/
import type { JSONSerializable } from "../types";
type integer = number;
export interface Debugger {
GetScriptSourceParams: {
/**
* Id of the script to get source for.
*/
scriptId: string;
};
GetScriptSourceResult: {
/**
* Script source (empty in case of Wasm bytecode).
*/
scriptSource: string;
/**
* Wasm bytecode. (Encoded as a base64 string when passed over JSON)
*/
bytecode?: string;
};
SetBreakpointByUrlParams: {
/**
* Line number to set breakpoint at.
*/
lineNumber: integer;
/**
* URL of the resources to set breakpoint on.
*/
url?: string;
/**
* Regex pattern for the URLs of the resources to set breakpoints on. Either `url` or
* `urlRegex` must be specified.
*/
urlRegex?: string;
/**
* Script hash of the resources to set breakpoint on.
*/
scriptHash?: string;
/**
* Offset in the line to set breakpoint at.
*/
columnNumber?: integer;
/**
* Expression to use as a breakpoint condition. When specified, debugger will only stop on the
* breakpoint if this expression evaluates to true.
*/
condition?: string;
};
ScriptParsedEvent: {
/**
* Identifier of the script parsed.
*/
scriptId: string;
/**
* URL or name of the script parsed (if any).
*/
url: string;
/**
* URL of source map associated with script (if any).
*/
sourceMapURL: string;
};
ConsoleAPICalled: {
args: Array<{ type: string; value: string }>;
executionContextId: number;
stackTrace: {
timestamp: number;
type: string;
callFrames: Array<{
columnNumber: number;
lineNumber: number;
functionName: string;
scriptId: string;
url: string;
}>;
};
};
}
export type Events = {
"Debugger.scriptParsed": Debugger["ScriptParsedEvent"];
"Runtime.consoleAPICalled": Debugger["ConsoleAPICalled"];
[method: string]: JSONSerializable;
};
export type Commands = {
"Debugger.getScriptSource": {
paramsType: Debugger["GetScriptSourceParams"];
resultType: Debugger["GetScriptSourceResult"];
};
"Debugger.setBreakpointByUrl": {
paramsType: Debugger["SetBreakpointByUrlParams"];
resultType: void;
};
[method: string]: {
paramsType: JSONSerializable;
resultType: JSONSerializable;
};
};

View File

@@ -0,0 +1 @@
"use strict";

View File

@@ -0,0 +1,124 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
// Adapted from https://github.com/ChromeDevTools/devtools-protocol/blob/master/types/protocol.d.ts
import type { JSONSerializable } from "../types";
type integer = number;
export interface Debugger {
GetScriptSourceParams: {
/**
* Id of the script to get source for.
*/
scriptId: string,
};
GetScriptSourceResult: {
/**
* Script source (empty in case of Wasm bytecode).
*/
scriptSource: string,
/**
* Wasm bytecode. (Encoded as a base64 string when passed over JSON)
*/
bytecode?: string,
};
SetBreakpointByUrlParams: {
/**
* Line number to set breakpoint at.
*/
lineNumber: integer,
/**
* URL of the resources to set breakpoint on.
*/
url?: string,
/**
* Regex pattern for the URLs of the resources to set breakpoints on. Either `url` or
* `urlRegex` must be specified.
*/
urlRegex?: string,
/**
* Script hash of the resources to set breakpoint on.
*/
scriptHash?: string,
/**
* Offset in the line to set breakpoint at.
*/
columnNumber?: integer,
/**
* Expression to use as a breakpoint condition. When specified, debugger will only stop on the
* breakpoint if this expression evaluates to true.
*/
condition?: string,
};
ScriptParsedEvent: {
/**
* Identifier of the script parsed.
*/
scriptId: string,
/**
* URL or name of the script parsed (if any).
*/
url: string,
/**
* URL of source map associated with script (if any).
*/
sourceMapURL: string,
};
ConsoleAPICalled: {
args: Array<{ type: string, value: string }>,
executionContextId: number,
stackTrace: {
timestamp: number,
type: string,
callFrames: Array<{
columnNumber: number,
lineNumber: number,
functionName: string,
scriptId: string,
url: string,
}>,
},
};
}
export type Events = {
"Debugger.scriptParsed": Debugger["ScriptParsedEvent"],
"Runtime.consoleAPICalled": Debugger["ConsoleAPICalled"],
[method: string]: JSONSerializable,
};
export type Commands = {
"Debugger.getScriptSource": {
paramsType: Debugger["GetScriptSourceParams"],
resultType: Debugger["GetScriptSourceResult"],
},
"Debugger.setBreakpointByUrl": {
paramsType: Debugger["SetBreakpointByUrlParams"],
resultType: void,
},
[method: string]: {
paramsType: JSONSerializable,
resultType: JSONSerializable,
},
};

View File

@@ -0,0 +1,115 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
* @format
*/
/**
* A capability flag disables a specific feature/hack in the InspectorProxy
* layer by indicating that the target supports one or more modern CDP features.
*/
export type TargetCapabilityFlags = Readonly<{
/**
* The target supports a stable page representation across reloads.
*
* In the proxy, this disables legacy page reload emulation and the
* additional 'React Native Experimental' target in `/json/list`.
*
* In the launch flow, this allows targets to be matched directly by
* `logicalDeviceId`.
*/
nativePageReloads?: boolean;
/**
* The target supports fetching source code and source maps.
*
* In the proxy, this disables source fetching emulation and host rewrites.
*/
nativeSourceCodeFetching?: boolean;
/**
* The target supports the modern `rn_fusebox.html` entry point.
*
* In the launch flow, this controls the Chrome DevTools entrypoint that is used.
*/
prefersFuseboxFrontend?: boolean;
}>;
export type PageFromDevice = Readonly<{
id: string;
title: string;
/** Sent from modern targets only */
description?: string;
/** @deprecated This is sent from legacy targets only */
vm?: string;
app: string;
capabilities?: TargetCapabilityFlags;
}>;
export type Page = Readonly<
Omit<
PageFromDevice,
keyof { capabilities: NonNullable<PageFromDevice["capabilities"]> }
> & { capabilities: NonNullable<PageFromDevice["capabilities"]> }
>;
export type WrappedEvent = Readonly<{
event: "wrappedEvent";
payload: Readonly<{ pageId: string; wrappedEvent: string }>;
}>;
export type ConnectRequest = Readonly<{
event: "connect";
payload: Readonly<{ pageId: string }>;
}>;
export type DisconnectRequest = Readonly<{
event: "disconnect";
payload: Readonly<{ pageId: string }>;
}>;
export type GetPagesRequest = { event: "getPages" };
export type GetPagesResponse = {
event: "getPages";
payload: ReadonlyArray<PageFromDevice>;
};
export type MessageFromDevice =
| GetPagesResponse
| WrappedEvent
| DisconnectRequest;
export type MessageToDevice =
| GetPagesRequest
| WrappedEvent
| ConnectRequest
| DisconnectRequest;
export type PageDescription = Readonly<{
id: string;
title: string;
appId: string;
description: string;
type: string;
devtoolsFrontendUrl: string;
webSocketDebuggerUrl: string;
/** @deprecated Prefer `title` */
deviceName: string;
/** @deprecated This is sent from legacy targets only */
vm?: string;
reactNative: Readonly<{
logicalDeviceId: string;
capabilities: Page["capabilities"];
}>;
}>;
export type JsonPagesListResponse = Array<PageDescription>;
export type JsonVersionResponse = Readonly<{
Browser: string;
"Protocol-Version": string;
}>;
export type JSONSerializable =
| boolean
| number
| string
| null
| ReadonlyArray<JSONSerializable>
| { readonly [$$Key$$: string]: JSONSerializable };
export type DeepReadOnly<T> =
T extends ReadonlyArray<infer V>
? ReadonlyArray<DeepReadOnly<V>>
: T extends {}
? { readonly [K in keyof T]: DeepReadOnly<T[K]> }
: T;

View File

@@ -0,0 +1 @@
"use strict";

View File

@@ -0,0 +1,152 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
/**
* A capability flag disables a specific feature/hack in the InspectorProxy
* layer by indicating that the target supports one or more modern CDP features.
*/
export type TargetCapabilityFlags = $ReadOnly<{
/**
* The target supports a stable page representation across reloads.
*
* In the proxy, this disables legacy page reload emulation and the
* additional 'React Native Experimental' target in `/json/list`.
*
* In the launch flow, this allows targets to be matched directly by
* `logicalDeviceId`.
*/
nativePageReloads?: boolean,
/**
* The target supports fetching source code and source maps.
*
* In the proxy, this disables source fetching emulation and host rewrites.
*/
nativeSourceCodeFetching?: boolean,
/**
* The target supports the modern `rn_fusebox.html` entry point.
*
* In the launch flow, this controls the Chrome DevTools entrypoint that is used.
*/
prefersFuseboxFrontend?: boolean,
}>;
// Page information received from the device. New page is created for
// each new instance of VM and can appear when user reloads React Native
// application.
export type PageFromDevice = $ReadOnly<{
id: string,
title: string,
/** Sent from modern targets only */
description?: string,
/** @deprecated This is sent from legacy targets only */
vm?: string,
app: string,
capabilities?: TargetCapabilityFlags,
}>;
export type Page = $ReadOnly<{
...PageFromDevice,
capabilities: $NonMaybeType<PageFromDevice["capabilities"]>,
}>;
// Chrome Debugger Protocol message/event passed between device and debugger.
export type WrappedEvent = $ReadOnly<{
event: "wrappedEvent",
payload: $ReadOnly<{
pageId: string,
wrappedEvent: string,
}>,
}>;
// Request sent from Inspector Proxy to Device when new debugger is connected
// to particular page.
export type ConnectRequest = $ReadOnly<{
event: "connect",
payload: $ReadOnly<{ pageId: string }>,
}>;
// Request sent from Inspector Proxy to Device to notify that debugger is
// disconnected.
export type DisconnectRequest = $ReadOnly<{
event: "disconnect",
payload: $ReadOnly<{ pageId: string }>,
}>;
// Request sent from Inspector Proxy to Device to get a list of pages.
export type GetPagesRequest = { event: "getPages" };
// Response to GetPagesRequest containing a list of page infos.
export type GetPagesResponse = {
event: "getPages",
payload: $ReadOnlyArray<PageFromDevice>,
};
// Union type for all possible messages sent from device to Inspector Proxy.
export type MessageFromDevice =
| GetPagesResponse
| WrappedEvent
| DisconnectRequest;
// Union type for all possible messages sent from Inspector Proxy to device.
export type MessageToDevice =
| GetPagesRequest
| WrappedEvent
| ConnectRequest
| DisconnectRequest;
// Page description object that is sent in response to /json HTTP request from debugger.
export type PageDescription = $ReadOnly<{
id: string,
title: string,
appId: string,
description: string,
type: string,
devtoolsFrontendUrl: string,
webSocketDebuggerUrl: string,
// React Native specific fields
/** @deprecated Prefer `title` */
deviceName: string,
/** @deprecated This is sent from legacy targets only */
vm?: string,
// React Native specific metadata
reactNative: $ReadOnly<{
logicalDeviceId: string,
capabilities: Page["capabilities"],
}>,
}>;
export type JsonPagesListResponse = Array<PageDescription>;
// Response to /json/version HTTP request from the debugger specifying browser type and
// Chrome protocol version.
export type JsonVersionResponse = $ReadOnly<{
Browser: string,
"Protocol-Version": string,
}>;
export type JSONSerializable =
| boolean
| number
| string
| null
| $ReadOnlyArray<JSONSerializable>
| { +[string]: JSONSerializable };
export type DeepReadOnly<T> =
T extends $ReadOnlyArray<infer V>
? $ReadOnlyArray<DeepReadOnly<V>>
: T extends { ... }
? { +[K in keyof T]: DeepReadOnly<T[K]> }
: T;