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,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 { CreateCustomMessageHandlerFn } from "./inspector-proxy/CustomMessageHandler";
import type { BrowserLauncher } from "./types/BrowserLauncher";
import type { EventReporter } from "./types/EventReporter";
import type { ExperimentsConfig } from "./types/Experiments";
import type { Logger } from "./types/Logger";
import type { NextHandleFunction } from "connect";
type Options = Readonly<{
/**
* The base URL to the dev server, as reachable from the machine on which
* dev-middleware is hosted. Typically `http://localhost:${metroPort}`.
*/
serverBaseUrl: string;
logger?: Logger;
/**
* An interface for integrators to provide a custom implementation for
* opening URLs in a web browser.
*
* This is an unstable API with no semver guarantees.
*/
unstable_browserLauncher?: BrowserLauncher;
/**
* An interface for logging events.
*
* This is an unstable API with no semver guarantees.
*/
unstable_eventReporter?: EventReporter;
/**
* The set of experimental features to enable.
*
* This is an unstable API with no semver guarantees.
*/
unstable_experiments?: ExperimentsConfig;
/**
* Create custom handler to add support for unsupported CDP events, or debuggers.
* This handler is instantiated per logical device and debugger pair.
*
* This is an unstable API with no semver guarantees.
*/
unstable_customInspectorMessageHandler?: CreateCustomMessageHandlerFn;
/**
* Whether to measure the event loop performance of inspector proxy and log report it via the event reporter.
*
* This is an unstable API with no semver guarantees.
*/
unstable_trackInspectorProxyEventLoopPerf?: boolean;
}>;
type DevMiddlewareAPI = Readonly<{
middleware: NextHandleFunction;
websocketEndpoints: { [path: string]: ws$WebSocketServer };
}>;
declare function createDevMiddleware($$PARAM_0$$: Options): DevMiddlewareAPI;
export default createDevMiddleware;

View File

@@ -0,0 +1,140 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = createDevMiddleware;
var _InspectorProxy = _interopRequireDefault(
require("./inspector-proxy/InspectorProxy"),
);
var _openDebuggerMiddleware = _interopRequireDefault(
require("./middleware/openDebuggerMiddleware"),
);
var _DefaultBrowserLauncher = _interopRequireDefault(
require("./utils/DefaultBrowserLauncher"),
);
var _debuggerFrontend = _interopRequireDefault(
require("@react-native/debugger-frontend"),
);
var _connect = _interopRequireDefault(require("connect"));
var _path = _interopRequireDefault(require("path"));
var _serveStatic = _interopRequireDefault(require("serve-static"));
function _interopRequireDefault(e) {
return e && e.__esModule ? e : { default: e };
}
function createDevMiddleware({
serverBaseUrl,
logger,
unstable_browserLauncher = _DefaultBrowserLauncher.default,
unstable_eventReporter,
unstable_experiments: experimentConfig = {},
unstable_customInspectorMessageHandler,
unstable_trackInspectorProxyEventLoopPerf = false,
}) {
const experiments = getExperiments(experimentConfig);
const eventReporter = createWrappedEventReporter(
unstable_eventReporter,
logger,
experiments,
);
const inspectorProxy = new _InspectorProxy.default(
serverBaseUrl,
eventReporter,
experiments,
logger,
unstable_customInspectorMessageHandler,
unstable_trackInspectorProxyEventLoopPerf,
);
const middleware = (0, _connect.default)()
.use(
"/open-debugger",
(0, _openDebuggerMiddleware.default)({
serverBaseUrl,
inspectorProxy,
browserLauncher: unstable_browserLauncher,
eventReporter,
experiments,
logger,
}),
)
.use(
"/debugger-frontend/embedder-static/embedderScript.js",
(_req, res) => {
res.setHeader("Content-Type", "application/javascript");
res.end("");
},
)
.use(
"/debugger-frontend",
(0, _serveStatic.default)(_path.default.join(_debuggerFrontend.default), {
fallthrough: false,
}),
)
.use((...args) => inspectorProxy.processRequest(...args));
return {
middleware,
websocketEndpoints: inspectorProxy.createWebSocketListeners(),
};
}
function getExperiments(config) {
return {
enableOpenDebuggerRedirect: config.enableOpenDebuggerRedirect ?? false,
enableNetworkInspector: config.enableNetworkInspector ?? false,
enableStandaloneFuseboxShell: config.enableStandaloneFuseboxShell ?? true,
};
}
function createWrappedEventReporter(reporter, logger, experiments) {
return {
logEvent(event) {
switch (event.type) {
case "profiling_target_registered":
logger?.info(
"Profiling build target '%s' registered for debugging",
event.appId ?? "unknown",
);
break;
case "fusebox_console_notice":
logger?.info(
"\u001B[1m\u001B[7m💡 JavaScript logs have moved!\u001B[22m They can now be " +
"viewed in React Native DevTools. Tip: Type \u001B[1mj\u001B[22m in " +
"the terminal to open" +
(experiments.enableStandaloneFuseboxShell
? ""
: " (requires Google Chrome or Microsoft Edge)") +
".\u001B[27m",
);
break;
case "fusebox_shell_preparation_attempt":
switch (event.result.code) {
case "success":
case "not_implemented":
break;
case "unexpected_error": {
let message =
event.result.humanReadableMessage ??
"An unknown error occurred while installing React Native DevTools.";
if (event.result.verboseInfo != null) {
message += ` Details:\n\n${event.result.verboseInfo}`;
} else {
message += ".";
}
logger?.error(message);
break;
}
case "possible_corruption":
case "platform_not_supported":
case "likely_offline":
logger?.warn(
event.result.humanReadableMessage ??
`An error of type ${event.result.code} occurred while installing React Native DevTools.`,
);
break;
default:
event.result.code;
break;
}
}
reporter?.logEvent(event);
},
};
}

