1import type { DebuggerInfo, Device as MetroDevice } from 'metro-inspector-proxy';
2import fetch from 'node-fetch';
3import type WS from 'ws';
4
5import { DebuggerScriptSourceHandler } from './handlers/DebuggerScriptSource';
6import { NetworkResponseHandler } from './handlers/NetworkResponse';
7import { VscodeCompatHandler } from './handlers/VscodeCompat';
8import { DeviceRequest, InspectorHandler, DebuggerRequest } from './handlers/types';
9
10export function createInspectorDeviceClass(MetroDeviceClass: typeof MetroDevice) {
11  return class ExpoInspectorDevice extends MetroDeviceClass implements InspectorHandler {
12    /** All handlers that should be used to intercept or reply to CDP events */
13    public handlers: InspectorHandler[] = [
14      new NetworkResponseHandler(),
15      new DebuggerScriptSourceHandler(this),
16      new VscodeCompatHandler(),
17    ];
18
19    onDeviceMessage(message: any, info: DebuggerInfo): boolean {
20      return this.handlers.some((handler) => handler.onDeviceMessage?.(message, info) ?? false);
21    }
22
23    onDebuggerMessage(message: any, info: DebuggerInfo): boolean {
24      return this.handlers.some((handler) => handler.onDebuggerMessage?.(message, info) ?? false);
25    }
26
27    /** Hook into the message life cycle to answer more complex CDP messages */
28    async _processMessageFromDevice(message: DeviceRequest<any>, info: DebuggerInfo) {
29      if (!this.onDeviceMessage(message, info)) {
30        await super._processMessageFromDevice(message, info);
31      }
32    }
33
34    /** Hook into the message life cycle to answer more complex CDP messages */
35    _interceptMessageFromDebugger(
36      request: DebuggerRequest,
37      info: DebuggerInfo,
38      socket: WS
39    ): boolean {
40      // Note, `socket` is the exact same as `info.socket`
41      if (this.onDebuggerMessage(request, info)) {
42        return true;
43      }
44
45      return super._interceptMessageFromDebugger(request, info, socket);
46    }
47
48    /**
49     * Overwrite the default text fetcher, to load sourcemaps from sources other than `localhost`.
50     * @todo Cedric: remove the custom `DebuggerScriptSource` handler when switching over to `metro@>=0.75.1`
51     * @see https://github.com/facebook/metro/blob/77f445f1bcd2264ad06174dbf8d542bc75834d29/packages/metro-inspector-proxy/src/Device.js#L573-L588
52     * @since [email protected]
53     */
54    async _fetchText(url: URL): Promise<string> {
55      const LENGTH_LIMIT_BYTES = 350_000_000; // 350mb
56
57      const response = await fetch(url);
58      if (!response.ok) {
59        throw new Error(`Received status ${response.status} while fetching: ${url}`);
60      }
61
62      const contentLength = response.headers.get('Content-Length');
63      if (contentLength && Number(contentLength) > LENGTH_LIMIT_BYTES) {
64        throw new Error('Expected file size is too large (more than 350mb)');
65      }
66
67      const text = await response.text();
68      if (Buffer.byteLength(text, 'utf8') > LENGTH_LIMIT_BYTES) {
69        throw new Error('File size is too large (more than 350mb)');
70      }
71
72      return text;
73    }
74  };
75}
76