diff --git a/packages/community-cli-plugin/package.json b/packages/community-cli-plugin/package.json index 2031b4bf70c8..4242bd5f5a6f 100644 --- a/packages/community-cli-plugin/package.json +++ b/packages/community-cli-plugin/package.json @@ -25,7 +25,7 @@ "@react-native/dev-middleware": "0.77.0-main", "@react-native/metro-babel-transformer": "0.77.0-main", "chalk": "^4.0.0", - "execa": "^5.1.1", + "invariant": "^2.2.4", "metro": "^0.81.0-alpha.2", "metro-config": "^0.81.0-alpha.2", "metro-core": "^0.81.0-alpha.2", diff --git a/packages/community-cli-plugin/src/commands/start/attachKeyHandlers.js b/packages/community-cli-plugin/src/commands/start/attachKeyHandlers.js index 7b5398d80397..8d76af29d456 100644 --- a/packages/community-cli-plugin/src/commands/start/attachKeyHandlers.js +++ b/packages/community-cli-plugin/src/commands/start/attachKeyHandlers.js @@ -12,11 +12,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 {spawn} from 'child_process'; +import invariant from 'invariant'; +import readline from 'readline'; +import {ReadStream} from 'tty'; const CTRL_C = '\u0003'; const CTRL_D = '\u0004'; @@ -33,6 +35,18 @@ const throttle = (callback: () => void, timeout: number) => { }; }; +type KeyEvent = { + sequence: string, + name: string, + ctrl: boolean, + meta: boolean, + shift: boolean, +}; + +const spawnOptions = { + env: {...process.env, FORCE_COLOR: chalk.supportsColor ? 'true' : 'false'}, +}; + export default function attachKeyHandlers({ cliConfig, devServerUrl, @@ -52,9 +66,8 @@ export default function attachKeyHandlers({ return; } - const execaOptions = { - env: {FORCE_COLOR: chalk.supportsColor ? 'true' : 'false'}, - }; + readline.emitKeypressEvents(process.stdin); + setRawMode(true); const reload = throttle(() => { logger.info('Reloading connected app(s)...'); @@ -66,12 +79,14 @@ export default function attachKeyHandlers({ devServerUrl, }); - const onPress = async (key: string) => { - if (openDebuggerKeyboardHandler.maybeHandleTargetSelection(key)) { + process.stdin.on('keypress', (str: string, key: KeyEvent) => { + logger.debug(`Key pressed: ${key.sequence}`); + + if (openDebuggerKeyboardHandler.maybeHandleTargetSelection(key.name)) { return; } - switch (key.toLowerCase()) { + switch (key.sequence) { case 'r': reload(); break; @@ -81,44 +96,42 @@ export default function attachKeyHandlers({ break; case 'i': logger.info('Opening app on iOS...'); - execa( + spawn( 'npx', [ 'react-native', 'run-ios', ...(cliConfig.project.ios?.watchModeCommandParams ?? []), ], - execaOptions, + spawnOptions, ).stdout?.pipe(process.stdout); break; case 'a': logger.info('Opening app on Android...'); - execa( + spawn( 'npx', [ 'react-native', 'run-android', ...(cliConfig.project.android?.watchModeCommandParams ?? []), ], - execaOptions, + spawnOptions, ).stdout?.pipe(process.stdout); break; case 'j': - await openDebuggerKeyboardHandler.handleOpenDebugger(); + // 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( [ @@ -132,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); +} diff --git a/packages/community-cli-plugin/src/utils/KeyPressHandler.js b/packages/community-cli-plugin/src/utils/KeyPressHandler.js deleted file mode 100644 index af3609f8da44..000000000000 --- a/packages/community-cli-plugin/src/utils/KeyPressHandler.js +++ /dev/null @@ -1,90 +0,0 @@ -/** - * 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 {CLIError} from './errors'; -import {logger} from './logger'; - -const CTRL_C = '\u0003'; - -/** An abstract key stroke interceptor. */ -export class KeyPressHandler { - _isInterceptingKeyStrokes = false; - _isHandlingKeyPress = false; - _onPress: (key: string) => Promise; - - constructor(onPress: (key: string) => Promise) { - this._onPress = onPress; - } - - /** Start observing interaction pause listeners. */ - createInteractionListener(): ({pause: boolean, ...}) => void { - // Support observing prompts. - let wasIntercepting = false; - - const listener = ({pause}: {pause: boolean, ...}) => { - if (pause) { - // Track if we were already intercepting key strokes before pausing, so we can - // resume after pausing. - wasIntercepting = this._isInterceptingKeyStrokes; - this.stopInterceptingKeyStrokes(); - } else if (wasIntercepting) { - // Only start if we were previously intercepting. - this.startInterceptingKeyStrokes(); - } - }; - - return listener; - } - - _handleKeypress = async (key: string): Promise => { - // Prevent sending another event until the previous event has finished. - if (this._isHandlingKeyPress && key !== CTRL_C) { - return; - } - this._isHandlingKeyPress = true; - try { - logger.debug(`Key pressed: ${key}`); - await this._onPress(key); - } catch (error) { - return new CLIError('There was an error with the key press handler.'); - } finally { - this._isHandlingKeyPress = false; - return; - } - }; - - /** Start intercepting all key strokes and passing them to the input `onPress` method. */ - startInterceptingKeyStrokes() { - if (this._isInterceptingKeyStrokes) { - return; - } - this._isInterceptingKeyStrokes = true; - const {stdin} = process; - // $FlowFixMe[prop-missing] - stdin.setRawMode(true); - stdin.resume(); - stdin.setEncoding('utf8'); - stdin.on('data', this._handleKeypress); - } - - /** Stop intercepting all key strokes. */ - stopInterceptingKeyStrokes() { - if (!this._isInterceptingKeyStrokes) { - return; - } - this._isInterceptingKeyStrokes = false; - const {stdin} = process; - stdin.removeListener('data', this._handleKeypress); - // $FlowFixMe[prop-missing] - stdin.setRawMode(false); - stdin.resume(); - } -} diff --git a/yarn.lock b/yarn.lock index cbe146be2bfe..97147991925d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4204,7 +4204,7 @@ eventemitter3@^5.0.1: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== -execa@^5.0.0, execa@^5.1.1: +execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==