View File

@@ -0,0 +1,72 @@
/**
* 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 { CreateCustomMessageHandlerFn } from "./inspector-proxy/CustomMessageHandler";
import type { BrowserLauncher } from "./types/BrowserLauncher";
import type { EventReporter } from "./types/EventReporter";
import type { ExperimentsConfig } from "./types/Experiments";
import type { Logger } from "./types/Logger";
import type { NextHandleFunction } from "connect";
type Options = $ReadOnly<{
/**
* The base URL to the dev server, as reachable from the machine on which
* dev-middleware is hosted. Typically `http://localhost:${metroPort}`.
*/
serverBaseUrl: string,
logger?: Logger,
/**
* An interface for integrators to provide a custom implementation for
* opening URLs in a web browser.
*
* This is an unstable API with no semver guarantees.
*/
unstable_browserLauncher?: BrowserLauncher,
/**
* An interface for logging events.
*
* This is an unstable API with no semver guarantees.
*/
unstable_eventReporter?: EventReporter,
/**
* The set of experimental features to enable.
*
* This is an unstable API with no semver guarantees.
*/
unstable_experiments?: ExperimentsConfig,
/**
* Create custom handler to add support for unsupported CDP events, or debuggers.
* This handler is instantiated per logical device and debugger pair.
*
* This is an unstable API with no semver guarantees.
*/
unstable_customInspectorMessageHandler?: CreateCustomMessageHandlerFn,
/**
* Whether to measure the event loop performance of inspector proxy and log report it via the event reporter.
*
* This is an unstable API with no semver guarantees.
*/
unstable_trackInspectorProxyEventLoopPerf?: boolean,
}>;
type DevMiddlewareAPI = $ReadOnly<{
middleware: NextHandleFunction,
websocketEndpoints: { [path: string]: ws$WebSocketServer },
}>;
declare export default function createDevMiddleware(
$$PARAM_0$$: Options,
): DevMiddlewareAPI;

View File

@@ -0,0 +1,23 @@
/**
* 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 {
BrowserLauncher,
DebuggerShellPreparationResult,
} from "./types/BrowserLauncher";
export type { EventReporter, ReportableEvent } from "./types/EventReporter";
export type {
CustomMessageHandler,
CustomMessageHandlerConnection,
CreateCustomMessageHandlerFn,
} from "./inspector-proxy/CustomMessageHandler";
export type { Logger } from "./types/Logger";
export { default as unstable_DefaultBrowserLauncher } from "./utils/DefaultBrowserLauncher";
export { default as createDevMiddleware } from "./createDevMiddleware";

View File

@@ -0,0 +1,26 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
Object.defineProperty(exports, "createDevMiddleware", {
enumerable: true,
get: function () {
return _createDevMiddleware.default;
},
});
Object.defineProperty(exports, "unstable_DefaultBrowserLauncher", {
enumerable: true,
get: function () {
return _DefaultBrowserLauncher.default;
},
});
var _DefaultBrowserLauncher = _interopRequireDefault(
require("./utils/DefaultBrowserLauncher"),
);
var _createDevMiddleware = _interopRequireDefault(
require("./createDevMiddleware"),
);
function _interopRequireDefault(e) {
return e && e.__esModule ? e : { default: e };
}

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.
*
* @flow strict-local
* @format
*/
export type {
BrowserLauncher,
DebuggerShellPreparationResult,
} from "./types/BrowserLauncher";
export type { EventReporter, ReportableEvent } from "./types/EventReporter";
export type {
CustomMessageHandler,
CustomMessageHandlerConnection,
CreateCustomMessageHandlerFn,
} from "./inspector-proxy/CustomMessageHandler";
export type { Logger } from "./types/Logger";
export { default as unstable_DefaultBrowserLauncher } from "./utils/DefaultBrowserLauncher";
export { default as createDevMiddleware } from "./createDevMiddleware";

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;

View File

