Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@
"jest": "^29.6.3",
"jest-junit": "^10.0.0",
"jscodeshift": "^0.14.0",
"metro-babel-register": "^0.81.0-alpha.0",
"metro-memory-fs": "^0.81.0-alpha.0",
"metro-babel-register": "^0.81.0-alpha.2",
"metro-memory-fs": "^0.81.0-alpha.2",
"micromatch": "^4.0.4",
"mkdirp": "^0.5.1",
"node-fetch": "^2.2.0",
Expand Down
9 changes: 5 additions & 4 deletions packages/community-cli-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@
"@react-native/metro-babel-transformer": "0.76.0-rc.2",
"chalk": "^4.0.0",
"execa": "^5.1.1",
"metro": "^0.81.0-alpha.0",
"metro-config": "^0.81.0-alpha.0",
"metro-core": "^0.81.0-alpha.0",
"invariant": "^2.2.4",
"metro": "^0.81.0-alpha.2",
"metro-config": "^0.81.0-alpha.2",
"metro-core": "^0.81.0-alpha.2",
"node-fetch": "^2.2.0",
"readline": "^1.3.0"
},
"devDependencies": {
"metro-resolver": "^0.81.0-alpha.0"
"metro-resolver": "^0.81.0-alpha.2"
},
"peerDependencies": {
"@react-native-community/cli-server-api": "*"
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@
*/

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';
import invariant from 'invariant';
import readline from 'readline';
import {ReadStream} from 'tty';

const CTRL_C = '\u0003';
const CTRL_D = '\u0004';
Expand All @@ -32,23 +35,36 @@ const throttle = (callback: () => void, timeout: number) => {
};
};

type KeyEvent = {
sequence: string,
name: string,
ctrl: boolean,
meta: boolean,
shift: boolean,
};

export default function attachKeyHandlers({
cliConfig,
devServerUrl,
messageSocket,
reporter,
}: {
cliConfig: Config,
devServerUrl: string,
messageSocket: $ReadOnly<{
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');
return;
}

readline.emitKeypressEvents(process.stdin);
setRawMode(true);

const execaOptions = {
env: {FORCE_COLOR: chalk.supportsColor ? 'true' : 'false'},
};
Expand All @@ -58,8 +74,19 @@ export default function attachKeyHandlers({
messageSocket.broadcast('reload', null);
}, RELOAD_TIMEOUT);

const onPress = async (key: string) => {
switch (key.toLowerCase()) {
const openDebuggerKeyboardHandler = new OpenDebuggerKeyboardHandler({
reporter,
devServerUrl,
});

process.stdin.on('keypress', (str: string, key: KeyEvent) => {
logger.debug(`Key pressed: ${key.sequence}`);

if (openDebuggerKeyboardHandler.maybeHandleTargetSelection(key.name)) {
return;
}

switch (key.sequence) {
case 'r':
reload();
break;
Expand Down Expand Up @@ -92,21 +119,19 @@ export default function attachKeyHandlers({
).stdout?.pipe(process.stdout);
break;
case 'j':
// TODO(T192878199): Add multi-target selection
await fetch(devServerUrl + '/open-debugger', {method: 'POST'});
// eslint-disable-next-line no-void
void openDebuggerKeyboardHandler.handleOpenDebugger();
break;
case CTRL_C:
case CTRL_D:
openDebuggerKeyboardHandler.dismiss();
logger.info('Stopping server');
keyPressHandler.stopInterceptingKeyStrokes();
setRawMode(false);
process.stdin.pause();
process.emit('SIGINT');
process.exit();
}
};

const keyPressHandler = new KeyPressHandler(onPress);
keyPressHandler.createInteractionListener();
keyPressHandler.startInterceptingKeyStrokes();
});

logger.log(
[
Expand All @@ -120,3 +145,11 @@ export default function attachKeyHandlers({
].join('\n'),
);
}

function setRawMode(enable: boolean) {
invariant(
process.stdin instanceof ReadStream,
'process.stdin must be a readable stream to modify raw mode',
);
process.stdin.setRawMode(enable);
}
13 changes: 8 additions & 5 deletions packages/community-cli-plugin/src/commands/start/runServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {Reporter} from 'metro/src/lib/reporting';
import type {TerminalReportableEvent} from 'metro/src/lib/TerminalReporter';
import typeof TerminalReporter from 'metro/src/lib/TerminalReporter';

import createDevMiddlewareLogger from '../../utils/createDevMiddlewareLogger';
import isDevServerRunning from '../../utils/isDevServerRunning';
import loadMetroConfig from '../../utils/loadMetroConfig';
import {logger} from '../../utils/logger';
Expand Down Expand Up @@ -98,6 +99,11 @@ async function runServer(
);
}

let reportEvent: (event: TerminalReportableEvent) => void;
const terminal = new Terminal(process.stdout);
const ReporterImpl = getReporterImpl(args.customLogReporterPath);
const terminalReporter = new ReporterImpl(terminal);

const {
middleware: communityMiddleware,
websocketEndpoints: communityWebsocketEndpoints,
Expand All @@ -111,13 +117,9 @@ async function runServer(
const {middleware, websocketEndpoints} = createDevMiddleware({
projectRoot,
serverBaseUrl: devServerUrl,
logger,
logger: createDevMiddlewareLogger(terminalReporter),
});

let reportEvent: (event: TerminalReportableEvent) => void;
const terminal = new Terminal(process.stdout);
const ReporterImpl = getReporterImpl(args.customLogReporterPath);
const terminalReporter = new ReporterImpl(terminal);
const reporter: Reporter = {
update(event: TerminalReportableEvent) {
terminalReporter.update(event);
Expand All @@ -130,6 +132,7 @@ async function runServer(
cliConfig: ctx,
devServerUrl,
messageSocket: messageSocketEndpoint,
reporter: terminalReporter,
});
}
},
Expand Down
Loading