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