@@ -0,0 +1,36 @@
/**
* 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 { InspectorProxyQueries } from "../inspector-proxy/InspectorProxy";
import type { BrowserLauncher } from "../types/BrowserLauncher";
import type { EventReporter } from "../types/EventReporter";
import type { Experiments } from "../types/Experiments";
import type { Logger } from "../types/Logger";
import type { NextHandleFunction } from "connect";
type Options = Readonly<{
serverBaseUrl: string;
logger?: Logger;
browserLauncher: BrowserLauncher;
eventReporter?: EventReporter;
experiments: Experiments;
inspectorProxy: InspectorProxyQueries;
}>;
/**
* Open the debugger frontend for a given CDP target.
*
* Currently supports React Native DevTools (rn_fusebox.html) and legacy Hermes
* (rn_inspector.html) targets.
*
* @see https://chromedevtools.github.io/devtools-protocol/
*/
declare function openDebuggerMiddleware(
$$PARAM_0$$: Options,
): NextHandleFunction;
export default openDebuggerMiddleware;

View File

@@ -0,0 +1,216 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = openDebuggerMiddleware;
var _getDevToolsFrontendUrl = _interopRequireDefault(
require("../utils/getDevToolsFrontendUrl"),
);
var _crypto = require("crypto");
var _url = _interopRequireDefault(require("url"));
function _interopRequireDefault(e) {
return e && e.__esModule ? e : { default: e };
}
const LEGACY_SYNTHETIC_PAGE_TITLE =
"React Native Experimental (Improved Chrome Reloads)";
function openDebuggerMiddleware({
serverBaseUrl,
logger,
browserLauncher,
eventReporter,
experiments,
inspectorProxy,
}) {
let shellPreparationPromise;
if (experiments.enableStandaloneFuseboxShell) {
shellPreparationPromise =
browserLauncher?.unstable_prepareFuseboxShell?.() ??
Promise.resolve({
code: "not_implemented",
});
shellPreparationPromise = shellPreparationPromise.then((result) => {
eventReporter?.logEvent({
type: "fusebox_shell_preparation_attempt",
result,
});
return result;
});
}
return async (req, res, next) => {
if (
req.method === "POST" ||
(experiments.enableOpenDebuggerRedirect && req.method === "GET")
) {
const parsedUrl = _url.default.parse(req.url, true);
const query = parsedUrl.query;
const targets = inspectorProxy
.getPageDescriptions({
requestorRelativeBaseUrl: new URL(serverBaseUrl),
})
.filter((app) => {
const betterReloadingSupport =
app.title === LEGACY_SYNTHETIC_PAGE_TITLE ||
app.reactNative.capabilities?.nativePageReloads === true;
if (!betterReloadingSupport) {
logger?.warn(
"Ignoring DevTools app debug target for '%s' with title '%s' and 'nativePageReloads' capability set to '%s'. ",
app.appId,
app.title,
String(app.reactNative.capabilities?.nativePageReloads),
);
}
return betterReloadingSupport;
});
let target;
const launchType = req.method === "POST" ? "launch" : "redirect";
if (
typeof query.target === "string" ||
typeof query.appId === "string" ||
typeof query.device === "string"
) {
logger?.info(
(launchType === "launch" ? "Launching" : "Redirecting to") +
" DevTools...",
);
target = targets.find(
(_target) =>
(query.target == null || _target.id === query.target) &&
(query.appId == null ||
(_target.appId === query.appId &&
_target.title === LEGACY_SYNTHETIC_PAGE_TITLE)) &&
(query.device == null ||
_target.reactNative.logicalDeviceId === query.device),
);
} else if (targets.length > 0) {
logger?.info(
(launchType === "launch" ? "Launching" : "Redirecting to") +
` DevTools${targets.length === 1 ? "" : " for most recently connected target"}...`,
);
target = targets[targets.length - 1];
}
if (!target) {
res.writeHead(404);
res.end("Unable to find debugger target");
logger?.warn(
"No compatible apps connected. React Native DevTools can only be used with the Hermes engine.",
);
eventReporter?.logEvent({
type: "launch_debugger_frontend",
launchType,
status: "coded_error",
errorCode: "NO_APPS_FOUND",
});
return;
}
const useFuseboxEntryPoint =
target.reactNative.capabilities?.prefersFuseboxFrontend ?? false;
try {
switch (launchType) {
case "launch": {
const frontendUrl = (0, _getDevToolsFrontendUrl.default)(
experiments,
target.webSocketDebuggerUrl,
serverBaseUrl,
{
launchId: query.launchId,
telemetryInfo: query.telemetryInfo,
appId: target.appId,
useFuseboxEntryPoint,
panel: query.panel,
},
);
let shouldUseStandaloneFuseboxShell =
useFuseboxEntryPoint && experiments.enableStandaloneFuseboxShell;
if (shouldUseStandaloneFuseboxShell) {
const shellPreparationResult = await shellPreparationPromise;
switch (shellPreparationResult.code) {
case "success":
case "not_implemented":
break;
case "platform_not_supported":
case "possible_corruption":
case "likely_offline":
case "unexpected_error":
shouldUseStandaloneFuseboxShell = false;
break;
default:
shellPreparationResult.code;
}
}
if (shouldUseStandaloneFuseboxShell) {
const windowKey = (0, _crypto.createHash)("sha256")
.update(
[
serverBaseUrl,
target.webSocketDebuggerUrl,
target.appId,
].join("-"),
)
.digest("hex");
if (!browserLauncher.unstable_showFuseboxShell) {
throw new Error(
"Fusebox shell is not supported by the current browser launcher",
);
}
await browserLauncher.unstable_showFuseboxShell(
frontendUrl,
windowKey,
);
} else {
await browserLauncher.launchDebuggerAppWindow(frontendUrl);
}
res.writeHead(200);
res.end();
break;
}
case "redirect":
res.writeHead(302, {
Location: (0, _getDevToolsFrontendUrl.default)(
experiments,
target.webSocketDebuggerUrl,
serverBaseUrl,
{
relative: true,
launchId: query.launchId,
telemetryInfo: query.telemetryInfo,
appId: target.appId,
useFuseboxEntryPoint,
},
),
});
res.end();
break;
default:
}
eventReporter?.logEvent({
type: "launch_debugger_frontend",
launchType,
status: "success",
appId: target.appId,
deviceId: target.reactNative.logicalDeviceId,
pageId: target.id,
deviceName: target.deviceName,
targetDescription: target.description,
prefersFuseboxFrontend: useFuseboxEntryPoint,
});
return;
} catch (e) {
logger?.error(
"Error launching DevTools: " + e.message ?? "Unknown error",
);
res.writeHead(500);
res.end();
eventReporter?.logEvent({
type: "launch_debugger_frontend",
launchType,
status: "error",
error: e,
prefersFuseboxFrontend: useFuseboxEntryPoint,
});
return;
}
}
next();
};
}

