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

103
node_modules/@react-native/dev-middleware/README.md generated vendored Normal file
View File

@@ -0,0 +1,103 @@
# @react-native/dev-middleware
![npm package](https://img.shields.io/npm/v/@react-native/dev-middleware?color=brightgreen&label=npm%20package)
Dev server middleware supporting core React Native development features. This package is preconfigured in all React Native projects.
## Usage
Middleware can be attached to a dev server (e.g. [Metro](https://facebook.github.io/metro/docs/getting-started)) using the `createDevMiddleware` API.
```js
import { createDevMiddleware } from '@react-native/dev-middleware';
function myDevServerImpl(args) {
...
const {middleware, websocketEndpoints} = createDevMiddleware({
projectRoot: metroConfig.projectRoot,
serverBaseUrl: `http://${args.host}:${args.port}`,
logger,
});
await Metro.runServer(metroConfig, {
host: args.host,
...,
unstable_extraMiddleware: [
middleware,
// Optionally extend with additional HTTP middleware
],
websocketEndpoints: {
...websocketEndpoints,
// Optionally extend with additional WebSocket endpoints
},
});
}
```
## Included middleware
`@react-native/dev-middleware` is designed for integrators such as [`@expo/dev-server`](https://www.npmjs.com/package/@expo/dev-server) and [`@react-native/community-cli-plugin`](https://github.com/facebook/react-native/tree/main/packages/community-cli-plugin). It provides a common default implementation for core React Native dev server responsibilities.
We intend to keep this to a narrow set of functionality, based around:
- **Debugging** — The [Chrome DevTools protocol (CDP)](https://chromedevtools.github.io/devtools-protocol/) endpoints supported by React Native, including the Inspector Proxy, which facilitates connections with multiple devices.
- **Dev actions** — Endpoints implementing core [Dev Menu](https://reactnative.dev/docs/debugging#accessing-the-dev-menu) actions, e.g. reloading the app, opening the debugger frontend.
### HTTP endpoints
<small>`DevMiddlewareAPI.middleware`</small>
These are exposed as a [`connect`](https://www.npmjs.com/package/connect) middleware handler, assignable to `Metro.runServer` or other compatible HTTP servers.
#### GET `/json/list`, `/json` ([CDP](https://chromedevtools.github.io/devtools-protocol/#endpoints))
Returns the list of available WebSocket targets for all connected React Native app sessions.
#### GET `/json/version` ([CDP](https://chromedevtools.github.io/devtools-protocol/#endpoints))
Returns version metadata used by Chrome DevTools.
#### GET `/debugger-frontend`
Subpaths of this endpoint are reserved to serve the JavaScript debugger frontend.
#### POST `/open-debugger`
Open the JavaScript debugger for a given CDP target. Must be provided with one of the following query params:
- `device` — An ID unique to a combination of device and app, stable across installs. Implemented by `getInspectorDeviceId` on each native platform.
- `target` The target page ID as returned by `/json/list` for the current dev server session.
- `appId` (deprecated, legacy only) — The application bundle identifier to match (non-unique across multiple connected devices). This param will only match legacy Hermes debugger targets.
<details>
<summary>Example</summary>
curl -X POST 'http://localhost:8081/open-debugger?target=<targetId>'
</details>
### WebSocket endpoints
<small>`DevMiddlewareAPI.websocketEndpoints`</small>
#### `/inspector/device`
WebSocket handler for registering device connections.
#### `/inspector/debug`
WebSocket handler that proxies CDP messages to/from the corresponding device.
## Experimental features
React Native frameworks may pass an `unstable_experiments` option to `createDevMiddleware` to configure experimental features. Note that these features might not work correctly, and they may change or be removed in the future without notice. Some of the experiment flags available are documented below.
### `unstable_experiments.enableStandaloneFuseboxShell`
Enables launching the debugger frontend in a standalone app shell (provided by the `@react-native/debugger-shell` package) rather than in a browser window. Since React Native 0.83 this defaults to `true`, and may be disabled by explicitly passing `false`.
The shell is powered by a separate binary that is downloaded and cached in the background (immediately after the call to `createDevMiddleware`). If there is a problem downloading or invoking this binary for the first time, the debugger frontend will revert to launching in a browser window until the next time `createDevMiddleware` is called (typically, on the next dev server start).
## Contributing
Changes to this package can be made locally and tested against the `rn-tester` app, per the [Contributing guide](https://reactnative.dev/contributing/overview#contributing-code). During development, this package is automatically run from source with no build step.

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;

47
node_modules/@react-native/dev-middleware/package.json generated vendored Normal file
View File

@@ -0,0 +1,47 @@
{
"name": "@react-native/dev-middleware",
"version": "0.83.2",
"description": "Dev server middleware for React Native",
"keywords": [
"react-native",
"tools"
],
"homepage": "https://github.com/facebook/react-native/tree/HEAD/packages/dev-middleware#readme",
"bugs": "https://github.com/facebook/react-native/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/facebook/react-native.git",
"directory": "packages/dev-middleware"
},
"license": "MIT",
"exports": {
".": "./dist/index.js",
"./package.json": "./package.json"
},
"files": [
"dist"
],
"dependencies": {
"@isaacs/ttlcache": "^1.4.1",
"@react-native/debugger-frontend": "0.83.2",
"@react-native/debugger-shell": "0.83.2",
"chrome-launcher": "^0.15.2",
"chromium-edge-launcher": "^0.2.0",
"connect": "^3.6.5",
"debug": "^4.4.0",
"invariant": "^2.2.4",
"nullthrows": "^1.1.1",
"open": "^7.0.3",
"serve-static": "^1.16.2",
"ws": "^7.5.10"
},
"engines": {
"node": ">= 20.19.4"
},
"devDependencies": {
"@react-native/debugger-shell": "0.83.2",
"selfsigned": "^2.4.1",
"undici": "^5.29.0",
"wait-for-expect": "^3.0.2"
}
}