1import type { DebuggerInfo, Device as MetroDevice } from 'metro-inspector-proxy'; 2import fetch from 'node-fetch'; 3import type WS from 'ws'; 4 5import { MetroBundlerDevServer } from '../MetroBundlerDevServer'; 6import { DebuggerScriptSourceHandler } from './handlers/DebuggerScriptSource'; 7import { NetworkResponseHandler } from './handlers/NetworkResponse'; 8import { PageReloadHandler } from './handlers/PageReload'; 9import { VscodeDebuggerGetPossibleBreakpointsHandler } from './handlers/VscodeDebuggerGetPossibleBreakpoints'; 10import { VscodeDebuggerScriptParsedHandler } from './handlers/VscodeDebuggerScriptParsed'; 11import { VscodeDebuggerSetBreakpointByUrlHandler } from './handlers/VscodeDebuggerSetBreakpointByUrl'; 12import { VscodeRuntimeGetPropertiesHandler } from './handlers/VscodeRuntimeGetProperties'; 13import { DeviceRequest, InspectorHandler, DebuggerRequest } from './handlers/types'; 14 15/** Export the supported debugger types this inspector proxy can handle */ 16export type DebuggerType = 'vscode' | 'generic'; 17 18/** The debugger information being tracked by this device class */ 19export type ExpoDebuggerInfo = DebuggerInfo & { debuggerType?: DebuggerType }; 20 21export function createInspectorDeviceClass( 22 metroBundler: MetroBundlerDevServer, 23 MetroDeviceClass: typeof MetroDevice 24) { 25 return class ExpoInspectorDevice extends MetroDeviceClass implements InspectorHandler { 26 /** Stores information about currently connected debugger (if any). */ 27 _debuggerConnection: ExpoDebuggerInfo | null = null; 28 29 /** All handlers that should be used to intercept or reply to CDP events */ 30 public handlers: InspectorHandler[] = [ 31 // Generic handlers 32 new NetworkResponseHandler(), 33 new DebuggerScriptSourceHandler(this), 34 new PageReloadHandler(metroBundler), 35 // Vscode-specific handlers 36 new VscodeDebuggerGetPossibleBreakpointsHandler(), 37 new VscodeDebuggerScriptParsedHandler(this), 38 new VscodeDebuggerSetBreakpointByUrlHandler(), 39 new VscodeRuntimeGetPropertiesHandler(), 40 ]; 41 42 onDeviceMessage(message: any, info: DebuggerInfo): boolean { 43 return this.handlers.some((handler) => handler.onDeviceMessage?.(message, info) ?? false); 44 } 45 46 onDebuggerMessage(message: any, info: DebuggerInfo): boolean { 47 return this.handlers.some((handler) => handler.onDebuggerMessage?.(message, info) ?? false); 48 } 49 50 /** 51 * Handle a new device connection with the same device identifier. 52 * When the app and device name matches, we can reuse the debugger connection. 53 * Else, we have to shut the debugger connection down. 54 */ 55 handleDuplicateDeviceConnection(newDevice: InstanceType<typeof MetroDeviceClass>) { 56 if (this._app !== newDevice._app || this._name !== newDevice._name) { 57 this._deviceSocket.close(); 58 this._debuggerConnection?.socket.close(); 59 return; 60 } 61 62 const oldDebugger = this._debuggerConnection; 63 this._debuggerConnection = null; 64 65 if (oldDebugger) { 66 oldDebugger.socket.removeAllListeners(); 67 this._deviceSocket.close(); 68 newDevice.handleDebuggerConnection(oldDebugger.socket, oldDebugger.pageId); 69 } 70 } 71 72 /** 73 * Handle a new debugger connection to this device. 74 * This adds the `debuggerType` property to the `DebuggerInfo` object. 75 * With that information, we can enable or disable debugger-specific handlers. 76 */ 77 handleDebuggerConnectionWithType(socket: WS, pageId: string, debuggerType: DebuggerType): void { 78 this.handleDebuggerConnection(socket, pageId); 79 80 if (this._debuggerConnection) { 81 this._debuggerConnection.debuggerType = debuggerType; 82 } 83 } 84 85 /** Hook into the message life cycle to answer more complex CDP messages */ 86 async _processMessageFromDevice(message: DeviceRequest<any>, info: DebuggerInfo) { 87 if (!this.onDeviceMessage(message, info)) { 88 await super._processMessageFromDevice(message, info); 89 } 90 } 91 92 /** Hook into the message life cycle to answer more complex CDP messages */ 93 _interceptMessageFromDebugger( 94 request: DebuggerRequest, 95 info: DebuggerInfo, 96 socket: WS 97 ): boolean { 98 // Note, `socket` is the exact same as `info.socket` 99 if (this.onDebuggerMessage(request, info)) { 100 return true; 101 } 102 103 return super._interceptMessageFromDebugger(request, info, socket); 104 } 105 106 /** 107 * Overwrite the default text fetcher, to load sourcemaps from sources other than `localhost`. 108 * @todo Cedric: remove the custom `DebuggerScriptSource` handler when switching over to `metro@>=0.75.1` 109 * @see https://github.com/facebook/metro/blob/77f445f1bcd2264ad06174dbf8d542bc75834d29/packages/metro-inspector-proxy/src/Device.js#L573-L588 110 * @since [email protected] 111 */ 112 async _fetchText(url: URL): Promise<string> { 113 const LENGTH_LIMIT_BYTES = 350_000_000; // 350mb 114 115 const response = await fetch(url); 116 if (!response.ok) { 117 throw new Error(`Received status ${response.status} while fetching: ${url}`); 118 } 119 120 const contentLength = response.headers.get('Content-Length'); 121 if (contentLength && Number(contentLength) > LENGTH_LIMIT_BYTES) { 122 throw new Error('Expected file size is too large (more than 350mb)'); 123 } 124 125 const text = await response.text(); 126 if (Buffer.byteLength(text, 'utf8') > LENGTH_LIMIT_BYTES) { 127 throw new Error('File size is too large (more than 350mb)'); 128 } 129 130 return text; 131 } 132 }; 133} 134