Files
react-native/packages/dev-middleware/src/middleware/openDebuggerMiddleware.js
T
Moti Zilberman b5dfb32ed3 Support preparing debugger shell ahead of "open DevTools" (#53437)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53437

Changelog: [Internal]

The React Native DevTools standalone shell is distributed as a DotSlash file that downloads the required binaries lazily. This diff adds support in dev-middleware for a new `BrowserLauncher.unstable_prepareFuseboxShell` method that integrations can use to kick off the download early. Integrations are expected to implement this by calling the `unstable_prepareDebuggerShell` function (added to the `debugger-shell` package in D78413091).

If `BrowserLauncher.unstable_prepareFuseboxShell` returns an error, dev-middleware will fall back to the browser-based launch flow, even for users opted into the `enableStandaloneFuseboxShell` experiment.

Reviewed By: huntie

Differential Revision: D78413092

fbshipit-source-id: 6868bf07e16353fcd83337ae54c87c5a641a0f99
2025-08-27 02:50:05 -07:00

266 lines
8.3 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {InspectorProxyQueries} from '../inspector-proxy/InspectorProxy';
import type {PageDescription} from '../inspector-proxy/types';
import type {
BrowserLauncher,
DebuggerShellPreparationResult,
} from '../types/BrowserLauncher';
import type {EventReporter} from '../types/EventReporter';
import type {Experiments} from '../types/Experiments';
import type {Logger} from '../types/Logger';
import type {NextHandleFunction} from 'connect';
import type {IncomingMessage, ServerResponse} from 'http';
import getDevToolsFrontendUrl from '../utils/getDevToolsFrontendUrl';
import url from 'url';
const LEGACY_SYNTHETIC_PAGE_TITLE =
'React Native Experimental (Improved Chrome Reloads)';
type Options = $ReadOnly<{
serverBaseUrl: string,
logger?: Logger,
browserLauncher: BrowserLauncher,
eventReporter?: EventReporter,
experiments: Experiments,
inspectorProxy: InspectorProxyQueries,
}>;
/**
* Open the debugger frontend for a given CDP target.
*
* Currently supports React Native DevTools (rn_fusebox.html) and legacy Hermes
* (rn_inspector.html) targets.
*
* @see https://chromedevtools.github.io/devtools-protocol/
*/
export default function openDebuggerMiddleware({
serverBaseUrl,
logger,
browserLauncher,
eventReporter,
experiments,
inspectorProxy,
}: Options): NextHandleFunction {
let shellPreparationPromise: Promise<DebuggerShellPreparationResult>;
if (experiments.enableStandaloneFuseboxShell) {
shellPreparationPromise =
browserLauncher?.unstable_prepareFuseboxShell?.() ??
Promise.resolve({code: 'not_implemented'});
shellPreparationPromise = shellPreparationPromise.then(result => {
eventReporter?.logEvent({
type: 'fusebox_shell_preparation_attempt',
result,
});
return result;
});
}
return async (
req: IncomingMessage,
res: ServerResponse,
next: (err?: Error) => void,
) => {
if (
req.method === 'POST' ||
(experiments.enableOpenDebuggerRedirect && req.method === 'GET')
) {
const parsedUrl = url.parse(req.url, true);
const query: {
/** @deprecated Will only match legacy Hermes targets */
appId?: string,
/** @deprecated Will only match legacy Hermes targets */
device?: string,
launchId?: string,
telemetryInfo?: string,
target?: string,
panel?: string,
...
} = parsedUrl.query;
const targets = inspectorProxy
.getPageDescriptions({requestorRelativeBaseUrl: new URL(serverBaseUrl)})
.filter(app => {
const betterReloadingSupport =
app.title === LEGACY_SYNTHETIC_PAGE_TITLE ||
app.reactNative.capabilities?.nativePageReloads === true;
if (!betterReloadingSupport) {
logger?.warn(
"Ignoring DevTools app debug target for '%s' with title '%s' and 'nativePageReloads' capability set to '%s'. ",
app.appId,
app.title,
String(app.reactNative.capabilities?.nativePageReloads),
);
}
return betterReloadingSupport;
});
let target: PageDescription | void;
const launchType: 'launch' | 'redirect' =
req.method === 'POST' ? 'launch' : 'redirect';
if (
typeof query.target === 'string' ||
typeof query.appId === 'string' ||
typeof query.device === 'string'
) {
logger?.info(
(launchType === 'launch' ? 'Launching' : 'Redirecting to') +
' DevTools...',
);
target = targets.find(
_target =>
(query.target == null || _target.id === query.target) &&
(query.appId == null ||
(_target.appId === query.appId &&
_target.title === LEGACY_SYNTHETIC_PAGE_TITLE)) &&
(query.device == null ||
_target.reactNative.logicalDeviceId === query.device),
);
} else if (targets.length > 0) {
logger?.info(
(launchType === 'launch' ? 'Launching' : 'Redirecting to') +
` DevTools${targets.length === 1 ? '' : ' for most recently connected target'}...`,
);
target = targets[targets.length - 1];
}
if (!target) {
res.writeHead(404);
res.end('Unable to find debugger target');
logger?.warn(
'No compatible apps connected. React Native DevTools can only be used with the Hermes engine.',
);
eventReporter?.logEvent({
type: 'launch_debugger_frontend',
launchType,
status: 'coded_error',
errorCode: 'NO_APPS_FOUND',
});
return;
}
const useFuseboxEntryPoint =
target.reactNative.capabilities?.prefersFuseboxFrontend ?? false;
try {
switch (launchType) {
case 'launch': {
const frontendUrl = getDevToolsFrontendUrl(
experiments,
target.webSocketDebuggerUrl,
serverBaseUrl,
{
launchId: query.launchId,
telemetryInfo: query.telemetryInfo,
appId: target.appId,
useFuseboxEntryPoint,
panel: query.panel,
},
);
let shouldUseStandaloneFuseboxShell =
useFuseboxEntryPoint && experiments.enableStandaloneFuseboxShell;
if (shouldUseStandaloneFuseboxShell) {
const shellPreparationResult = await shellPreparationPromise;
switch (shellPreparationResult.code) {
case 'success':
case 'not_implemented':
break;
case 'platform_not_supported':
case 'possible_corruption':
case 'likely_offline':
case 'unexpected_error':
shouldUseStandaloneFuseboxShell = false;
break;
default:
(shellPreparationResult.code: empty);
}
}
if (shouldUseStandaloneFuseboxShell) {
const windowKey = [
serverBaseUrl,
target.webSocketDebuggerUrl,
target.appId,
].join('-');
if (!browserLauncher.unstable_showFuseboxShell) {
throw new Error(
'Fusebox shell is not supported by the current browser launcher',
);
}
await browserLauncher.unstable_showFuseboxShell(
frontendUrl,
windowKey,
);
} else {
await browserLauncher.launchDebuggerAppWindow(frontendUrl);
}
res.writeHead(200);
res.end();
break;
}
case 'redirect':
res.writeHead(302, {
Location: getDevToolsFrontendUrl(
experiments,
target.webSocketDebuggerUrl,
serverBaseUrl,
{
relative: true,
launchId: query.launchId,
telemetryInfo: query.telemetryInfo,
appId: target.appId,
useFuseboxEntryPoint,
},
),
});
res.end();
break;
default:
(launchType: empty);
}
eventReporter?.logEvent({
type: 'launch_debugger_frontend',
launchType,
status: 'success',
appId: target.appId,
deviceId: target.reactNative.logicalDeviceId,
pageId: target.id,
deviceName: target.deviceName,
targetDescription: target.description,
prefersFuseboxFrontend: useFuseboxEntryPoint,
});
return;
} catch (e) {
logger?.error(
'Error launching DevTools: ' + e.message ?? 'Unknown error',
);
res.writeHead(500);
res.end();
eventReporter?.logEvent({
type: 'launch_debugger_frontend',
launchType,
status: 'error',
error: e,
prefersFuseboxFrontend: useFuseboxEntryPoint,
});
return;
}
}
next();
};
}