View File

@@ -0,0 +1,36 @@
/**
* 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 { InspectorProxyQueries } from "../inspector-proxy/InspectorProxy";
import type { BrowserLauncher } from "../types/BrowserLauncher";
import type { EventReporter } from "../types/EventReporter";
import type { Experiments } from "../types/Experiments";
import type { Logger } from "../types/Logger";
import type { NextHandleFunction } from "connect";
type Options = $ReadOnly<{
serverBaseUrl: string,
logger?: Logger,
browserLauncher: BrowserLauncher,
eventReporter?: EventReporter,
experiments: Experiments,
inspectorProxy: InspectorProxyQueries,
}>;
/**
* Open the debugger frontend for a given CDP target.
*
* Currently supports React Native DevTools (rn_fusebox.html) and legacy Hermes
* (rn_inspector.html) targets.
*
* @see https://chromedevtools.github.io/devtools-protocol/
*/
declare export default function openDebuggerMiddleware(
$$PARAM_0$$: Options,
): NextHandleFunction;

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 { DebuggerShellPreparationResult } from "@react-native/debugger-shell";
export type { DebuggerShellPreparationResult };
/**
* An interface for integrators to provide a custom implementation for
* opening URLs in a web browser.
*/
export interface BrowserLauncher {
/**
* Attempt to open a debugger frontend URL in a browser app window,
* optionally returning an object to control the launched browser instance.
* The browser used should be capable of running Chrome DevTools.
*
* The provided URL is based on serverBaseUrl, and therefore reachable from
* the host of dev-middleware. Implementations are responsible for rewriting
* this as necessary where the server is remote.
*/
launchDebuggerAppWindow: (url: string) => Promise<void>;
/**
* Attempt to open a debugger frontend URL in a standalone shell window
* designed specifically for React Native DevTools. The provided windowKey
* should be used to identify an existing window that can be reused instead
* of opening a new one.
*
* Implementations SHOULD treat an existing session with the same windowKey
* (as long as it's still connected and healthy) as equaivalent to a new
* session with the new URL, even if the launch URLs for the two sessions are
* not identical. Implementations SHOULD NOT unnecessarily close and reopen
* the connection when reusing a session. Implementations SHOULD process any
* changed/new parameters in the URL and update the session accordingly (e.g.
* to preserve telemetry data that may have changed).
*
* The provided URL is based on serverBaseUrl, and therefore reachable from
* the host of dev-middleware. Implementations are responsible for rewriting
* this as necessary where the server is remote.
*/
readonly unstable_showFuseboxShell?: (
url: string,
windowKey: string,
) => Promise<void>;
/**
* Attempt to prepare the debugger shell for use and returns a coded result
* that can be used to advise the user on how to proceed in case of failure.
*
* This function MAY be called multiple times or not at all. Implementers
* SHOULD use the opportunity to prefetch and cache any expensive resources (e.g
* platform-specific binaries needed in order to show the Fusebox shell). After a
* successful call, subsequent calls SHOULD complete quickly. The implementation
* SHOULD NOT return a rejecting promise in any case, and instead SHOULD report
* errors via the returned result object.
*/
readonly unstable_prepareFuseboxShell?: () => Promise<DebuggerShellPreparationResult>;
}

View File

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

View File

