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"