Skip to content

Commit 63f4fb1

Browse files
motiz88facebook-github-bot
authored andcommitted
Scaffold debugger-shell package (#51688)
Summary: Pull Request resolved: #51688 Changelog: [Internal] # Context See D74904547. ## This diff Creates the `react-native/debugger-shell` package, containing a basic implementation of an Electron-based shell for React Native DevTools. At this point, there is no direct dependency on the new package from the rest of React Native - it's designed to be used as part of a Meta-internal experimental rollout of the new debugger shell via the `BrowserLauncher` interface in `dev-middleware`. Reviewed By: huntie Differential Revision: D74820232 fbshipit-source-id: cb06ea9e2ed8c8822019cad8296cc19e69f9db0b
1 parent 55ff50f commit 63f4fb1

File tree

12 files changed

+716
-6
lines changed

12 files changed

+716
-6
lines changed

flow-typed/npm/cross-spawn_v7.x.x.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict
8+
* @format
9+
*/
10+
11+
declare module 'cross-spawn' {
12+
import * as child_process from 'child_process';
13+
14+
type spawn = typeof child_process.spawn & {
15+
spawn: spawn,
16+
sync: typeof child_process.spawnSync,
17+
};
18+
19+
declare module.exports: spawn;
20+
}

flow-typed/npm/electron_v36.x.x.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
// Types for Electron when required as a Node package.
12+
declare module 'electron' {
13+
declare module.exports: string;
14+
}

packages/debugger-shell/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# @react-native/debugger-shell
2+
3+
![npm package](https://img.shields.io/npm/v/@react-native/debugger-shell?color=brightgreen&label=npm%20package)
4+
5+
Experimental Electron-based shell for React Native DevTools. This package is not part of React Native's public API.
6+
7+
## Why Electron?
8+
9+
The React Native DevTools frontend is based on Chrome DevTools, which is a web app, but is not particularly portable: it's designed to run in Chromium, and Chromium only. Prior to `@react-native/debugger-shell`, we would run it in [hosted mode](https://chromium.googlesource.com/devtools/devtools-frontend/+/main/docs/get_the_code.md#running-in-hosted-mode) in an instance of Chrome or Edge.
10+
11+
Relying on hosted mode presents a variety of UX issues in the debugging workflow, such as the need to ask developers to install a particular browser before they can debug in React Native, and the inability to foreground/reuse existing debugger windows when relaunching the debugger for the same app. In order to address these issues effectively, we fundamentally need to leave the browser sandbox and run the debugger in a shell we can bundle with React Native, and whose behavior we can control.
12+
13+
Electron is a tried-and-tested framework for the *specific* task of embedding a Chromium browser in a portable, customized shell. As a rule we'll hold a high bar for performance and reliability, and we'll only add features to the shell if they are strictly necessary to complement the DevTools frontend's built-in capabilities.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
const semver = require('semver');
14+
15+
// This test ensures that the electron dependency declared in our package.json
16+
// is semver-satisfied by the actual version of the electron package we're using.
17+
// While this is normally the job of a package manager like Yarn, in our case we
18+
// may use Yarn forced resolutions that defeat versioning, so we want additional
19+
// safety to ensure the target of the resolution is in sync with the declared dependency.
20+
describe('Electron dependency', () => {
21+
test('should be semver-satisfied by the actual electron version', () => {
22+
// $FlowIssue[untyped-import] - package.json is not typed
23+
const ourPackageJson = require('../package.json');
24+
25+
const declaredElectronVersion = ourPackageJson.dependencies.electron;
26+
expect(declaredElectronVersion).toBeTruthy();
27+
28+
// $FlowIssue[untyped-import] - package.json is not typed
29+
const electronPackageJson = require('electron/package.json');
30+
31+
const actualElectronVersion = electronPackageJson.version;
32+
expect(actualElectronVersion).toBeTruthy();
33+
34+
const isSatisfied = semver.satisfies(
35+
actualElectronVersion,
36+
declaredElectronVersion,
37+
);
38+
39+
expect(isSatisfied).toBe(true);
40+
});
41+
});

packages/debugger-shell/package.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "@react-native/debugger-shell",
3+
"version": "0.80.0-main",
4+
"description": "Experimental debugger shell for React Native for use with @react-native/debugger-frontend",
5+
"keywords": [
6+
"react-native",
7+
"tools"
8+
],
9+
"homepage": "https://github.com/facebook/react-native/tree/HEAD/packages/debugger-shell#readme",
10+
"bugs": "https://github.com/facebook/react-native/issues",
11+
"main": "./src/node/index.js",
12+
"exports": {
13+
"node": "./src/node/index.js",
14+
"electron": "./src/electron/index.js"
15+
},
16+
"scripts": {
17+
"dev": "electron src/electron"
18+
},
19+
"repository": {
20+
"type": "git",
21+
"url": "git+https://github.com/facebook/react-native.git",
22+
"directory": "packages/debugger-shell"
23+
},
24+
"license": "MIT",
25+
"engines": {
26+
"node": ">=18",
27+
"electron": ">=36.2.0"
28+
},
29+
"dependencies": {
30+
"cross-spawn": "^7.0.6",
31+
"electron": "36.2.0"
32+
},
33+
"devDependencies": {
34+
"semver": "^7.1.3"
35+
}
36+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
// $FlowFixMe[unclear-type] We have no Flow types for the Electron API.
12+
const {BrowserWindow, app, shell} = require('electron') as any;
13+
const util = require('util');
14+
15+
const windowMetadata = new WeakMap<
16+
typeof BrowserWindow,
17+
$ReadOnly<{
18+
windowKey: string,
19+
}>,
20+
>();
21+
22+
function handleLaunchArgs(argv: string[]) {
23+
const {
24+
values: {frontendUrl, windowKey},
25+
} = util.parseArgs({
26+
options: {
27+
frontendUrl: {
28+
type: 'string',
29+
},
30+
windowKey: {
31+
type: 'string',
32+
},
33+
},
34+
args: argv,
35+
});
36+
37+
// Find an existing window for this app and launch configuration.
38+
const existingWindow = BrowserWindow.getAllWindows().find(window => {
39+
const metadata = windowMetadata.get(window);
40+
if (!metadata) {
41+
return false;
42+
}
43+
return metadata.windowKey === windowKey;
44+
});
45+
46+
if (existingWindow) {
47+
// If the window is already visible, flash it.
48+
if (existingWindow.isVisible()) {
49+
existingWindow.flashFrame(true);
50+
setTimeout(() => {
51+
existingWindow.flashFrame(false);
52+
}, 1000);
53+
}
54+
if (process.platform === 'darwin') {
55+
app.focus({
56+
steal: true,
57+
});
58+
}
59+
existingWindow.focus();
60+
return;
61+
}
62+
63+
// Create the browser window.
64+
const frontendWindow = new BrowserWindow({
65+
width: 1200,
66+
height: 600,
67+
webPreferences: {
68+
partition: 'persist:react-native-devtools',
69+
},
70+
});
71+
72+
// Open links in the default browser instead of in new Electron windows.
73+
frontendWindow.webContents.setWindowOpenHandler(({url}) => {
74+
shell.openExternal(url);
75+
return {action: 'deny'};
76+
});
77+
78+
frontendWindow.loadURL(frontendUrl);
79+
80+
windowMetadata.set(frontendWindow, {
81+
windowKey,
82+
});
83+
84+
if (process.platform === 'darwin') {
85+
app.focus({
86+
steal: true,
87+
});
88+
}
89+
}
90+
91+
app.whenReady().then(() => {
92+
handleLaunchArgs(process.argv.slice(2));
93+
94+
app.on(
95+
'second-instance',
96+
(event, electronArgv, workingDirectory, additionalData) => {
97+
handleLaunchArgs(additionalData.argv);
98+
},
99+
);
100+
});
101+
102+
app.on('window-all-closed', function () {
103+
app.quit();
104+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
// $FlowFixMe[unclear-type] We have no Flow types for the Electron API.
12+
const {app} = require('electron') as any;
13+
14+
const gotTheLock = app.requestSingleInstanceLock({
15+
argv: process.argv.slice(2),
16+
});
17+
18+
if (!gotTheLock) {
19+
app.quit();
20+
} else {
21+
require('./MainInstanceEntryPoint.js');
22+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
* @format
9+
*/
10+
11+
/*::
12+
export type * from './index.flow';
13+
*/
14+
15+
if (!process.env.BUILD_EXCLUDE_BABEL_REGISTER) {
16+
require('../../../../scripts/babel-register').registerForMonorepo();
17+
}
18+
19+
module.exports = require('./index.flow');
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
const {spawn} = require('cross-spawn');
12+
13+
async function unstable_spawnDebuggerShellWithArgs(
14+
args: string[],
15+
{
16+
mode = 'detached',
17+
}: $ReadOnly<{
18+
// In 'syncAndExit' mode, the current process will block until the spawned process exits, and then it will exit
19+
// with the same exit code as the spawned process.
20+
// In 'detached' mode, the spawned process will be detached from the current process and the current process will
21+
// continue to run normally.
22+
mode?: 'syncThenExit' | 'detached',
23+
}> = {},
24+
): Promise<void> {
25+
// NOTE: Internally at Meta, this is aliased to a workspace that is
26+
// API-compatible with the 'electron' package, but contains prebuilt binaries
27+
// that do not need to be downloaded in a postinstall action.
28+
const electronPath = require('electron');
29+
30+
return new Promise((resolve, reject) => {
31+
const child = spawn(
32+
electronPath,
33+
[require.resolve('../electron'), ...args],
34+
{
35+
stdio: 'inherit',
36+
windowsHide: true,
37+
detached: mode === 'detached',
38+
},
39+
);
40+
if (mode === 'detached') {
41+
child.on('spawn', () => {
42+
resolve();
43+
});
44+
child.on('close', (code /*: number */) => {
45+
if (code !== 0) {
46+
reject(
47+
new Error(
48+
`Failed to open debugger shell: ${electronPath} exited with code ${code}`,
49+
),
50+
);
51+
}
52+
});
53+
child.unref();
54+
} else if (mode === 'syncThenExit') {
55+
child.on('close', function (code, signal) {
56+
if (code === null) {
57+
console.error(electronPath, 'exited with signal', signal);
58+
process.exit(1);
59+
}
60+
process.exit(code);
61+
});
62+
63+
const handleTerminationSignal = function (signal: string) {
64+
process.on(signal, function signalHandler() {
65+
if (!child.killed) {
66+
child.kill(signal);
67+
}
68+
});
69+
};
70+
71+
handleTerminationSignal('SIGINT');
72+
handleTerminationSignal('SIGTERM');
73+
}
74+
});
75+
}
76+
77+
export {unstable_spawnDebuggerShellWithArgs};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
* @format
9+
*/
10+
11+
/*::
12+
export type * from './index.flow';
13+
*/
14+
15+
if (!process.env.BUILD_EXCLUDE_BABEL_REGISTER) {
16+
require('../../../../scripts/babel-register').registerForMonorepo();
17+
}
18+
19+
export * from './index.flow';

scripts/build/config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ const buildConfig: BuildConfig = {
4545
emitTypeScriptDefs: true,
4646
target: 'node',
4747
},
48+
'debugger-shell': {
49+
emitTypeScriptDefs: true,
50+
target: 'node',
51+
},
4852
'dev-middleware': {
4953
emitTypeScriptDefs: true,
5054
target: 'node',

0 commit comments

Comments
 (0)