Files
react-native/packages/dev-middleware/src/middleware/openDebuggerMiddleware.js
T
Rob Hogan acf384a72e dev-middleware: Redefine "serverBaseUrl" as server-relative, '/json/list' by requestor (#47628)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47628

`serverBaseUrl` is currently documented as:

> The base URL to the dev server, as addressible from the local developer machine

This is problematic in general because `dev-middleware` on a server doesn't necessarily know about where clients might be reaching it from, how tunnels or port-forwards are set up, etc., and this can change over the lifetime of the server and vary between clients.

Indeed, our own use of `serverBaseUrl` from both `community-cli-plugin` and internally simply sets it to the host and port the dev server is listening on - ie it's the address of the dev server accessible *from the server*.

This PR changes the docs, redefining `serverBaseUrl`, to match the way we currently specify it.

One usage where we *do* want the previously documented behaviour is in responses to `/json/list` (`getPageDescriptions`) where the URLs in the response should be reachable by a browser requesting `/json/list`.

Here, we use the request (host header, etc.) to attempt to get working base URL.

History:
It should be mentioned that this is the latest in a series of changes like this:
 - https://github.com/facebook/react-native/pull/39394
 - https://github.com/facebook/react-native/pull/39456

Learning from those:
 - This change does *not* break Android emulators, which routes `10.0.2.2` to localhost, or other routed devices, because `/open-debugger` still uses server-relative URLs, and now formally delegates to `BrowserLauncher` to decide what to do with those URLs (internally, VSCode / `xdg-open` handles port forwarding)
 - Middleware configuration is no longer required to specify how it is reachable from clients.

This sets up some subsequent changes for more robust handling of tunnelled connections.

Changelog:
[General][Breaking] dev-middleware: Frameworks should specify `serverBaseUrl` relative to the middleware host.

Reviewed By: huntie

Differential Revision: D65974487

fbshipit-source-id: 1face8fc7715df387f75b329e80932d8543ee419
2024-11-18 13:46:50 -08:00

191 lines
5.5 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
* @oncall react_native
*/
import type {InspectorProxyQueries} from '../inspector-proxy/InspectorProxy';
import type {BrowserLauncher} 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 {
return async (
req: IncomingMessage,
res: ServerResponse,
next: (err?: Error) => void,
) => {
if (
req.method === 'POST' ||
(experiments.enableOpenDebuggerRedirect && req.method === 'GET')
) {
const {query} = url.parse(req.url, true);
const {
appId,
device,
launchId,
target: targetId,
}: {
/** @deprecated Will only match legacy Hermes targets */
appId?: string,
device?: string,
launchId?: string,
target?: string,
...
} = query;
const targets = inspectorProxy
.getPageDescriptions(new URL(serverBaseUrl))
.filter(
// Only use targets with better reloading support
app =>
app.title === LEGACY_SYNTHETIC_PAGE_TITLE ||
app.reactNative.capabilities?.nativePageReloads === true,
);
let target;
const launchType: 'launch' | 'redirect' =
req.method === 'POST' ? 'launch' : 'redirect';
if (
typeof targetId === 'string' ||
typeof appId === 'string' ||
typeof device === 'string'
) {
logger?.info(
(launchType === 'launch' ? 'Launching' : 'Redirecting to') +
' DevTools...',
);
target = targets.find(
_target =>
(targetId == null || _target.id === targetId) &&
(appId == null ||
(_target.appId === appId &&
_target.title === LEGACY_SYNTHETIC_PAGE_TITLE)) &&
(device == null || _target.reactNative.logicalDeviceId === 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;
try {
switch (launchType) {
case 'launch':
await browserLauncher.launchDebuggerAppWindow(
getDevToolsFrontendUrl(
experiments,
target.webSocketDebuggerUrl,
serverBaseUrl,
{launchId, useFuseboxEntryPoint},
),
);
res.writeHead(200);
res.end();
break;
case 'redirect':
res.writeHead(302, {
Location: getDevToolsFrontendUrl(
experiments,
target.webSocketDebuggerUrl,
serverBaseUrl,
{relative: true, launchId, useFuseboxEntryPoint},
),
});
res.end();
break;
default:
(launchType: empty);
}
eventReporter?.logEvent({
type: 'launch_debugger_frontend',
launchType,
status: 'success',
appId: appId ?? null,
deviceId: device ?? null,
resolvedTargetDescription: target.description,
resolvedTargetAppId: target.appId,
prefersFuseboxFrontend: useFuseboxEntryPoint ?? false,
});
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 ?? false,
});
return;
}
}
next();
};
}