Add serverBaseUrl option, set client-accessible URL value externally (#39456)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/39456

**Fixes new debugger launch flow on Android:**

D49158227 aimed to improve proxy-safe behaviour for remote dev servers by auto-detecting the appropriate server URL for clients using the `Host` header (etc) from the HTTP request. However, this approach broke the local case for Android emulators and externally connected devices since they would originate from a device-relative server hostname — e.g. `10.0.2.2` for the stock Android emulator.

https://pxl.cl/3mVmR

This commit reverts to an explicit approach where callers specify the base URL to the dev server that should be addressible from the development machine — now as a single `serverBaseUrl` option.

**Changes**

- Adds new `serverBaseUrl` option to `createDevMiddleware`, designed to be the base URL value for constructing dev server URLs returned in endpoints such as `/json/list`.
    - This changes little for the `localhost` case (now enabling `https://` URLs), but enables remote dev server setups to specify this cleanly.
- Updates call site in `community-cli-plugin`.

Changelog: [Internal]

Reviewed By: robhogan

Differential Revision: D49276125

fbshipit-source-id: 2b6a8507073649832993971aa9d0870f54c9bd44
This commit is contained in:
Alex Hunt 2023-09-15 13:11:04 -07:00 коммит произвёл Facebook GitHub Bot
Родитель fd32931f95
Коммит 850e550422
9 изменённых файлов: 63 добавлений и 124 удалений

Просмотреть файл

@ -114,6 +114,7 @@ async function runServer(
});
const {middleware, websocketEndpoints} = createDevMiddleware({
projectRoot,
serverBaseUrl: devServerUrl,
logger,
unstable_experiments: {
// NOTE: Only affects the /open-debugger endpoint

Просмотреть файл

@ -16,6 +16,7 @@ function myDevServerImpl(args) {
const {middleware, websocketEndpoints} = createDevMiddleware({
projectRoot: metroConfig.projectRoot,
serverBaseUrl: `http://${args.host}:${args.port}`,
logger,
});

Просмотреть файл

@ -24,7 +24,6 @@
"dependencies": {
"@isaacs/ttlcache": "^1.4.1",
"@react-native/debugger-frontend": "^0.73.0",
"actual-request-url": "^1.0.4",
"chrome-launcher": "^0.15.2",
"chromium-edge-launcher": "^1.0.0",
"connect": "^3.6.5",

Просмотреть файл

@ -26,9 +26,38 @@ import DefaultBrowserLauncher from './utils/DefaultBrowserLauncher';
type Options = $ReadOnly<{
projectRoot: string,
/**
* The base URL to the dev server, as addressible from the local developer
* machine. This is used in responses which return URLs to other endpoints,
* e.g. the debugger frontend and inspector proxy targets.
*
* Example: `'http://localhost:8081'`.
*/
serverBaseUrl: string,
logger?: Logger,
/**
* An interface for integrators to provide a custom implementation for
* opening URLs in a web browser.
*
* This is an unstable API with no semver guarantees.
*/
unstable_browserLauncher?: BrowserLauncher,
/**
* An interface for logging events.
*
* This is an unstable API with no semver guarantees.
*/
unstable_eventReporter?: EventReporter,
/**
* The set of experimental features to enable.
*
* This is an unstable API with no semver guarantees.
*/
unstable_experiments?: ExperimentsConfig,
}>;
@ -39,6 +68,7 @@ type DevMiddlewareAPI = $ReadOnly<{
export default function createDevMiddleware({
projectRoot,
serverBaseUrl,
logger,
unstable_browserLauncher = DefaultBrowserLauncher,
unstable_eventReporter,
@ -48,6 +78,7 @@ export default function createDevMiddleware({
const inspectorProxy = new InspectorProxy(
projectRoot,
serverBaseUrl,
unstable_eventReporter,
experiments,
);
@ -56,10 +87,11 @@ export default function createDevMiddleware({
.use(
'/open-debugger',
openDebuggerMiddleware({
serverBaseUrl,
inspectorProxy,
browserLauncher: unstable_browserLauncher,
eventReporter: unstable_eventReporter,
experiments,
inspectorProxy,
logger,
}),
)

Просмотреть файл

@ -22,7 +22,6 @@ import type {IncomingMessage, ServerResponse} from 'http';
import url from 'url';
import WS from 'ws';
import Device from './Device';
import getDevServerUrl from '../utils/getDevServerUrl';
const debug = require('debug')('Metro:InspectorProxy');
@ -35,11 +34,7 @@ const PAGES_LIST_JSON_VERSION_URL = '/json/version';
const INTERNAL_ERROR_CODE = 1011;
export interface InspectorProxyQueries {
getPageDescriptions(
options: $ReadOnly<{
wsPublicBaseUrl: string,
}>,
): Array<PageDescription>;
getPageDescriptions(): Array<PageDescription>;
}
/**
@ -49,6 +44,9 @@ export default class InspectorProxy implements InspectorProxyQueries {
// Root of the project used for relative to absolute source path conversion.
_projectRoot: string;
/** The base URL to the dev server from the developer machine. */
_serverBaseUrl: string;
// Maps device ID to Device instance.
_devices: Map<string, Device>;
@ -61,20 +59,18 @@ export default class InspectorProxy implements InspectorProxyQueries {
constructor(
projectRoot: string,
serverBaseUrl: string,
eventReporter: ?EventReporter,
experiments: Experiments,
) {
this._projectRoot = projectRoot;
this._serverBaseUrl = serverBaseUrl;
this._devices = new Map();
this._eventReporter = eventReporter;
this._experiments = experiments;
}
getPageDescriptions({
wsPublicBaseUrl,
}: $ReadOnly<{
wsPublicBaseUrl: string,
}>): Array<PageDescription> {
getPageDescriptions(): Array<PageDescription> {
// Build list of pages from all devices.
let result: Array<PageDescription> = [];
Array.from(this._devices.entries()).forEach(([deviceId, device]) => {
@ -82,7 +78,7 @@ export default class InspectorProxy implements InspectorProxyQueries {
device
.getPagesList()
.map((page: Page) =>
this._buildPageDescription(deviceId, device, page, wsPublicBaseUrl),
this._buildPageDescription(deviceId, device, page),
),
);
});
@ -102,14 +98,7 @@ export default class InspectorProxy implements InspectorProxyQueries {
request.url === PAGES_LIST_JSON_URL ||
request.url === PAGES_LIST_JSON_URL_2
) {
const wsPublicBaseUrl = getDevServerUrl(request, 'public', 'ws');
this._sendJsonResponse(
response,
this.getPageDescriptions({
wsPublicBaseUrl,
}),
);
this._sendJsonResponse(response, this.getPageDescriptions());
} else if (request.url === PAGES_LIST_JSON_VERSION_URL) {
this._sendJsonResponse(response, {
Browser: 'Mobile JavaScript',
@ -135,21 +124,18 @@ export default class InspectorProxy implements InspectorProxyQueries {
deviceId: string,
device: Device,
page: Page,
wsServerBaseUrl: string,
): PageDescription {
const webSocketDebuggerUrl = `${wsServerBaseUrl}${WS_DEBUGGER_URL}?device=${deviceId}&page=${page.id}`;
const {host, protocol} = new URL(this._serverBaseUrl);
const webSocketScheme = protocol === 'https:' ? 'wss' : 'ws';
const webSocketUrlWithoutProtocol = `${host}${WS_DEBUGGER_URL}?device=${deviceId}&page=${page.id}`;
const webSocketDebuggerUrl = `${webSocketScheme}://${webSocketUrlWithoutProtocol}`;
const isSecure = webSocketDebuggerUrl.startsWith('wss://');
const webSocketUrlWithoutProtocol = webSocketDebuggerUrl.replace(
/^wss?:\/\//,
'',
);
const scheme = isSecure ? 'wss' : 'ws';
// For now, `/json/list` returns the legacy built-in `devtools://` URL, to
// preserve existing handling by Flipper. This may return a placeholder in
// future -- please use the `/open-debugger` endpoint.
const devtoolsFrontendUrl =
`devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&${scheme}=` +
`devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&${webSocketScheme}=` +
encodeURIComponent(webSocketUrlWithoutProtocol);
return {

Просмотреть файл

@ -18,14 +18,14 @@ import type {Experiments} from '../types/Experiments';
import type {Logger} from '../types/Logger';
import url from 'url';
import getDevServerUrl from '../utils/getDevServerUrl';
import getDevToolsFrontendUrl from '../utils/getDevToolsFrontendUrl';
const debuggerInstances = new Map<string, ?LaunchedBrowser>();
type Options = $ReadOnly<{
browserLauncher: BrowserLauncher,
serverBaseUrl: string,
logger?: Logger,
browserLauncher: BrowserLauncher,
eventReporter?: EventReporter,
experiments: Experiments,
inspectorProxy: InspectorProxyQueries,
@ -40,11 +40,12 @@ type Options = $ReadOnly<{
* @see https://chromedevtools.github.io/devtools-protocol/
*/
export default function openDebuggerMiddleware({
serverBaseUrl,
logger,
browserLauncher,
eventReporter,
experiments,
inspectorProxy,
logger,
}: Options): NextHandleFunction {
return async (
req: IncomingMessage,
@ -58,15 +59,11 @@ export default function openDebuggerMiddleware({
const {query} = url.parse(req.url, true);
const {appId} = query;
const targets = inspectorProxy
.getPageDescriptions({
wsPublicBaseUrl: getDevServerUrl(req, 'public', 'ws'),
})
.filter(
// Only use targets with better reloading support
app =>
app.title === 'React Native Experimental (Improved Chrome Reloads)',
);
const targets = inspectorProxy.getPageDescriptions().filter(
// Only use targets with better reloading support
app =>
app.title === 'React Native Experimental (Improved Chrome Reloads)',
);
let target;
const launchType: 'launch' | 'redirect' =
@ -110,7 +107,7 @@ export default function openDebuggerMiddleware({
await browserLauncher.launchDebuggerAppWindow(
getDevToolsFrontendUrl(
target.webSocketDebuggerUrl,
getDevServerUrl(req, 'public'),
serverBaseUrl,
experiments,
),
),

Просмотреть файл

@ -1,73 +0,0 @@
/**
* 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
* @format
* @oncall react_native
*/
import type {IncomingMessage} from 'http';
import {getProto, getHost} from 'actual-request-url';
import net from 'net';
import {TLSSocket} from 'tls';
/**
* Get the base URL to address the current development server.
*/
export default function getDevServerUrl(
/** The current HTTP request. */
req: IncomingMessage,
/**
* 'public' for a URL accessible by the same client that sent the request, or
* 'local' for for a URL accessible from the machine running the dev server.
*/
kind: 'public' | 'local',
/**
* If 'ws', returns a WebSocket URL corresponding to the current server,
* with the appropriate scheme (ws or wss) for HTTP / HTTPS connections.
*/
protocolType: 'http' | 'ws' = 'http',
): string {
if (kind === 'public') {
const host = getHost(req);
if (host != null) {
let scheme = getProto(req);
if (protocolType === 'ws') {
scheme = httpSchemeToWsScheme(scheme);
}
return `${scheme}://${host}`;
}
// If we can't determine a public URL, fall back to a local URL, which *might* still work.
}
let scheme =
req.socket instanceof TLSSocket && req.socket.encrypted === true
? 'https'
: 'http';
if (protocolType === 'ws') {
scheme = httpSchemeToWsScheme(scheme);
}
const {localAddress, localPort} = req.socket;
const address =
localAddress && net.isIPv6(localAddress)
? `[${localAddress}]`
: localAddress;
return `${scheme}://${address}:${localPort}`;
}
function httpSchemeToWsScheme(scheme: string) {
switch (scheme) {
case 'https':
return 'wss';
case 'http':
return 'ws';
default:
throw new Error(`Expected http or https but received ${scheme}`);
}
}

Просмотреть файл

@ -28,18 +28,19 @@ export default function getDevToolsFrontendUrl(
devServerUrl: string,
experiments: Experiments,
): string {
const isSecure = webSocketDebuggerUrl.startsWith('wss://');
const scheme = new URL(webSocketDebuggerUrl).protocol.slice(0, -1);
const webSocketUrlWithoutProtocol = webSocketDebuggerUrl.replace(
/^wss?:\/\//,
'',
);
const scheme = isSecure ? 'wss' : 'ws';
if (experiments.enableCustomDebuggerFrontend) {
const urlBase = `${devServerUrl}/debugger-frontend/rn_inspector.html`;
return `${urlBase}?${scheme}=${encodeURIComponent(
webSocketUrlWithoutProtocol,
)}&sources.hide_add_folder=true`;
}
const urlBase = `https://chrome-devtools-frontend.appspot.com/serve_rev/@${DEVTOOLS_FRONTEND_REV}/devtools_app.html`;
return `${urlBase}?panel=console&${scheme}=${encodeURIComponent(
webSocketUrlWithoutProtocol,

Просмотреть файл

@ -3021,11 +3021,6 @@ acorn@^8.9.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
actual-request-url@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/actual-request-url/-/actual-request-url-1.0.4.tgz#54454514e3715a4c60a1e26e9a49423e376a7563"
integrity sha512-9AOnrTOkog3eM7l4Y702+BKTYL1Tvxcl4EBbKrhDTB87npkss+I1dirUox2OyZVVz95BnavGgSZanWWas43j7A==
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"