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