mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
b5dfb32ed3
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
266 lines
8.3 KiB
JavaScript
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();
|
|
};
|
|
}
|