1import Protocol from 'devtools-protocol';
2import { Device as MetroDevice } from 'metro-inspector-proxy';
3
4import { CdpMessage, DeviceRequest, InspectorHandler } from './types';
5import { ExpoDebuggerInfo } from '../device';
6
7/** Android's stock emulator and other emulators such as genymotion use a standard localhost alias. */
8const EMULATOR_LOCALHOST_ADDRESSES: Readonly<string[]> = ['10.0.2.2', '10.0.3.2'];
9/** Prefix for script URLs that are alphanumeric IDs. */
10const FILE_PREFIX = 'file://';
11
12/**
13 * Some debug clients does not support fetching source maps by URL.
14 * By default, the `metro-inspector-proxy` inlines the source map as base64 string.
15 * Unfortunately, that causes a multi-second delay in VS Code (±5s).
16 * This handler disables inlining the source maps for VS Code only.
17 */
18export class VscodeDebuggerScriptParsedHandler implements InspectorHandler {
19  constructor(private readonly device: MetroDevice) {}
20
21  onDeviceMessage(message: DeviceRequest<DebuggerScriptParsed>, debuggerInfo: ExpoDebuggerInfo) {
22    if (debuggerInfo.debuggerType !== 'vscode' || message.method !== 'Debugger.scriptParsed') {
23      return false;
24    }
25
26    // See: https://github.com/facebook/metro/blob/f43caa371a813b257cb0b42028079645a1e85e0e/packages/metro-inspector-proxy/src/Device.js#L401-L410
27    if (message.params.sourceMapURL) {
28      for (let i = 0; i < EMULATOR_LOCALHOST_ADDRESSES.length; ++i) {
29        const address = EMULATOR_LOCALHOST_ADDRESSES[i];
30        if (message.params.sourceMapURL.indexOf(address) >= 0) {
31          message.params.sourceMapURL = message.params.sourceMapURL.replace(address, 'localhost');
32          debuggerInfo.originalSourceURLAddress = address;
33        }
34      }
35    }
36
37    // See: https://github.com/facebook/metro/blob/f43caa371a813b257cb0b42028079645a1e85e0e/packages/metro-inspector-proxy/src/Device.js#L431-L453
38    if (message.params.url) {
39      for (let i = 0; i < EMULATOR_LOCALHOST_ADDRESSES.length; ++i) {
40        const address = EMULATOR_LOCALHOST_ADDRESSES[i];
41        if (message.params.url.indexOf(address) >= 0) {
42          message.params.url = message.params.url.replace(address, 'localhost');
43          debuggerInfo.originalSourceURLAddress = address;
44        }
45      }
46
47      // Chrome doesn't download source maps if URL param is not a valid
48      // URL. Some frameworks pass alphanumeric script ID instead of URL which causes
49      // Chrome to not download source maps. In this case we want to prepend script ID
50      // with 'file://' prefix.
51      if (message.params.url.match(/^[0-9a-z]+$/)) {
52        message.params.url = FILE_PREFIX + message.params.url;
53        debuggerInfo.prependedFilePrefix = true;
54      }
55
56      if (message.params.scriptId != null) {
57        this.device._scriptIdToSourcePathMapping.set(message.params.scriptId, message.params.url);
58      }
59    }
60
61    // Block `metro-inspector-proxy`'s default source map inlining
62    return true;
63  }
64}
65
66/** @see https://chromedevtools.github.io/devtools-protocol/v8/Debugger/#event-scriptParsed */
67export type DebuggerScriptParsed = CdpMessage<
68  'Debugger.scriptParsed',
69  Protocol.Debugger.ScriptParsedEvent,
70  never
71>;
72