@@ -0,0 +1,66 @@
/**
* 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 { DebuggerShellPreparationResult } from "@react-native/debugger-shell";
export type { DebuggerShellPreparationResult };
/**
* An interface for integrators to provide a custom implementation for
* opening URLs in a web browser.
*/
export interface BrowserLauncher {
/**
* Attempt to open a debugger frontend URL in a browser app window,
* optionally returning an object to control the launched browser instance.
* The browser used should be capable of running Chrome DevTools.
*
* The provided URL is based on serverBaseUrl, and therefore reachable from
* the host of dev-middleware. Implementations are responsible for rewriting
* this as necessary where the server is remote.
*/
launchDebuggerAppWindow: (url: string) => Promise<void>;
/**
* Attempt to open a debugger frontend URL in a standalone shell window
* designed specifically for React Native DevTools. The provided windowKey
* should be used to identify an existing window that can be reused instead
* of opening a new one.
*
* Implementations SHOULD treat an existing session with the same windowKey
* (as long as it's still connected and healthy) as equaivalent to a new
* session with the new URL, even if the launch URLs for the two sessions are
* not identical. Implementations SHOULD NOT unnecessarily close and reopen
* the connection when reusing a session. Implementations SHOULD process any
* changed/new parameters in the URL and update the session accordingly (e.g.
* to preserve telemetry data that may have changed).
*
* The provided URL is based on serverBaseUrl, and therefore reachable from
* the host of dev-middleware. Implementations are responsible for rewriting
* this as necessary where the server is remote.
*/
+unstable_showFuseboxShell?: (
url: string,
windowKey: string,
) => Promise<void>;
/**
* Attempt to prepare the debugger shell for use and returns a coded result
* that can be used to advise the user on how to proceed in case of failure.
*
* This function MAY be called multiple times or not at all. Implementers
* SHOULD use the opportunity to prefetch and cache any expensive resources (e.g
* platform-specific binaries needed in order to show the Fusebox shell). After a
* successful call, subsequent calls SHOULD complete quickly. The implementation
* SHOULD NOT return a rejecting promise in any case, and instead SHOULD report
* errors via the returned result object.
*/
+unstable_prepareFuseboxShell?: () => Promise<DebuggerShellPreparationResult>;
}

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.
*
*
* @format
*/
import type { DebuggerShellPreparationResult } from "./BrowserLauncher";
type SuccessResult<Props extends {} | void = {}> =
/**
* > 15 | ...Props,
* | ^^^^^^^^ Unsupported feature: Translating "object types with spreads in the middle or at the end" is currently not supported.
**/
any;
type ErrorResult<ErrorT = unknown, Props extends {} | void = {}> =
/**
* > 22 | ...Props,
* | ^^^^^^^^ Unsupported feature: Translating "object types with spreads in the middle or at the end" is currently not supported.
**/
any;
type CodedErrorResult<ErrorCode extends string> = {
status: "coded_error";
errorCode: ErrorCode;
errorDetails?: string;
};
export type DebuggerSessionIDs = {
appId: string | null;
deviceName: string | null;
deviceId: string | null;
pageId: string | null;
};
export type ConnectionUptime = { connectionUptime: number };
export type ReportableEvent =
| /**
* > 46 | ...
* | ^^^
* > 47 | | SuccessResult<{
* | ^^^^^^^^^^^^^^^^^^^^^^^^^
* > 48 | targetDescription: string,
* | ^^^^^^^^^^^^^^^^^^^^^^^^^
* > 49 | prefersFuseboxFrontend: boolean,
* | ^^^^^^^^^^^^^^^^^^^^^^^^^
* > 50 | ...DebuggerSessionIDs,
* | ^^^^^^^^^^^^^^^^^^^^^^^^^
* > 51 | }>
* | ^^^^^^^^^^^^^^^^^^^^^^^^^
* > 52 | | ErrorResult<mixed>
* | ^^^^^^^^^^^^^^^^^^^^^^^^^
* > 53 | | CodedErrorResult<"NO_APPS_FOUND">,
* | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unsupported feature: Translating "object types with spreads in the middle or at the end" is currently not supported.
**/
any
| /**
* > 57 | ...
* | ^^^
* > 58 | | SuccessResult<{
* | ^^^^^^^^^^^^^^^^^^^^^^^^^
* > 59 | ...DebuggerSessionIDs,
* | ^^^^^^^^^^^^^^^^^^^^^^^^^
* > 60 | frontendUserAgent: string | null,
* | ^^^^^^^^^^^^^^^^^^^^^^^^^
* > 61 | }>
* | ^^^^^^^^^^^^^^^^^^^^^^^^^
* > 62 | | ErrorResult<mixed, DebuggerSessionIDs>,
* | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unsupported feature: Translating "object types with spreads in the middle or at the end" is currently not supported.
**/
any
| /**
* > 72 | ...DebuggerSessionIDs,
* | ^^^^^^^^^^^^^^^^^^^^^ Unsupported feature: Translating "object types with spreads in the middle or at the end" is currently not supported.
**/
any
| /**
* > 89 | ...DebuggerSessionIDs,
* | ^^^^^^^^^^^^^^^^^^^^^ Unsupported feature: Translating "object types with spreads in the middle or at the end" is currently not supported.
**/
any
| { type: "fusebox_console_notice" }
| /**
* > 96 | ...DebuggerSessionIDs,
* | ^^^^^^^^^^^^^^^^^^^^^ Unsupported feature: Translating "object types with spreads in the middle or at the end" is currently not supported.
**/
any
| /**
* > 105 | ...ConnectionUptime,
* | ^^^^^^^^^^^^^^^^^^^ Unsupported feature: Translating "object types with spreads in the middle or at the end" is currently not supported.
**/
any
| /**
* > 112 | ...ConnectionUptime,
* | ^^^^^^^^^^^^^^^^^^^ Unsupported feature: Translating "object types with spreads in the middle or at the end" is currently not supported.
**/
any
| /**
* > 119 | ...ConnectionUptime,
* | ^^^^^^^^^^^^^^^^^^^ Unsupported feature: Translating "object types with spreads in the middle or at the end" is currently not supported.
**/
any
| /**
* > 127 | ...ConnectionUptime,
* | ^^^^^^^^^^^^^^^^^^^ Unsupported feature: Translating "object types with spreads in the middle or at the end" is currently not supported.
**/
any
| /**
* > 135 | ...ConnectionUptime,
* | ^^^^^^^^^^^^^^^^^^^ Unsupported feature: Translating "object types with spreads in the middle or at the end" is currently not supported.
**/
any
| {
type: "fusebox_shell_preparation_attempt";
result: DebuggerShellPreparationResult;
};
/**
* A simple interface for logging events, to be implemented by integrators of
* `dev-middleware`.
*
* This is an unstable API with no semver guarantees.
*/
export interface EventReporter {
logEvent(event: ReportableEvent): void;
}

