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:
Родитель
fd32931f95
Коммит
850e550422
|
@ -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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче