15234fe38SCedric van Puttenimport type { DebuggerInfo, Device as MetroDevice } from 'metro-inspector-proxy';
26d2ad561SCedric van Puttenimport fetch from 'node-fetch';
35234fe38SCedric van Puttenimport type WS from 'ws';
45234fe38SCedric van Putten
55234fe38SCedric van Puttenimport { NetworkResponseHandler } from './handlers/NetworkResponse';
603d43e7dSCedric van Puttenimport { PageReloadHandler } from './handlers/PageReload';
7033ea1fcSCedric van Puttenimport { VscodeDebuggerGetPossibleBreakpointsHandler } from './handlers/VscodeDebuggerGetPossibleBreakpoints';
8033ea1fcSCedric van Puttenimport { VscodeDebuggerScriptParsedHandler } from './handlers/VscodeDebuggerScriptParsed';
9033ea1fcSCedric van Puttenimport { VscodeDebuggerSetBreakpointByUrlHandler } from './handlers/VscodeDebuggerSetBreakpointByUrl';
10033ea1fcSCedric van Puttenimport { VscodeRuntimeGetPropertiesHandler } from './handlers/VscodeRuntimeGetProperties';
115234fe38SCedric van Puttenimport { DeviceRequest, InspectorHandler, DebuggerRequest } from './handlers/types';
12*8a424bebSJames Ideimport { MetroBundlerDevServer } from '../MetroBundlerDevServer';
135234fe38SCedric van Putten
14033ea1fcSCedric van Putten/** Export the supported debugger types this inspector proxy can handle */
15033ea1fcSCedric van Puttenexport type DebuggerType = 'vscode' | 'generic';
16033ea1fcSCedric van Putten
17033ea1fcSCedric van Putten/** The debugger information being tracked by this device class */
18033ea1fcSCedric van Puttenexport type ExpoDebuggerInfo = DebuggerInfo & { debuggerType?: DebuggerType };
19033ea1fcSCedric van Putten
2003d43e7dSCedric van Puttenexport function createInspectorDeviceClass(
2103d43e7dSCedric van Putten  metroBundler: MetroBundlerDevServer,
2203d43e7dSCedric van Putten  MetroDeviceClass: typeof MetroDevice
2303d43e7dSCedric van Putten) {
245234fe38SCedric van Putten  return class ExpoInspectorDevice extends MetroDeviceClass implements InspectorHandler {
25033ea1fcSCedric van Putten    /** Stores information about currently connected debugger (if any). */
26033ea1fcSCedric van Putten    _debuggerConnection: ExpoDebuggerInfo | null = null;
27033ea1fcSCedric van Putten
285234fe38SCedric van Putten    /** All handlers that should be used to intercept or reply to CDP events */
296d2ad561SCedric van Putten    public handlers: InspectorHandler[] = [
30033ea1fcSCedric van Putten      // Generic handlers
316d2ad561SCedric van Putten      new NetworkResponseHandler(),
3203d43e7dSCedric van Putten      new PageReloadHandler(metroBundler),
33033ea1fcSCedric van Putten      // Vscode-specific handlers
34033ea1fcSCedric van Putten      new VscodeDebuggerGetPossibleBreakpointsHandler(),
35033ea1fcSCedric van Putten      new VscodeDebuggerScriptParsedHandler(this),
36033ea1fcSCedric van Putten      new VscodeDebuggerSetBreakpointByUrlHandler(),
37033ea1fcSCedric van Putten      new VscodeRuntimeGetPropertiesHandler(),
386d2ad561SCedric van Putten    ];
395234fe38SCedric van Putten
405234fe38SCedric van Putten    onDeviceMessage(message: any, info: DebuggerInfo): boolean {
415234fe38SCedric van Putten      return this.handlers.some((handler) => handler.onDeviceMessage?.(message, info) ?? false);
425234fe38SCedric van Putten    }
435234fe38SCedric van Putten
445234fe38SCedric van Putten    onDebuggerMessage(message: any, info: DebuggerInfo): boolean {
455234fe38SCedric van Putten      return this.handlers.some((handler) => handler.onDebuggerMessage?.(message, info) ?? false);
465234fe38SCedric van Putten    }
475234fe38SCedric van Putten
48f5fa30f6SCedric van Putten    /**
49f5fa30f6SCedric van Putten     * Handle a new device connection with the same device identifier.
50f5fa30f6SCedric van Putten     * When the app and device name matches, we can reuse the debugger connection.
51f5fa30f6SCedric van Putten     * Else, we have to shut the debugger connection down.
52f5fa30f6SCedric van Putten     */
53f5fa30f6SCedric van Putten    handleDuplicateDeviceConnection(newDevice: InstanceType<typeof MetroDeviceClass>) {
54f5fa30f6SCedric van Putten      if (this._app !== newDevice._app || this._name !== newDevice._name) {
55f5fa30f6SCedric van Putten        this._deviceSocket.close();
56f5fa30f6SCedric van Putten        this._debuggerConnection?.socket.close();
57f5fa30f6SCedric van Putten        return;
58f5fa30f6SCedric van Putten      }
59f5fa30f6SCedric van Putten
60f5fa30f6SCedric van Putten      const oldDebugger = this._debuggerConnection;
61f5fa30f6SCedric van Putten      this._debuggerConnection = null;
62f5fa30f6SCedric van Putten
63f5fa30f6SCedric van Putten      if (oldDebugger) {
64f5fa30f6SCedric van Putten        oldDebugger.socket.removeAllListeners();
65f5fa30f6SCedric van Putten        this._deviceSocket.close();
66f5fa30f6SCedric van Putten        newDevice.handleDebuggerConnection(oldDebugger.socket, oldDebugger.pageId);
67f5fa30f6SCedric van Putten      }
68f5fa30f6SCedric van Putten    }
69f5fa30f6SCedric van Putten
70033ea1fcSCedric van Putten    /**
71033ea1fcSCedric van Putten     * Handle a new debugger connection to this device.
72033ea1fcSCedric van Putten     * This adds the `debuggerType` property to the `DebuggerInfo` object.
73033ea1fcSCedric van Putten     * With that information, we can enable or disable debugger-specific handlers.
74033ea1fcSCedric van Putten     */
75033ea1fcSCedric van Putten    handleDebuggerConnectionWithType(socket: WS, pageId: string, debuggerType: DebuggerType): void {
76033ea1fcSCedric van Putten      this.handleDebuggerConnection(socket, pageId);
77033ea1fcSCedric van Putten
78033ea1fcSCedric van Putten      if (this._debuggerConnection) {
79033ea1fcSCedric van Putten        this._debuggerConnection.debuggerType = debuggerType;
80033ea1fcSCedric van Putten      }
81033ea1fcSCedric van Putten    }
82033ea1fcSCedric van Putten
835234fe38SCedric van Putten    /** Hook into the message life cycle to answer more complex CDP messages */
845234fe38SCedric van Putten    async _processMessageFromDevice(message: DeviceRequest<any>, info: DebuggerInfo) {
855234fe38SCedric van Putten      if (!this.onDeviceMessage(message, info)) {
865234fe38SCedric van Putten        await super._processMessageFromDevice(message, info);
875234fe38SCedric van Putten      }
885234fe38SCedric van Putten    }
895234fe38SCedric van Putten
905234fe38SCedric van Putten    /** Hook into the message life cycle to answer more complex CDP messages */
915234fe38SCedric van Putten    _interceptMessageFromDebugger(
925234fe38SCedric van Putten      request: DebuggerRequest,
935234fe38SCedric van Putten      info: DebuggerInfo,
945234fe38SCedric van Putten      socket: WS
955234fe38SCedric van Putten    ): boolean {
965234fe38SCedric van Putten      // Note, `socket` is the exact same as `info.socket`
975234fe38SCedric van Putten      if (this.onDebuggerMessage(request, info)) {
985234fe38SCedric van Putten        return true;
995234fe38SCedric van Putten      }
1005234fe38SCedric van Putten
1015234fe38SCedric van Putten      return super._interceptMessageFromDebugger(request, info, socket);
1025234fe38SCedric van Putten    }
1036d2ad561SCedric van Putten
1046d2ad561SCedric van Putten    /**
1056d2ad561SCedric van Putten     * Overwrite the default text fetcher, to load sourcemaps from sources other than `localhost`.
1066d2ad561SCedric van Putten     * @todo Cedric: remove the custom `DebuggerScriptSource` handler when switching over to `metro@>=0.75.1`
1076d2ad561SCedric van Putten     * @see https://github.com/facebook/metro/blob/77f445f1bcd2264ad06174dbf8d542bc75834d29/packages/metro-inspector-proxy/src/Device.js#L573-L588
1086d2ad561SCedric van Putten     * @since [email protected]
1096d2ad561SCedric van Putten     */
1106d2ad561SCedric van Putten    async _fetchText(url: URL): Promise<string> {
1116d2ad561SCedric van Putten      const LENGTH_LIMIT_BYTES = 350_000_000; // 350mb
1126d2ad561SCedric van Putten
1136d2ad561SCedric van Putten      const response = await fetch(url);
1146d2ad561SCedric van Putten      if (!response.ok) {
1156d2ad561SCedric van Putten        throw new Error(`Received status ${response.status} while fetching: ${url}`);
1166d2ad561SCedric van Putten      }
1176d2ad561SCedric van Putten
1186d2ad561SCedric van Putten      const contentLength = response.headers.get('Content-Length');
1196d2ad561SCedric van Putten      if (contentLength && Number(contentLength) > LENGTH_LIMIT_BYTES) {
1206d2ad561SCedric van Putten        throw new Error('Expected file size is too large (more than 350mb)');
1216d2ad561SCedric van Putten      }
1226d2ad561SCedric van Putten
1236d2ad561SCedric van Putten      const text = await response.text();
1246d2ad561SCedric van Putten      if (Buffer.byteLength(text, 'utf8') > LENGTH_LIMIT_BYTES) {
1256d2ad561SCedric van Putten        throw new Error('File size is too large (more than 350mb)');
1266d2ad561SCedric van Putten      }
1276d2ad561SCedric van Putten
1286d2ad561SCedric van Putten      return text;
1296d2ad561SCedric van Putten    }
1305234fe38SCedric van Putten  };
1315234fe38SCedric van Putten}
132