mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
[0.76] Add CLI selection of multiple debug targets (#46644)
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* 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 TerminalReporter from 'metro/src/lib/TerminalReporter';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
type PageDescription = $ReadOnly<{
|
||||
id: string,
|
||||
title: string,
|
||||
description: string,
|
||||
deviceName: string,
|
||||
...
|
||||
}>;
|
||||
|
||||
export default class OpenDebuggerKeyboardHandler {
|
||||
#devServerUrl: string;
|
||||
#reporter: TerminalReporter;
|
||||
#targetsShownForSelection: ?$ReadOnlyArray<PageDescription> = null;
|
||||
|
||||
constructor({
|
||||
devServerUrl,
|
||||
reporter,
|
||||
}: {
|
||||
devServerUrl: string,
|
||||
reporter: TerminalReporter,
|
||||
}) {
|
||||
this.#devServerUrl = devServerUrl;
|
||||
this.#reporter = reporter;
|
||||
}
|
||||
|
||||
async #tryOpenDebuggerForTarget(target: PageDescription): Promise<void> {
|
||||
this.#targetsShownForSelection = null;
|
||||
this.#clearTerminalMenu();
|
||||
|
||||
try {
|
||||
await fetch(
|
||||
new URL(
|
||||
'/open-debugger?target=' + encodeURIComponent(target.id),
|
||||
this.#devServerUrl,
|
||||
).href,
|
||||
{method: 'POST'},
|
||||
);
|
||||
} catch (e) {
|
||||
this.#log(
|
||||
'error',
|
||||
'Failed to open debugger for %s on %s debug targets: %s',
|
||||
target.description,
|
||||
target.deviceName,
|
||||
e.message,
|
||||
);
|
||||
this.#clearTerminalMenu();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in response to 'j' to debug - fetch the available debug targets and:
|
||||
* - If no targets, warn
|
||||
* - If one target, open it
|
||||
* - If more, show a list. The keyboard listener should run subsequent key
|
||||
* presses through maybeHandleTargetSelection, which will launch the
|
||||
* debugger if a match is made.
|
||||
*/
|
||||
async handleOpenDebugger(): Promise<void> {
|
||||
this.#setTerminalMenu('Fetching available debugging targets...');
|
||||
this.#targetsShownForSelection = null;
|
||||
|
||||
try {
|
||||
const res = await fetch(this.#devServerUrl + '/json/list', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`Unexpected status code: ${res.status}`);
|
||||
}
|
||||
const targets = (await res.json()) as $ReadOnlyArray<PageDescription>;
|
||||
if (!Array.isArray(targets)) {
|
||||
throw new Error('Expected array.');
|
||||
}
|
||||
|
||||
if (targets.length === 0) {
|
||||
this.#log('warn', 'No connected targets');
|
||||
this.#clearTerminalMenu();
|
||||
} else if (targets.length === 1) {
|
||||
const target = targets[0];
|
||||
// eslint-disable-next-line no-void
|
||||
void this.#tryOpenDebuggerForTarget(target);
|
||||
} else {
|
||||
this.#targetsShownForSelection = targets;
|
||||
|
||||
if (targets.length > 9) {
|
||||
this.#log(
|
||||
'warn',
|
||||
'10 or more debug targets available, showing the first 9.',
|
||||
);
|
||||
}
|
||||
|
||||
this.#setTerminalMenu(
|
||||
`Multiple debug targets available, please select:\n ${targets
|
||||
.slice(0, 9)
|
||||
.map(
|
||||
({description, deviceName}, i) =>
|
||||
` ${chalk.white.inverse(` ${i + 1} `)} - "${description}" on "${deviceName}"`,
|
||||
)
|
||||
.join('\n ')}`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.#log('error', `Failed to fetch debug targets: ${e.message}`);
|
||||
this.#clearTerminalMenu();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle key presses that correspond to a valid selection from a visible
|
||||
* selection list.
|
||||
*
|
||||
* @return true if we've handled the key as a target selection, false if the
|
||||
* caller should handle the key.
|
||||
*/
|
||||
maybeHandleTargetSelection(keyName: string): boolean {
|
||||
if (keyName >= '1' && keyName <= '9') {
|
||||
const targetIndex = Number(keyName) - 1;
|
||||
if (
|
||||
this.#targetsShownForSelection != null &&
|
||||
targetIndex < this.#targetsShownForSelection.length
|
||||
) {
|
||||
const target = this.#targetsShownForSelection[targetIndex];
|
||||
// eslint-disable-next-line no-void
|
||||
void this.#tryOpenDebuggerForTarget(target);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss any target selection UI, if shown.
|
||||
*/
|
||||
dismiss() {
|
||||
this.#clearTerminalMenu();
|
||||
this.#targetsShownForSelection = null;
|
||||
}
|
||||
|
||||
#log(level: 'info' | 'warn' | 'error', ...data: Array<mixed>): void {
|
||||
this.#reporter.update({
|
||||
type: 'unstable_server_log',
|
||||
level,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
#setTerminalMenu(message: string) {
|
||||
this.#reporter.update({
|
||||
type: 'unstable_server_menu_updated',
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
#clearTerminalMenu() {
|
||||
this.#reporter.update({
|
||||
type: 'unstable_server_menu_cleared',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,13 @@
|
||||
*/
|
||||
|
||||
import type {Config} from '@react-native-community/cli-types';
|
||||
import type TerminalReporter from 'metro/src/lib/TerminalReporter';
|
||||
|
||||
import {KeyPressHandler} from '../../utils/KeyPressHandler';
|
||||
import {logger} from '../../utils/logger';
|
||||
import OpenDebuggerKeyboardHandler from './OpenDebuggerKeyboardHandler';
|
||||
import chalk from 'chalk';
|
||||
import execa from 'execa';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
const CTRL_C = '\u0003';
|
||||
const CTRL_D = '\u0004';
|
||||
@@ -36,6 +37,7 @@ export default function attachKeyHandlers({
|
||||
cliConfig,
|
||||
devServerUrl,
|
||||
messageSocket,
|
||||
reporter,
|
||||
}: {
|
||||
cliConfig: Config,
|
||||
devServerUrl: string,
|
||||
@@ -43,6 +45,7 @@ export default function attachKeyHandlers({
|
||||
broadcast: (type: string, params?: Record<string, mixed> | null) => void,
|
||||
...
|
||||
}>,
|
||||
reporter: TerminalReporter,
|
||||
}) {
|
||||
if (process.stdin.isTTY !== true) {
|
||||
logger.debug('Interactive mode is not supported in this environment');
|
||||
@@ -58,7 +61,16 @@ export default function attachKeyHandlers({
|
||||
messageSocket.broadcast('reload', null);
|
||||
}, RELOAD_TIMEOUT);
|
||||
|
||||
const openDebuggerKeyboardHandler = new OpenDebuggerKeyboardHandler({
|
||||
reporter,
|
||||
devServerUrl,
|
||||
});
|
||||
|
||||
const onPress = async (key: string) => {
|
||||
if (openDebuggerKeyboardHandler.maybeHandleTargetSelection(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (key.toLowerCase()) {
|
||||
case 'r':
|
||||
reload();
|
||||
@@ -92,11 +104,11 @@ export default function attachKeyHandlers({
|
||||
).stdout?.pipe(process.stdout);
|
||||
break;
|
||||
case 'j':
|
||||
// TODO(T192878199): Add multi-target selection
|
||||
await fetch(devServerUrl + '/open-debugger', {method: 'POST'});
|
||||
await openDebuggerKeyboardHandler.handleOpenDebugger();
|
||||
break;
|
||||
case CTRL_C:
|
||||
case CTRL_D:
|
||||
openDebuggerKeyboardHandler.dismiss();
|
||||
logger.info('Stopping server');
|
||||
keyPressHandler.stopInterceptingKeyStrokes();
|
||||
process.emit('SIGINT');
|
||||
|
||||
@@ -130,6 +130,7 @@ async function runServer(
|
||||
cliConfig: ctx,
|
||||
devServerUrl,
|
||||
messageSocket: messageSocketEndpoint,
|
||||
reporter: terminalReporter,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user