import type { DebuggerInfo, Device as MetroDevice } from 'metro-inspector-proxy'; import fetch from 'node-fetch'; import type WS from 'ws'; import { NetworkResponseHandler } from './handlers/NetworkResponse'; import { PageReloadHandler } from './handlers/PageReload'; import { VscodeDebuggerGetPossibleBreakpointsHandler } from './handlers/VscodeDebuggerGetPossibleBreakpoints'; import { VscodeDebuggerScriptParsedHandler } from './handlers/VscodeDebuggerScriptParsed'; import { VscodeDebuggerSetBreakpointByUrlHandler } from './handlers/VscodeDebuggerSetBreakpointByUrl'; import { VscodeRuntimeGetPropertiesHandler } from './handlers/VscodeRuntimeGetProperties'; import { DeviceRequest, InspectorHandler, DebuggerRequest } from './handlers/types'; import { MetroBundlerDevServer } from '../MetroBundlerDevServer'; /** Export the supported debugger types this inspector proxy can handle */ export type DebuggerType = 'vscode' | 'generic'; /** The debugger information being tracked by this device class */ export type ExpoDebuggerInfo = DebuggerInfo & { debuggerType?: DebuggerType }; export function createInspectorDeviceClass( metroBundler: MetroBundlerDevServer, MetroDeviceClass: typeof MetroDevice ) { return class ExpoInspectorDevice extends MetroDeviceClass implements InspectorHandler { /** Stores information about currently connected debugger (if any). */ _debuggerConnection: ExpoDebuggerInfo | null = null; /** All handlers that should be used to intercept or reply to CDP events */ public handlers: InspectorHandler[] = [ // Generic handlers new NetworkResponseHandler(), new PageReloadHandler(metroBundler), // Vscode-specific handlers new VscodeDebuggerGetPossibleBreakpointsHandler(), new VscodeDebuggerScriptParsedHandler(this), new VscodeDebuggerSetBreakpointByUrlHandler(), new VscodeRuntimeGetPropertiesHandler(), ]; onDeviceMessage(message: any, info: DebuggerInfo): boolean { return this.handlers.some((handler) => handler.onDeviceMessage?.(message, info) ?? false); } onDebuggerMessage(message: any, info: DebuggerInfo): boolean { return this.handlers.some((handler) => handler.onDebuggerMessage?.(message, info) ?? false); } /** * Handle a new device connection with the same device identifier. * When the app and device name matches, we can reuse the debugger connection. * Else, we have to shut the debugger connection down. */ handleDuplicateDeviceConnection(newDevice: InstanceType) { if (this._app !== newDevice._app || this._name !== newDevice._name) { this._deviceSocket.close(); this._debuggerConnection?.socket.close(); return; } const oldDebugger = this._debuggerConnection; this._debuggerConnection = null; if (oldDebugger) { oldDebugger.socket.removeAllListeners(); this._deviceSocket.close(); newDevice.handleDebuggerConnection(oldDebugger.socket, oldDebugger.pageId); } } /** * Handle a new debugger connection to this device. * This adds the `debuggerType` property to the `DebuggerInfo` object. * With that information, we can enable or disable debugger-specific handlers. */ handleDebuggerConnectionWithType(socket: WS, pageId: string, debuggerType: DebuggerType): void { this.handleDebuggerConnection(socket, pageId); if (this._debuggerConnection) { this._debuggerConnection.debuggerType = debuggerType; } } /** Hook into the message life cycle to answer more complex CDP messages */ async _processMessageFromDevice(message: DeviceRequest, info: DebuggerInfo) { if (!this.onDeviceMessage(message, info)) { await super._processMessageFromDevice(message, info); } } /** Hook into the message life cycle to answer more complex CDP messages */ _interceptMessageFromDebugger( request: DebuggerRequest, info: DebuggerInfo, socket: WS ): boolean { // Note, `socket` is the exact same as `info.socket` if (this.onDebuggerMessage(request, info)) { return true; } return super._interceptMessageFromDebugger(request, info, socket); } /** * Overwrite the default text fetcher, to load sourcemaps from sources other than `localhost`. * @todo Cedric: remove the custom `DebuggerScriptSource` handler when switching over to `metro@>=0.75.1` * @see https://github.com/facebook/metro/blob/77f445f1bcd2264ad06174dbf8d542bc75834d29/packages/metro-inspector-proxy/src/Device.js#L573-L588 * @since metro-inspector-proxy@0.75.1 */ async _fetchText(url: URL): Promise { const LENGTH_LIMIT_BYTES = 350_000_000; // 350mb const response = await fetch(url); if (!response.ok) { throw new Error(`Received status ${response.status} while fetching: ${url}`); } const contentLength = response.headers.get('Content-Length'); if (contentLength && Number(contentLength) > LENGTH_LIMIT_BYTES) { throw new Error('Expected file size is too large (more than 350mb)'); } const text = await response.text(); if (Buffer.byteLength(text, 'utf8') > LENGTH_LIMIT_BYTES) { throw new Error('File size is too large (more than 350mb)'); } return text; } }; }