From b5dfb32ed3ea12faf32de3ee02c6e8cf99ed0d8f Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Wed, 27 Aug 2025 02:50:05 -0700 Subject: [PATCH] Support preparing debugger shell ahead of "open DevTools" (#53437) 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 --- .../debugger-shell/src/node/index.flow.js | 1 + packages/dev-middleware/package.json | 1 + .../dev-middleware/src/createDevMiddleware.js | 29 +++++++++++++ packages/dev-middleware/src/index.flow.js | 5 ++- .../src/middleware/openDebuggerMiddleware.js | 41 ++++++++++++++++--- .../src/types/BrowserLauncher.js | 17 ++++++++ .../dev-middleware/src/types/EventReporter.js | 6 +++ 7 files changed, 94 insertions(+), 6 deletions(-) diff --git a/packages/debugger-shell/src/node/index.flow.js b/packages/debugger-shell/src/node/index.flow.js index 88cbee5cc1b..3b7e0dbcca6 100644 --- a/packages/debugger-shell/src/node/index.flow.js +++ b/packages/debugger-shell/src/node/index.flow.js @@ -87,6 +87,7 @@ async function unstable_spawnDebuggerShellWithArgs( export type DebuggerShellPreparationResult = $ReadOnly<{ code: | 'success' + | 'not_implemented' | 'likely_offline' | 'platform_not_supported' | 'possible_corruption' diff --git a/packages/dev-middleware/package.json b/packages/dev-middleware/package.json index 2183969a850..cb420fc8132 100644 --- a/packages/dev-middleware/package.json +++ b/packages/dev-middleware/package.json @@ -38,6 +38,7 @@ "node": ">= 20.19.4" }, "devDependencies": { + "@react-native/debugger-shell": "0.82.0-main", "selfsigned": "^2.4.1", "undici": "^5.29.0", "wait-for-expect": "^3.0.2" diff --git a/packages/dev-middleware/src/createDevMiddleware.js b/packages/dev-middleware/src/createDevMiddleware.js index 67928d7bb06..1263fec7e5c 100644 --- a/packages/dev-middleware/src/createDevMiddleware.js +++ b/packages/dev-middleware/src/createDevMiddleware.js @@ -170,6 +170,35 @@ function createWrappedEventReporter( '\u001B[27m', ); break; + case 'fusebox_shell_preparation_attempt': + switch (event.result.code) { + case 'success': + case 'not_implemented': + break; + case 'unexpected_error': { + let message = + event.result.humanReadableMessage ?? + 'An unknown error occurred while installing React Native DevTools.'; + if (event.result.verboseInfo != null) { + message += ` Details:\n\n${event.result.verboseInfo}`; + } else { + message += '.'; + } + logger?.error(message); + break; + } + case 'possible_corruption': + case 'platform_not_supported': + case 'likely_offline': + logger?.warn( + event.result.humanReadableMessage ?? + `An error of type ${event.result.code} occurred while installing React Native DevTools.`, + ); + break; + default: + (event.result.code: empty); + break; + } } reporter?.logEvent(event); diff --git a/packages/dev-middleware/src/index.flow.js b/packages/dev-middleware/src/index.flow.js index ef1027e3910..868b45f002c 100644 --- a/packages/dev-middleware/src/index.flow.js +++ b/packages/dev-middleware/src/index.flow.js @@ -10,7 +10,10 @@ export {default as createDevMiddleware} from './createDevMiddleware'; -export type {BrowserLauncher} from './types/BrowserLauncher'; +export type { + BrowserLauncher, + DebuggerShellPreparationResult, +} from './types/BrowserLauncher'; export type {EventReporter, ReportableEvent} from './types/EventReporter'; export type { CustomMessageHandler, diff --git a/packages/dev-middleware/src/middleware/openDebuggerMiddleware.js b/packages/dev-middleware/src/middleware/openDebuggerMiddleware.js index fb60f116a4d..a6bc5a5ea29 100644 --- a/packages/dev-middleware/src/middleware/openDebuggerMiddleware.js +++ b/packages/dev-middleware/src/middleware/openDebuggerMiddleware.js @@ -10,7 +10,10 @@ import type {InspectorProxyQueries} from '../inspector-proxy/InspectorProxy'; import type {PageDescription} from '../inspector-proxy/types'; -import type {BrowserLauncher} from '../types/BrowserLauncher'; +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'; @@ -48,6 +51,19 @@ export default function openDebuggerMiddleware({ experiments, inspectorProxy, }: Options): NextHandleFunction { + let shellPreparationPromise: Promise; + 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, @@ -155,10 +171,25 @@ export default function openDebuggerMiddleware({ panel: query.panel, }, ); - if ( - useFuseboxEntryPoint && - experiments.enableStandaloneFuseboxShell - ) { + 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, diff --git a/packages/dev-middleware/src/types/BrowserLauncher.js b/packages/dev-middleware/src/types/BrowserLauncher.js index 8e3b9169e6f..36a5ac1555a 100644 --- a/packages/dev-middleware/src/types/BrowserLauncher.js +++ b/packages/dev-middleware/src/types/BrowserLauncher.js @@ -8,6 +8,10 @@ * @format */ +import type {DebuggerShellPreparationResult} from '@react-native/debugger-shell'; + +export type {DebuggerShellPreparationResult}; + /** * An interface for integrators to provide a custom implementation for * opening URLs in a web browser. @@ -43,4 +47,17 @@ export interface BrowserLauncher { * this as necessary where the server is remote. */ unstable_showFuseboxShell?: (url: string, windowKey: string) => Promise; + + /** + * Attempt to prepare the debugger shell for use and returns a coded result + * that can be used to advise the user on how to proceed in case of failure. + * + * This function MAY be called multiple times or not at all. Implementers + * SHOULD use the opportunity to prefetch and cache any expensive resources (e.g + * platform-specific binaries needed in order to show the Fusebox shell). After a + * successful call, subsequent calls SHOULD complete quickly. The implementation + * SHOULD NOT return a rejecting promise in any case, and instead SHOULD report + * errors via the returned result object. + */ + unstable_prepareFuseboxShell?: () => Promise; } diff --git a/packages/dev-middleware/src/types/EventReporter.js b/packages/dev-middleware/src/types/EventReporter.js index 1f7e89b8d4b..f26ec260b46 100644 --- a/packages/dev-middleware/src/types/EventReporter.js +++ b/packages/dev-middleware/src/types/EventReporter.js @@ -8,6 +8,8 @@ * @format */ +import type {DebuggerShellPreparationResult} from './BrowserLauncher'; + type SuccessResult = { status: 'success', ...Props, @@ -132,6 +134,10 @@ export type ReportableEvent = duration: number, ...ConnectionUptime, ...DebuggerSessionIDs, + } + | { + type: 'fusebox_shell_preparation_attempt', + result: DebuggerShellPreparationResult, }; /**