From 9e068ac163db057cc76bac2caba76136d19bcf49 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Tue, 10 Oct 2023 09:37:41 -0700 Subject: [PATCH] Add --experimental-debugger-frontend flag, restore 0.72 flow as base (#40766) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/40766 This changeset allows users to opt into the new debugger frontend experience by passing `--experimental-debugger` to `react-native start`. **We are defaulting this option to `true`** for now, but will continue to evaluate this feature before 0.73 ships. It restores Flipper (via `flipper://`) as the default handling for `/open-debugger` (matching 0.72 behaviour) when this flag is not enabled. Detailed changes: - Replaces `enableCustomDebuggerFrontend` experiment in `dev-middleware` with `enableNewDebugger`. The latter now hard-swaps between the Flipper and new launch flows. - Removes now-unused switching of `devtoolsFrontendUrl`. - Implements `deprecated_openFlipperMiddleware` (matching previous RN CLI implementation). - Disables "`j` to debug" key handler by default. - Marks "`j` to debug" and `/open-debugger` console logs as experimental. Changelog: [Changed][General] Gate new debugger frontend behind `--experimental-debugger` flag, restore Flipper as base launch flow Reviewed By: motiz88 Differential Revision: D50084590 fbshipit-source-id: 5234634f20110cb7933b1787bd2c86f645411fff --- flow-typed/npm/open_v7.x.x.js | 29 +++++++++ .../src/commands/start/attachKeyHandlers.js | 23 ++++--- .../src/commands/start/index.js | 10 ++++ .../src/commands/start/runServer.js | 4 +- packages/dev-middleware/package.json | 1 + .../dev-middleware/src/createDevMiddleware.js | 23 ++++--- .../deprecated_openFlipperMiddleware.js | 60 +++++++++++++++++++ .../src/middleware/openDebuggerMiddleware.js | 4 +- .../dev-middleware/src/types/Experiments.js | 7 ++- .../src/utils/getDevToolsFrontendUrl.js | 32 ++-------- yarn.lock | 10 +++- 11 files changed, 152 insertions(+), 51 deletions(-) create mode 100644 flow-typed/npm/open_v7.x.x.js create mode 100644 packages/dev-middleware/src/middleware/deprecated_openFlipperMiddleware.js diff --git a/flow-typed/npm/open_v7.x.x.js b/flow-typed/npm/open_v7.x.x.js new file mode 100644 index 00000000000000..a60b26aa5b7974 --- /dev/null +++ b/flow-typed/npm/open_v7.x.x.js @@ -0,0 +1,29 @@ +/** + * 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 + */ + +declare module 'open' { + import type {ChildProcess} from 'child_process'; + + declare export type Options = $ReadOnly<{ + wait?: boolean, + background?: boolean, + newInstance?: boolean, + allowNonzeroExitCode?: boolean, + ... + }>; + + declare type open = ( + target: string, + options?: Options, + ) => Promise; + + declare module.exports: open; +} diff --git a/packages/community-cli-plugin/src/commands/start/attachKeyHandlers.js b/packages/community-cli-plugin/src/commands/start/attachKeyHandlers.js index d33505ccb8d3e4..548a9e8f44d865 100644 --- a/packages/community-cli-plugin/src/commands/start/attachKeyHandlers.js +++ b/packages/community-cli-plugin/src/commands/start/attachKeyHandlers.js @@ -24,6 +24,7 @@ export default function attachKeyHandlers({ cliConfig, devServerUrl, messageSocket, + experimentalDebuggerFrontend, }: { cliConfig: Config, devServerUrl: string, @@ -31,6 +32,7 @@ export default function attachKeyHandlers({ broadcast: (type: string, params?: Record | null) => void, ... }>, + experimentalDebuggerFrontend: boolean, }) { if (process.stdin.isTTY !== true) { logger.debug('Interactive mode is not supported in this environment'); @@ -76,6 +78,9 @@ export default function attachKeyHandlers({ ).stdout?.pipe(process.stdout); break; case 'j': + if (!experimentalDebuggerFrontend) { + return; + } await fetch(devServerUrl + '/open-debugger', {method: 'POST'}); break; case CTRL_C: @@ -92,12 +97,16 @@ export default function attachKeyHandlers({ keyPressHandler.startInterceptingKeyStrokes(); logger.log( - ` -${chalk.bold('i')} - run on iOS -${chalk.bold('a')} - run on Android -${chalk.bold('d')} - open Dev Menu -${chalk.bold('j')} - open debugger -${chalk.bold('r')} - reload app -`, + [ + '', + `${chalk.bold('i')} - run on iOS`, + `${chalk.bold('a')} - run on Android`, + `${chalk.bold('d')} - open Dev Menu`, + ...(experimentalDebuggerFrontend + ? [`${chalk.bold('j')} - open debugger (experimental)`] + : []), + `${chalk.bold('r')} - reload app`, + '', + ].join('\n'), ); } diff --git a/packages/community-cli-plugin/src/commands/start/index.js b/packages/community-cli-plugin/src/commands/start/index.js index 8bcf6e1ac4b086..2a259367e2d1cd 100644 --- a/packages/community-cli-plugin/src/commands/start/index.js +++ b/packages/community-cli-plugin/src/commands/start/index.js @@ -95,6 +95,16 @@ const startCommand: Command = { name: '--no-interactive', description: 'Disables interactive mode', }, + { + name: '--experimental-debugger [bool]', + description: + "[Experimental] Enable the new debugger experience and 'j' to " + + 'debug. This enables the new frontend experience only: connection ' + + 'reliability and some basic features are unstable in this release.', + parse: (val: ?string): boolean => + val !== undefined && val !== 'false' && val !== '0', + default: true, + }, ], }; diff --git a/packages/community-cli-plugin/src/commands/start/runServer.js b/packages/community-cli-plugin/src/commands/start/runServer.js index a7cc82a9b5c7c1..60477b3131021f 100644 --- a/packages/community-cli-plugin/src/commands/start/runServer.js +++ b/packages/community-cli-plugin/src/commands/start/runServer.js @@ -33,6 +33,7 @@ export type StartCommandArgs = { assetPlugins?: string[], cert?: string, customLogReporterPath?: string, + experimentalDebugger: boolean, host?: string, https?: boolean, maxWorkers?: number, @@ -118,7 +119,7 @@ async function runServer( logger, unstable_experiments: { // NOTE: Only affects the /open-debugger endpoint - enableCustomDebuggerFrontend: true, + enableNewDebugger: args.experimentalDebugger, }, }); @@ -138,6 +139,7 @@ async function runServer( cliConfig: ctx, devServerUrl, messageSocket: messageSocketEndpoint, + experimentalDebuggerFrontend: args.experimentalDebugger, }); } }, diff --git a/packages/dev-middleware/package.json b/packages/dev-middleware/package.json index cf44092dc1cc32..8186610c2d89df 100644 --- a/packages/dev-middleware/package.json +++ b/packages/dev-middleware/package.json @@ -29,6 +29,7 @@ "connect": "^3.6.5", "debug": "^2.2.0", "node-fetch": "^2.2.0", + "open": "^7.0.3", "serve-static": "^1.13.1", "temp-dir": "^2.0.0" }, diff --git a/packages/dev-middleware/src/createDevMiddleware.js b/packages/dev-middleware/src/createDevMiddleware.js index 0958a37889cfff..c73d19a173743a 100644 --- a/packages/dev-middleware/src/createDevMiddleware.js +++ b/packages/dev-middleware/src/createDevMiddleware.js @@ -19,6 +19,7 @@ import reactNativeDebuggerFrontendPath from '@react-native/debugger-frontend'; import connect from 'connect'; import path from 'path'; import serveStaticMiddleware from 'serve-static'; +import deprecated_openFlipperMiddleware from './middleware/deprecated_openFlipperMiddleware'; import openDebuggerMiddleware from './middleware/openDebuggerMiddleware'; import InspectorProxy from './inspector-proxy/InspectorProxy'; import DefaultBrowserLauncher from './utils/DefaultBrowserLauncher'; @@ -85,14 +86,18 @@ export default function createDevMiddleware({ const middleware = connect() .use( '/open-debugger', - openDebuggerMiddleware({ - serverBaseUrl, - inspectorProxy, - browserLauncher: unstable_browserLauncher, - eventReporter: unstable_eventReporter, - experiments, - logger, - }), + experiments.enableNewDebugger + ? openDebuggerMiddleware({ + serverBaseUrl, + inspectorProxy, + browserLauncher: unstable_browserLauncher, + eventReporter: unstable_eventReporter, + experiments, + logger, + }) + : deprecated_openFlipperMiddleware({ + logger, + }), ) .use( '/debugger-frontend', @@ -110,7 +115,7 @@ export default function createDevMiddleware({ function getExperiments(config: ExperimentsConfig): Experiments { return { - enableCustomDebuggerFrontend: config.enableCustomDebuggerFrontend ?? false, + enableNewDebugger: config.enableNewDebugger ?? false, enableOpenDebuggerRedirect: config.enableOpenDebuggerRedirect ?? false, }; } diff --git a/packages/dev-middleware/src/middleware/deprecated_openFlipperMiddleware.js b/packages/dev-middleware/src/middleware/deprecated_openFlipperMiddleware.js new file mode 100644 index 00000000000000..35a222ce7f0207 --- /dev/null +++ b/packages/dev-middleware/src/middleware/deprecated_openFlipperMiddleware.js @@ -0,0 +1,60 @@ +/** + * 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 {NextHandleFunction} from 'connect'; +import type {IncomingMessage, ServerResponse} from 'http'; +import type {Logger} from '../types/Logger'; + +import open from 'open'; + +const FLIPPER_SELF_CONNECT_URL = + 'flipper://null/Hermesdebuggerrn?device=React%20Native'; + +type Options = $ReadOnly<{ + logger?: Logger, +}>; + +/** + * Open the legacy Flipper debugger (Hermes). + * + * @deprecated This replicates the pre-0.73 workflow of opening Flipper via the + * `flipper://` URL scheme, failing if Flipper is not installed locally. This + * flow will be removed in a future version. + */ +export default function deprecated_openFlipperMiddleware({ + logger, +}: Options): NextHandleFunction { + return async ( + req: IncomingMessage, + res: ServerResponse, + next: (err?: Error) => void, + ) => { + if (req.method === 'POST') { + logger?.info('Launching JS debugger...'); + + try { + logger?.warn( + 'Attempting to debug JS in Flipper (deprecated). This requires ' + + 'Flipper to be installed on your system to handle the ' + + "'flipper://' URL scheme.", + ); + await open(FLIPPER_SELF_CONNECT_URL); + res.end(); + } catch (e) { + logger?.error( + 'Error launching Flipper: ' + e.message ?? 'Unknown error', + ); + res.writeHead(500); + res.end(); + } + } + }; +} diff --git a/packages/dev-middleware/src/middleware/openDebuggerMiddleware.js b/packages/dev-middleware/src/middleware/openDebuggerMiddleware.js index 2e3e1f5594bfb5..14ee05051a6099 100644 --- a/packages/dev-middleware/src/middleware/openDebuggerMiddleware.js +++ b/packages/dev-middleware/src/middleware/openDebuggerMiddleware.js @@ -72,7 +72,7 @@ export default function openDebuggerMiddleware({ if (typeof appId === 'string') { logger?.info( (launchType === 'launch' ? 'Launching' : 'Redirecting to') + - ' JS debugger...', + ' JS debugger (experimental)...', ); target = targets.find(_target => _target.description === appId); } else { @@ -108,7 +108,6 @@ export default function openDebuggerMiddleware({ getDevToolsFrontendUrl( target.webSocketDebuggerUrl, serverBaseUrl, - experiments, ), ), ); @@ -120,7 +119,6 @@ export default function openDebuggerMiddleware({ target.webSocketDebuggerUrl, // Use a relative URL. '', - experiments, ), }); res.end(); diff --git a/packages/dev-middleware/src/types/Experiments.js b/packages/dev-middleware/src/types/Experiments.js index 8e0db090f44ffc..9f15d94c8ef3d7 100644 --- a/packages/dev-middleware/src/types/Experiments.js +++ b/packages/dev-middleware/src/types/Experiments.js @@ -10,10 +10,11 @@ export type Experiments = $ReadOnly<{ /** - * Enables the use of the custom debugger frontend (@react-native/debugger-frontend) - * in the /open-debugger endpoint. + * Enables the new JS debugger launch flow and custom debugger frontend + * (@react-native/debugger-frontend). When disabled, /open-debugger will + * trigger the legacy Flipper connection flow. */ - enableCustomDebuggerFrontend: boolean, + enableNewDebugger: boolean, /** * Enables the handling of GET requests in the /open-debugger endpoint, diff --git a/packages/dev-middleware/src/utils/getDevToolsFrontendUrl.js b/packages/dev-middleware/src/utils/getDevToolsFrontendUrl.js index 4d95f18bf48457..0178c4db2d3f73 100644 --- a/packages/dev-middleware/src/utils/getDevToolsFrontendUrl.js +++ b/packages/dev-middleware/src/utils/getDevToolsFrontendUrl.js @@ -9,40 +9,18 @@ * @oncall react_native */ -import type {Experiments} from '../types/Experiments'; - -/** - * The Chrome DevTools frontend revision to use. This should be set to the - * latest version known to be compatible with Hermes. - * - * Revision should be the full identifier from: - * https://chromium.googlesource.com/chromium/src.git - */ -const DEVTOOLS_FRONTEND_REV = 'd9568d04d7dd79269c5a655d7ada69650c5a8336'; // Chrome 100.0.4896.75 - /** - * Construct the URL to Chrome DevTools connected to a given debugger target. + * Get the DevTools frontend URL to debug a given React Native CDP target. */ export default function getDevToolsFrontendUrl( webSocketDebuggerUrl: string, devServerUrl: string, - experiments: Experiments, ): string { const scheme = new URL(webSocketDebuggerUrl).protocol.slice(0, -1); - const webSocketUrlWithoutProtocol = webSocketDebuggerUrl.replace( - /^wss?:\/\//, - '', + const appUrl = `${devServerUrl}/debugger-frontend/rn_inspector.html`; + const webSocketUrlWithoutProtocol = encodeURIComponent( + webSocketDebuggerUrl.replace(/^wss?:\/\//, ''), ); - 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, - )}`; + return `${appUrl}?${scheme}=${webSocketUrlWithoutProtocol}&sources.hide_add_folder=true`; } diff --git a/yarn.lock b/yarn.lock index e639b25624d762..525c1c06069d96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5983,7 +5983,7 @@ is-wsl@^1.1.0: resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= -is-wsl@^2.2.0: +is-wsl@^2.1.1, is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== @@ -7617,6 +7617,14 @@ open@^6.2.0: dependencies: is-wsl "^1.1.0" +open@^7.0.3: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"