From 0f4e5c382e32a2b223127479834b758116f9fc57 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Wed, 27 Aug 2025 02:50:05 -0700 Subject: [PATCH] Provisionally support using prebuilt shell binaries via DotSlash (#53436) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/53436 Changelog: [Internal] Adds a `flavor` option to `unstable_spawnDebuggerShellWithArgs` to select between two modes: 1. `flavor: 'dev'` (current behaviour) - launching a stock Electron binary (from the `electron` package) and pointing it directly at the shell code from the `src/electron` directory. 2. `flavor: 'prebuilt'` (new in this diff) - launching the prebuilt React Native DevTools binary included in the package (built continuously at Meta and committed as a DotSlash file in automated diffs e.g. D79836825). Note that this binary includes Electron *and* a frozen version of the shell code from `src/electron`. Going forward, `'dev'` will only be used when developing the package (e.g. in D78351934 we will move `electron` to `devDependencies`). The published version of the package is only intended to work with `flavor: 'prebuilt'`. Reviewed By: huntie Differential Revision: D78351931 fbshipit-source-id: d0e66b54c142dc2910619ba3d6d149d88324c872 --- packages/debugger-shell/package.json | 6 +- .../src/node/__tests__/debugger-shell-test.js | 48 ++++++++++++++++ .../debugger-shell/src/node/index.flow.js | 56 +++++++++++++------ yarn.lock | 5 ++ 4 files changed, 96 insertions(+), 19 deletions(-) create mode 100644 packages/debugger-shell/src/node/__tests__/debugger-shell-test.js diff --git a/packages/debugger-shell/package.json b/packages/debugger-shell/package.json index 4b0602b08bb..6c1706dd55d 100644 --- a/packages/debugger-shell/package.json +++ b/packages/debugger-shell/package.json @@ -26,12 +26,12 @@ }, "license": "MIT", "engines": { - "node": ">= 20.19.4", - "electron": ">=37.2.6" + "node": ">= 20.19.4" }, "dependencies": { "cross-spawn": "^7.0.6", - "electron": "37.2.6" + "electron": "37.2.6", + "fb-dotslash": "0.5.8" }, "devDependencies": { "semver": "^7.1.3" diff --git a/packages/debugger-shell/src/node/__tests__/debugger-shell-test.js b/packages/debugger-shell/src/node/__tests__/debugger-shell-test.js new file mode 100644 index 00000000000..f74281512be --- /dev/null +++ b/packages/debugger-shell/src/node/__tests__/debugger-shell-test.js @@ -0,0 +1,48 @@ +/** + * 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 + */ + +const {unstable_spawnDebuggerShellWithArgs} = require('../../'); + +describe('debugger-shell Node package', () => { + test('can spawn in detached+prebuilt mode without crashing', async () => { + await expect( + unstable_spawnDebuggerShellWithArgs(['--version'], { + flavor: 'prebuilt', + mode: 'detached', + }), + ).resolves.toBeUndefined(); + }); + + // When running in the internal react-native-oss-js job, Electron isn't + // installed correctly (postinstall scripts don't run) but the internal + // `electron` workspace isn't available either. Detecting this dynamically + // weakens the test somewhat in environments where it *should* pass, but this + // is a dev-only feature anyway so this is fine. + if (isElectronInstalled()) { + test('can spawn in detached+dev mode without crashing', async () => { + await expect( + unstable_spawnDebuggerShellWithArgs(['--version'], { + flavor: 'dev', + mode: 'detached', + }), + ).resolves.toBeUndefined(); + }); + } +}); + +function isElectronInstalled() { + try { + require('electron'); + return true; + } catch { + return false; + } +} diff --git a/packages/debugger-shell/src/node/index.flow.js b/packages/debugger-shell/src/node/index.flow.js index 8f1d2edd32d..11a1137555e 100644 --- a/packages/debugger-shell/src/node/index.flow.js +++ b/packages/debugger-shell/src/node/index.flow.js @@ -9,43 +9,43 @@ */ const {spawn} = require('cross-spawn'); +const path = require('path'); + +// The 'prebuilt' flavor will use the prebuilt shell binary (and the JavaScript embedded in it). +// The 'dev' flavor will use a stock Electron binary and run the shell code from the `electron/` directory. +type DebuggerShellFlavor = 'prebuilt' | 'dev'; async function unstable_spawnDebuggerShellWithArgs( args: string[], { mode = 'detached', + flavor = 'dev', }: $ReadOnly<{ // In 'syncAndExit' mode, the current process will block until the spawned process exits, and then it will exit // with the same exit code as the spawned process. // In 'detached' mode, the spawned process will be detached from the current process and the current process will // continue to run normally. mode?: 'syncThenExit' | 'detached', + flavor?: DebuggerShellFlavor, }> = {}, ): Promise { - // NOTE: Internally at Meta, this is aliased to a workspace that is - // API-compatible with the 'electron' package, but contains prebuilt binaries - // that do not need to be downloaded in a postinstall action. - const electronPath = require('electron'); + const [binaryPath, baseArgs] = getShellBinaryAndArgs(flavor); return new Promise((resolve, reject) => { - const child = spawn( - electronPath, - [require.resolve('../electron'), ...args], - { - stdio: 'inherit', - windowsHide: true, - detached: mode === 'detached', - }, - ); + const child = spawn(binaryPath, [...baseArgs, ...args], { + stdio: 'inherit', + windowsHide: true, + detached: mode === 'detached', + }); if (mode === 'detached') { child.on('spawn', () => { resolve(); }); - child.on('close', (code /*: number */) => { + child.on('close', (code: number) => { if (code !== 0) { reject( new Error( - `Failed to open debugger shell: ${electronPath} exited with code ${code}`, + `Failed to open debugger shell: exited with code ${code}`, ), ); } @@ -54,7 +54,7 @@ async function unstable_spawnDebuggerShellWithArgs( } else if (mode === 'syncThenExit') { child.on('close', function (code, signal) { if (code === null) { - console.error(electronPath, 'exited with signal', signal); + console.error('Debugger shell exited with signal', signal); process.exit(1); } process.exit(code); @@ -74,4 +74,28 @@ async function unstable_spawnDebuggerShellWithArgs( }); } +function getShellBinaryAndArgs( + flavor: DebuggerShellFlavor, +): [string, Array] { + switch (flavor) { + case 'prebuilt': + return [ + // $FlowIssue[cannot-resolve-module] fb-dotslash includes Flow types but Flow does not pick them up + require('fb-dotslash'), + [path.join(__dirname, '../../bin/react-native-devtools')], + ]; + case 'dev': + return [ + // NOTE: Internally at Meta, this is aliased to a workspace that is + // API-compatible with the 'electron' package, but contains prebuilt binaries + // that do not need to be downloaded in a postinstall action. + require('electron'), + [require.resolve('../electron')], + ]; + default: + flavor as empty; + throw new Error(`Unknown flavor: ${flavor}`); + } +} + export {unstable_spawnDebuggerShellWithArgs}; diff --git a/yarn.lock b/yarn.lock index 25056c18f0b..af341029532 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4507,6 +4507,11 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fb-dotslash@0.5.8: + version "0.5.8" + resolved "https://registry.yarnpkg.com/fb-dotslash/-/fb-dotslash-0.5.8.tgz#c5ef3dacd75e1ddb2197c367052464ddde0115f5" + integrity sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA== + fb-watchman@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c"