View File

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

View File

@@ -0,0 +1,151 @@
/**
* 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 { DebuggerShellPreparationResult } from "./BrowserLauncher";
type SuccessResult<Props: { ... } | void = {}> = {
status: "success",
...Props,
};
type ErrorResult<ErrorT = mixed, Props: { ... } | void = {}> = {
status: "error",
error: ErrorT,
prefersFuseboxFrontend?: ?boolean,
...Props,
};
type CodedErrorResult<ErrorCode: string> = {
status: "coded_error",
errorCode: ErrorCode,
errorDetails?: string,
};
export type DebuggerSessionIDs = {
appId: string | null,
deviceName: string | null,
deviceId: string | null,
pageId: string | null,
};
export type ConnectionUptime = {
connectionUptime: number,
};
export type ReportableEvent =
| {
type: "launch_debugger_frontend",
launchType: "launch" | "redirect",
...
| SuccessResult<{
targetDescription: string,
prefersFuseboxFrontend: boolean,
...DebuggerSessionIDs,
}>
| ErrorResult<mixed>
| CodedErrorResult<"NO_APPS_FOUND">,
}
| {
type: "connect_debugger_frontend",
...
| SuccessResult<{
...DebuggerSessionIDs,
frontendUserAgent: string | null,
}>
| ErrorResult<mixed, DebuggerSessionIDs>,
}
| {
type: "debugger_command",
protocol: "CDP",
// With some errors, the method might not be known
method: string | null,
requestOrigin: "proxy" | "debugger" | null,
responseOrigin: "proxy" | "device",
timeSinceStart: number | null,
...DebuggerSessionIDs,
...ConnectionUptime,
frontendUserAgent: string | null,
prefersFuseboxFrontend: boolean | null,
...
| SuccessResult<void>
| CodedErrorResult<
| "TIMED_OUT"
| "DEVICE_DISCONNECTED"
| "DEBUGGER_DISCONNECTED"
| "UNMATCHED_REQUEST_ID"
| "PROTOCOL_ERROR",
>,
}
| {
type: "profiling_target_registered",
status: "success",
...DebuggerSessionIDs,
}
| {
type: "fusebox_console_notice",
}
| {
type: "no_debug_pages_for_device",
...DebuggerSessionIDs,
}
| {
type: "proxy_error",
status: "error",
messageOrigin: "debugger" | "device",
message: string,
error: string,
errorStack: string,
...ConnectionUptime,
...DebuggerSessionIDs,
}
| {
type: "debugger_high_ping" | "device_high_ping",
duration: number,
timeSinceLastCommunication: number | null,
...ConnectionUptime,
...DebuggerSessionIDs,
}
| {
type: "debugger_timeout" | "device_timeout",
duration: number,
timeSinceLastCommunication: number | null,
...ConnectionUptime,
...DebuggerSessionIDs,
}
| {
type: "debugger_connection_closed" | "device_connection_closed",
code: number,
reason: string,
timeSinceLastCommunication: number | null,
...ConnectionUptime,
...DebuggerSessionIDs,
}
| {
type: "high_event_loop_delay",
eventLoopUtilization: number,
maxEventLoopDelayPercent: number,
duration: number,
...ConnectionUptime,
...DebuggerSessionIDs,
}
| {
type: "fusebox_shell_preparation_attempt",
result: DebuggerShellPreparationResult,
};
/**
* A simple interface for logging events, to be implemented by integrators of
* `dev-middleware`.
*
* This is an unstable API with no semver guarantees.
*/
export interface EventReporter {
logEvent(event: ReportableEvent): void;
}

