1import fetch from 'node-fetch';
2
3import { launchBrowserAsync, type LaunchBrowserInstance } from './LaunchBrowser';
4
5export interface MetroInspectorProxyApp {
6  description: string;
7  devtoolsFrontendUrl: string;
8  faviconUrl: string;
9  id: string;
10  title: string;
11  type: 'node';
12  vm: 'Hermes' | "don't use";
13  webSocketDebuggerUrl: string;
14}
15
16let openingBrowserInstance: LaunchBrowserInstance | null = null;
17
18export async function openJsInspector(app: MetroInspectorProxyApp) {
19  // To update devtoolsFrontendRev, find the full commit hash in the url:
20  // https://chromium.googlesource.com/chromium/src.git/+log/refs/tags/{CHROME_VERSION}/chrome/VERSION
21  //
22  // 1. Replace {CHROME_VERSION} with the target chrome version
23  // 2. Click the first log item in the webpage
24  // 3. The full commit hash is the desired revision
25  const devtoolsFrontendRev = 'd9568d04d7dd79269c5a655d7ada69650c5a8336'; // Chrome 100.0.4896.75
26
27  const urlBase = `https://chrome-devtools-frontend.appspot.com/serve_rev/@${devtoolsFrontendRev}/devtools_app.html`;
28  const ws = app.webSocketDebuggerUrl.replace(/^ws:\/\//, '');
29  const url = `${urlBase}?panel=console&ws=${encodeURIComponent(ws)}`;
30  await closeJsInspector();
31  openingBrowserInstance = await launchBrowserAsync(url);
32}
33
34export async function closeJsInspector() {
35  await openingBrowserInstance?.close();
36  openingBrowserInstance = null;
37}
38
39export async function queryInspectorAppAsync(
40  metroServerOrigin: string,
41  appId: string
42): Promise<MetroInspectorProxyApp | null> {
43  const apps = await queryAllInspectorAppsAsync(metroServerOrigin);
44  return apps.find((app) => app.description === appId) ?? null;
45}
46
47export async function queryAllInspectorAppsAsync(
48  metroServerOrigin: string
49): Promise<MetroInspectorProxyApp[]> {
50  const resp = await fetch(`${metroServerOrigin}/json/list`);
51  const apps: MetroInspectorProxyApp[] = transformApps(await resp.json());
52  // Only use targets with better reloading support
53  return apps.filter((app) => app.title === 'React Native Experimental (Improved Chrome Reloads)');
54}
55
56// The description of `React Native Experimental (Improved Chrome Reloads)` target is `don't use` from metro.
57// This function tries to transform the unmeaningful description to appId
58function transformApps(apps: MetroInspectorProxyApp[]): MetroInspectorProxyApp[] {
59  const deviceIdToAppId: Record<string, string> = {};
60
61  for (const app of apps) {
62    if (app.description !== "don't use") {
63      const deviceId = app.id.split('-')[0];
64      const appId = app.description;
65      deviceIdToAppId[deviceId] = appId;
66    }
67  }
68
69  return apps.map((app) => {
70    if (app.description === "don't use") {
71      const deviceId = app.id.split('-')[0];
72      app.description = deviceIdToAppId[deviceId] ?? app.description;
73    }
74    return app;
75  });
76}
77