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 { VscodeCompatHandler } from './handlers/VscodeCompat'; 10import { DeviceRequest, InspectorHandler, DebuggerRequest } from './handlers/types'; 11 12export function createInspectorDeviceClass( 13 metroBundler: MetroBundlerDevServer, 14 MetroDeviceClass: typeof MetroDevice 15) { 16 return class ExpoInspectorDevice extends MetroDeviceClass implements InspectorHandler { 17 /** All handlers that should be used to intercept or reply to CDP events */ 18 public handlers: InspectorHandler[] = [ 19 new NetworkResponseHandler(), 20 new DebuggerScriptSourceHandler(this), 21 new PageReloadHandler(metroBundler), 22 new VscodeCompatHandler(), 23 ]; 24 25 onDeviceMessage(message: any, info: DebuggerInfo): boolean { 26 return this.handlers.some((handler) => handler.onDeviceMessage?.(message, info) ?? false); 27 } 28 29 onDebuggerMessage(message: any, info: DebuggerInfo): boolean { 30 return this.handlers.some((handler) => handler.onDebuggerMessage?.(message, info) ?? false); 31 } 32 33 /** 34 * Handle a new device connection with the same device identifier. 35 * When the app and device name matches, we can reuse the debugger connection. 36 * Else, we have to shut the debugger connection down. 37 */ 38 handleDuplicateDeviceConnection(newDevice: InstanceType<typeof MetroDeviceClass>) { 39 if (this._app !== newDevice._app || this._name !== newDevice._name) { 40 this._deviceSocket.close(); 41 this._debuggerConnection?.socket.close(); 42 return; 43 } 44 45 const oldDebugger = this._debuggerConnection; 46 this._debuggerConnection = null; 47 48 if (oldDebugger) { 49 oldDebugger.socket.removeAllListeners(); 50 this._deviceSocket.close(); 51 newDevice.handleDebuggerConnection(oldDebugger.socket, oldDebugger.pageId); 52 } 53 } 54 55 /** Hook into the message life cycle to answer more complex CDP messages */ 56 async _processMessageFromDevice(message: DeviceRequest<any>, info: DebuggerInfo) { 57 if (!this.onDeviceMessage(message, info)) { 58 await super._processMessageFromDevice(message, info); 59 } 60 } 61 62 /** Hook into the message life cycle to answer more complex CDP messages */ 63 _interceptMessageFromDebugger( 64 request: DebuggerRequest, 65 info: DebuggerInfo, 66 socket: WS 67 ): boolean { 68 // Note, `socket` is the exact same as `info.socket` 69 if (this.onDebuggerMessage(request, info)) { 70 return true; 71 } 72 73 return super._interceptMessageFromDebugger(request, info, socket); 74 } 75 76 /** 77 * Overwrite the default text fetcher, to load sourcemaps from sources other than `localhost`. 78 * @todo Cedric: remove the custom `DebuggerScriptSource` handler when switching over to `metro@>=0.75.1` 79 * @see https://github.com/facebook/metro/blob/77f445f1bcd2264ad06174dbf8d542bc75834d29/packages/metro-inspector-proxy/src/Device.js#L573-L588 80 * @since [email protected] 81 */ 82 async _fetchText(url: URL): Promise<string> { 83 const LENGTH_LIMIT_BYTES = 350_000_000; // 350mb 84 85 const response = await fetch(url); 86 if (!response.ok) { 87 throw new Error(`Received status ${response.status} while fetching: ${url}`); 88 } 89 90 const contentLength = response.headers.get('Content-Length'); 91 if (contentLength && Number(contentLength) > LENGTH_LIMIT_BYTES) { 92 throw new Error('Expected file size is too large (more than 350mb)'); 93 } 94 95 const text = await response.text(); 96 if (Buffer.byteLength(text, 'utf8') > LENGTH_LIMIT_BYTES) { 97 throw new Error('File size is too large (more than 350mb)'); 98 } 99 100 return text; 101 } 102 }; 103} 104