View File

@@ -0,0 +1,30 @@
/**
* 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 Experiments = Readonly<{
/**
* Enables the handling of GET requests in the /open-debugger endpoint,
* in addition to POST requests. GET requests respond by redirecting to
* the debugger frontend, instead of opening it using the BrowserLauncher
* interface.
*/
enableOpenDebuggerRedirect: boolean;
/**
* Enables the Network panel in the debugger frontend.
*/
enableNetworkInspector: boolean;
/**
* Launch the Fusebox frontend in a standalone shell instead of a browser.
* When this is enabled, we will use the optional unstable_showFuseboxShell
* method on the BrowserLauncher, or throw an error if the method is missing.
*/
enableStandaloneFuseboxShell: boolean;
}>;
export type ExperimentsConfig = Partial<Experiments>;

View File

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

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
*/
export type Experiments = $ReadOnly<{
/**
* Enables the handling of GET requests in the /open-debugger endpoint,
* in addition to POST requests. GET requests respond by redirecting to
* the debugger frontend, instead of opening it using the BrowserLauncher
* interface.
*/
enableOpenDebuggerRedirect: boolean,
/**
* Enables the Network panel in the debugger frontend.
*/
// NOTE: Used by Expo, exposing a tab labelled "Network (Expo)"
enableNetworkInspector: boolean,
/**
* Launch the Fusebox frontend in a standalone shell instead of a browser.
* When this is enabled, we will use the optional unstable_showFuseboxShell
* method on the BrowserLauncher, or throw an error if the method is missing.
*/
enableStandaloneFuseboxShell: boolean,
}>;
export type ExperimentsConfig = Partial<Experiments>;

View File

@@ -0,0 +1,15 @@
/**
* 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 Logger = Readonly<{
error: (...message: Array<string>) => void;
info: (...message: Array<string>) => void;
warn: (...message: Array<string>) => void;
}>;

View File

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

View File

@@ -0,0 +1,16 @@
/**
* 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
* @format
*/
export type Logger = $ReadOnly<{
error: (...message: Array<string>) => void,
info: (...message: Array<string>) => void,
warn: (...message: Array<string>) => void,
...
}>;

View File

@@ -0,0 +1,28 @@
/**
* 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 { DebuggerShellPreparationResult } from "../";
/**
* Default `BrowserLauncher` implementation which opens URLs on the host
* machine.
*/
declare const DefaultBrowserLauncher: {
/**
* Attempt to open the debugger frontend in a Google Chrome or Microsoft Edge
* app window.
*/
launchDebuggerAppWindow: (url: string) => Promise<void>;
unstable_showFuseboxShell(url: string, windowKey: string): Promise<void>;
unstable_prepareFuseboxShell(): Promise<DebuggerShellPreparationResult>;
};
declare const $$EXPORT_DEFAULT_DECLARATION$$: typeof DefaultBrowserLauncher;
declare type $$EXPORT_DEFAULT_DECLARATION$$ =
typeof $$EXPORT_DEFAULT_DECLARATION$$;
export default $$EXPORT_DEFAULT_DECLARATION$$;

View File

@@ -0,0 +1,62 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = void 0;
const {
unstable_prepareDebuggerShell,
unstable_spawnDebuggerShellWithArgs,
} = require("@react-native/debugger-shell");
const { spawn } = require("child_process");
const ChromeLauncher = require("chrome-launcher");
const { Launcher: EdgeLauncher } = require("chromium-edge-launcher");
const open = require("open");
const DefaultBrowserLauncher = {
launchDebuggerAppWindow: async (url) => {
let chromePath;
try {
chromePath = ChromeLauncher.getChromePath();
} catch (e) {
chromePath = EdgeLauncher.getFirstInstallation();
}
if (chromePath == null) {
await open(url);
return;
}
const chromeFlags = [`--app=${url}`, "--window-size=1200,600"];
return new Promise((resolve, reject) => {
const childProcess = spawn(chromePath, chromeFlags, {
detached: true,
stdio: "ignore",
});
childProcess.on("data", () => {
resolve();
});
childProcess.on("close", (code) => {
if (code !== 0) {
reject(
new Error(
`Failed to launch debugger app window: ${chromePath} exited with code ${code}`,
),
);
}
});
});
},
async unstable_showFuseboxShell(url, windowKey) {
return await unstable_spawnDebuggerShellWithArgs(
["--frontendUrl=" + url, "--windowKey=" + windowKey],
{
mode: "detached",
flavor: process.env.RNDT_DEV === "1" ? "dev" : "prebuilt",
},
);
},
async unstable_prepareFuseboxShell() {
return await unstable_prepareDebuggerShell(
process.env.RNDT_DEV === "1" ? "dev" : "prebuilt",
);
},
};
var _default = (exports.default = DefaultBrowserLauncher);

View File

@@ -0,0 +1,27 @@
/**
* 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 { DebuggerShellPreparationResult } from "../";
/**
* Default `BrowserLauncher` implementation which opens URLs on the host
* machine.
*/
declare const DefaultBrowserLauncher: {
/**
* Attempt to open the debugger frontend in a Google Chrome or Microsoft Edge
* app window.
*/
launchDebuggerAppWindow: (url: string) => Promise<void>,
unstable_showFuseboxShell(url: string, windowKey: string): Promise<void>,
unstable_prepareFuseboxShell(): Promise<DebuggerShellPreparationResult>,
};
declare export default typeof DefaultBrowserLauncher;

View File

@@ -0,0 +1,14 @@
/**
* 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
*/
declare function getBaseUrlFromRequest(
req: http$IncomingMessage<tls$TLSSocket> | http$IncomingMessage<net$Socket>,
): null | undefined | URL;
export default getBaseUrlFromRequest;

View File

@@ -0,0 +1,19 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = getBaseUrlFromRequest;
function getBaseUrlFromRequest(req) {
const hostHeader = req.headers.host;
if (hostHeader == null) {
return null;
}
const scheme = req.socket.encrypted === true ? "https" : "http";
const url = `${scheme}://${req.headers.host}`;
try {
return new URL(url);
} catch {
return null;
}
}

View File

@@ -0,0 +1,17 @@
/**
* 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
*/
// Determine the base URL (scheme and host) used by a client to reach this
// server.
//
// TODO: Support X-Forwarded-Host, etc. for trusted proxies
declare export default function getBaseUrlFromRequest(
req: http$IncomingMessage<tls$TLSSocket> | http$IncomingMessage<net$Socket>,
): ?URL;

View File

@@ -0,0 +1,29 @@
/**
* 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 { Experiments } from "../types/Experiments";
/**
* Get the DevTools frontend URL to debug a given React Native CDP target.
*/
declare function getDevToolsFrontendUrl(
experiments: Experiments,
webSocketDebuggerUrl: string,
devServerUrl: string,
options?: Readonly<{
relative?: boolean;
launchId?: string;
telemetryInfo?: string;
/** Whether to use the modern `rn_fusebox.html` entry point. */
useFuseboxEntryPoint?: boolean;
appId?: string;
panel?: string;
}>,
): string;
export default getDevToolsFrontendUrl;

View File

@@ -0,0 +1,58 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = getDevToolsFrontendUrl;
function getDevToolsFrontendUrl(
experiments,
webSocketDebuggerUrl,
devServerUrl,
options,
) {
const wsParam = getWsParam({
webSocketDebuggerUrl,
devServerUrl,
});
const appUrl =
(options?.relative === true ? "" : devServerUrl) +
"/debugger-frontend/" +
(options?.useFuseboxEntryPoint === true
? "rn_fusebox.html"
: "rn_inspector.html");
const searchParams = new URLSearchParams([
[wsParam.key, wsParam.value],
["sources.hide_add_folder", "true"],
]);
if (experiments.enableNetworkInspector) {
searchParams.append("unstable_enableNetworkPanel", "true");
}
if (options?.launchId != null && options.launchId !== "") {
searchParams.append("launchId", options.launchId);
}
if (options?.appId != null && options.appId !== "") {
searchParams.append("appId", options.appId);
}
if (options?.telemetryInfo != null && options.telemetryInfo !== "") {
searchParams.append("telemetryInfo", options.telemetryInfo);
}
if (options?.panel != null && options.panel !== "") {
searchParams.append("panel", options.panel);
}
return appUrl + "?" + searchParams.toString();
}
function getWsParam({ webSocketDebuggerUrl, devServerUrl }) {
const wsUrl = new URL(webSocketDebuggerUrl);
const serverHost = new URL(devServerUrl).host;
let value;
if (wsUrl.host === serverHost) {
value = wsUrl.pathname + wsUrl.search + wsUrl.hash;
} else {
value = wsUrl.host + wsUrl.pathname + wsUrl.search + wsUrl.hash;
}
const key = wsUrl.protocol.slice(0, -1);
return {
key,
value,
};
}

View File

@@ -0,0 +1,29 @@
/**
* 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 { Experiments } from "../types/Experiments";
/**
* Get the DevTools frontend URL to debug a given React Native CDP target.
*/
declare export default function getDevToolsFrontendUrl(
experiments: Experiments,
webSocketDebuggerUrl: string,
devServerUrl: string,
options?: $ReadOnly<{
relative?: boolean,
launchId?: string,
telemetryInfo?: string,
/** Whether to use the modern `rn_fusebox.html` entry point. */
useFuseboxEntryPoint?: boolean,
appId?: string,
panel?: string,
}>,
): string;