From 2d75b5082e12506b7936369ea9de6e312bbc8525 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Tue, 1 Oct 2024 14:19:55 -0700 Subject: [PATCH 1/3] Replace execa with child_process Differential Revision: D63255320 --- packages/community-cli-plugin/package.json | 1 - .../src/commands/start/attachKeyHandlers.js | 18 +++++++++--------- yarn.lock | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/community-cli-plugin/package.json b/packages/community-cli-plugin/package.json index 2031b4bf70c8..1f6322a68f06 100644 --- a/packages/community-cli-plugin/package.json +++ b/packages/community-cli-plugin/package.json @@ -25,7 +25,6 @@ "@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", "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..e290ea78fa12 100644 --- a/packages/community-cli-plugin/src/commands/start/attachKeyHandlers.js +++ b/packages/community-cli-plugin/src/commands/start/attachKeyHandlers.js @@ -16,7 +16,7 @@ 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'; const CTRL_C = '\u0003'; const CTRL_D = '\u0004'; @@ -33,6 +33,10 @@ const throttle = (callback: () => void, timeout: number) => { }; }; +const spawnOptions = { + env: {...process.env, FORCE_COLOR: chalk.supportsColor ? 'true' : 'false'}, +}; + export default function attachKeyHandlers({ cliConfig, devServerUrl, @@ -52,10 +56,6 @@ export default function attachKeyHandlers({ return; } - const execaOptions = { - env: {FORCE_COLOR: chalk.supportsColor ? 'true' : 'false'}, - }; - const reload = throttle(() => { logger.info('Reloading connected app(s)...'); messageSocket.broadcast('reload', null); @@ -81,26 +81,26 @@ 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': 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== From 54c5015d722857d5afab0e636d52484f478bc8b3 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Tue, 1 Oct 2024 14:19:55 -0700 Subject: [PATCH 2/3] Simplify key handling in start command Differential Revision: D63255321 --- packages/community-cli-plugin/package.json | 1 + .../src/commands/start/attachKeyHandlers.js | 43 ++++++--- .../src/utils/KeyPressHandler.js | 90 ------------------- 3 files changed, 33 insertions(+), 101 deletions(-) delete mode 100644 packages/community-cli-plugin/src/utils/KeyPressHandler.js diff --git a/packages/community-cli-plugin/package.json b/packages/community-cli-plugin/package.json index 1f6322a68f06..4242bd5f5a6f 100644 --- a/packages/community-cli-plugin/package.json +++ b/packages/community-cli-plugin/package.json @@ -25,6 +25,7 @@ "@react-native/dev-middleware": "0.77.0-main", "@react-native/metro-babel-transformer": "0.77.0-main", "chalk": "^4.0.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", diff --git a/packages/community-cli-plugin/src/commands/start/attachKeyHandlers.js b/packages/community-cli-plugin/src/commands/start/attachKeyHandlers.js index e290ea78fa12..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 {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,14 @@ 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'}, }; @@ -56,6 +66,9 @@ export default function attachKeyHandlers({ return; } + readline.emitKeypressEvents(process.stdin); + setRawMode(true); + const reload = throttle(() => { logger.info('Reloading connected app(s)...'); messageSocket.broadcast('reload', null); @@ -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; @@ -104,21 +119,19 @@ export default function attachKeyHandlers({ ).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(); - } -} From 5f68c5f7df6e236b56b3087d1892a89018c80b07 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Tue, 1 Oct 2024 14:35:11 -0700 Subject: [PATCH 3/3] Replace and remove optional dep on cli-tools logger Summary: (Further refactors to logging after D63255296.) Fully decouples `community-cli-plugin` from the unlisted optional dependency on `react-native-community/cli-tools'`. This is motivated by changes in https://github.com/facebook/react-native/pull/46627 which switch to using Metro's `TerminalReporter` API for emitting logs safely. - Swaps out logs in the dev server for the `unstable_server_log` Metro reporter event. - Swaps out `logger.debug()` calls for the `debug` package, currently used by Metro and `dev-middleware`. - Swaps out other logs in the `bundle` command for `console`. - (Also specify missing `semver` dep.) Changelog: [Internal] Reviewed By: hoxyq Differential Revision: D63328268 --- packages/community-cli-plugin/package.json | 4 +- .../src/commands/bundle/buildBundle.js | 9 +- .../src/commands/bundle/saveAssets.js | 16 +-- .../src/commands/start/attachKeyHandlers.js | 62 ++++++---- .../src/commands/start/middleware.js | 4 +- .../src/commands/start/runServer.js | 31 +++-- .../src/utils/createDevMiddlewareLogger.js | 44 +++++++ .../src/utils/loadMetroConfig.js | 7 +- .../community-cli-plugin/src/utils/logger.js | 112 ------------------ .../community-cli-plugin/src/utils/version.js | 44 +++---- 10 files changed, 149 insertions(+), 184 deletions(-) create mode 100644 packages/community-cli-plugin/src/utils/createDevMiddlewareLogger.js delete mode 100644 packages/community-cli-plugin/src/utils/logger.js diff --git a/packages/community-cli-plugin/package.json b/packages/community-cli-plugin/package.json index 4242bd5f5a6f..fed3df0a8bc9 100644 --- a/packages/community-cli-plugin/package.json +++ b/packages/community-cli-plugin/package.json @@ -25,12 +25,14 @@ "@react-native/dev-middleware": "0.77.0-main", "@react-native/metro-babel-transformer": "0.77.0-main", "chalk": "^4.0.0", + "debug": "^2.2.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" + "readline": "^1.3.0", + "semver": "^7.1.3" }, "devDependencies": { "metro-resolver": "^0.81.0-alpha.2" diff --git a/packages/community-cli-plugin/src/commands/bundle/buildBundle.js b/packages/community-cli-plugin/src/commands/bundle/buildBundle.js index c4446ea1c8eb..c18aa9bc06ca 100644 --- a/packages/community-cli-plugin/src/commands/bundle/buildBundle.js +++ b/packages/community-cli-plugin/src/commands/bundle/buildBundle.js @@ -14,7 +14,6 @@ import type {ConfigT} from 'metro-config'; import type {RequestOptions} from 'metro/src/shared/types.flow'; import loadMetroConfig from '../../utils/loadMetroConfig'; -import {logger} from '../../utils/logger'; import parseKeyValueParamArray from '../../utils/parseKeyValueParamArray'; import saveAssets from './saveAssets'; import chalk from 'chalk'; @@ -72,13 +71,13 @@ async function buildBundleWithConfig( ); if (config.resolver.platforms.indexOf(args.platform) === -1) { - logger.error( - `Invalid platform ${ + console.error( + `${chalk.red('error')}: Invalid platform ${ args.platform ? `"${chalk.bold(args.platform)}" ` : '' }selected.`, ); - logger.info( + console.info( `Available platforms are: ${config.resolver.platforms .map(x => `"${chalk.bold(x)}"`) .join( @@ -123,7 +122,7 @@ async function buildBundleWithConfig( // $FlowIgnore[incompatible-call] // $FlowIgnore[prop-missing] // $FlowIgnore[incompatible-exact] - await bundleImpl.save(bundle, args, logger.info); + await bundleImpl.save(bundle, args, console.info); // Save the assets of the bundle const outputAssets = await server.getAssets({ diff --git a/packages/community-cli-plugin/src/commands/bundle/saveAssets.js b/packages/community-cli-plugin/src/commands/bundle/saveAssets.js index a92e718182ad..cb40997c3223 100644 --- a/packages/community-cli-plugin/src/commands/bundle/saveAssets.js +++ b/packages/community-cli-plugin/src/commands/bundle/saveAssets.js @@ -11,7 +11,6 @@ import type {AssetData} from 'metro/src/Assets'; -import {logger} from '../../utils/logger'; import { cleanAssetCatalog, getImageSet, @@ -21,6 +20,7 @@ import { import filterPlatformAssetScales from './filterPlatformAssetScales'; import getAssetDestPathAndroid from './getAssetDestPathAndroid'; import getAssetDestPathIOS from './getAssetDestPathIOS'; +import chalk from 'chalk'; import fs from 'fs'; import path from 'path'; @@ -35,7 +35,7 @@ async function saveAssets( assetCatalogDest?: string, ): Promise { if (assetsDest == null) { - logger.warn('Assets destination folder is not set, skipping...'); + console.warn('Warning: Assets destination folder is not set, skipping...'); return; } @@ -64,13 +64,13 @@ async function saveAssets( // remove unused scales from the optimized bundle. const catalogDir = path.join(assetCatalogDest, 'RNAssets.xcassets'); if (!fs.existsSync(catalogDir)) { - logger.error( - `Could not find asset catalog 'RNAssets.xcassets' in ${assetCatalogDest}. Make sure to create it if it does not exist.`, + console.error( + `${chalk.red('error')}: Could not find asset catalog 'RNAssets.xcassets' in ${assetCatalogDest}. Make sure to create it if it does not exist.`, ); return; } - logger.info('Adding images to asset catalog', catalogDir); + console.info('Adding images to asset catalog', catalogDir); cleanAssetCatalog(catalogDir); for (const asset of assets) { if (isCatalogAsset(asset)) { @@ -84,7 +84,7 @@ async function saveAssets( addAssetToCopy(asset); } } - logger.info('Done adding images to asset catalog'); + console.info('Done adding images to asset catalog'); } else { assets.forEach(addAssetToCopy); } @@ -98,7 +98,7 @@ function copyAll(filesToCopy: CopiedFiles) { return Promise.resolve(); } - logger.info(`Copying ${queue.length} asset files`); + console.info(`Copying ${queue.length} asset files`); return new Promise((resolve, reject) => { const copyNext = (error?: Error) => { if (error) { @@ -106,7 +106,7 @@ function copyAll(filesToCopy: CopiedFiles) { return; } if (queue.length === 0) { - logger.info('Done copying assets'); + console.info('Done copying assets'); resolve(); } else { // queue.length === 0 is checked in previous branch, so this is string diff --git a/packages/community-cli-plugin/src/commands/start/attachKeyHandlers.js b/packages/community-cli-plugin/src/commands/start/attachKeyHandlers.js index 8d76af29d456..51fcf04e8f82 100644 --- a/packages/community-cli-plugin/src/commands/start/attachKeyHandlers.js +++ b/packages/community-cli-plugin/src/commands/start/attachKeyHandlers.js @@ -12,7 +12,6 @@ import type {Config} from '@react-native-community/cli-types'; import type TerminalReporter from 'metro/src/lib/TerminalReporter'; -import {logger} from '../../utils/logger'; import OpenDebuggerKeyboardHandler from './OpenDebuggerKeyboardHandler'; import chalk from 'chalk'; import {spawn} from 'child_process'; @@ -62,7 +61,11 @@ export default function attachKeyHandlers({ reporter: TerminalReporter, }) { if (process.stdin.isTTY !== true) { - logger.debug('Interactive mode is not supported in this environment'); + reporter.update({ + type: 'unstable_server_log', + level: 'info', + data: 'Interactive mode is not supported in this environment', + }); return; } @@ -70,7 +73,11 @@ export default function attachKeyHandlers({ setRawMode(true); const reload = throttle(() => { - logger.info('Reloading connected app(s)...'); + reporter.update({ + type: 'unstable_server_log', + level: 'info', + data: 'Reloading connected app(s)...', + }); messageSocket.broadcast('reload', null); }, RELOAD_TIMEOUT); @@ -80,8 +87,6 @@ export default function attachKeyHandlers({ }); process.stdin.on('keypress', (str: string, key: KeyEvent) => { - logger.debug(`Key pressed: ${key.sequence}`); - if (openDebuggerKeyboardHandler.maybeHandleTargetSelection(key.name)) { return; } @@ -91,11 +96,19 @@ export default function attachKeyHandlers({ reload(); break; case 'd': - logger.info('Opening Dev Menu...'); + reporter.update({ + type: 'unstable_server_log', + level: 'info', + data: 'Opening Dev Menu...', + }); messageSocket.broadcast('devMenu', null); break; case 'i': - logger.info('Opening app on iOS...'); + reporter.update({ + type: 'unstable_server_log', + level: 'info', + data: 'Opening app on iOS...', + }); spawn( 'npx', [ @@ -107,7 +120,11 @@ export default function attachKeyHandlers({ ).stdout?.pipe(process.stdout); break; case 'a': - logger.info('Opening app on Android...'); + reporter.update({ + type: 'unstable_server_log', + level: 'info', + data: 'Opening app on Android...', + }); spawn( 'npx', [ @@ -125,7 +142,11 @@ export default function attachKeyHandlers({ case CTRL_C: case CTRL_D: openDebuggerKeyboardHandler.dismiss(); - logger.info('Stopping server'); + reporter.update({ + type: 'unstable_server_log', + level: 'info', + data: 'Stopping server', + }); setRawMode(false); process.stdin.pause(); process.emit('SIGINT'); @@ -133,17 +154,18 @@ export default function attachKeyHandlers({ } }); - logger.log( - [ - '', - `${chalk.bold('i')} - run on iOS`, - `${chalk.bold('a')} - run on Android`, - `${chalk.bold('r')} - reload app`, - `${chalk.bold('d')} - open Dev Menu`, - `${chalk.bold('j')} - open DevTools`, - '', - ].join('\n'), - ); + reporter.update({ + type: 'unstable_server_log', + level: 'info', + data: `Key commands available: + + ${chalk.bold('i')} - run on iOS + ${chalk.bold('a')} - run on Android + ${chalk.bold('r')} - reload app(s) + ${chalk.bold('d')} - open Dev Menu + ${chalk.bold('j')} - open DevTools +`, + }); } function setRawMode(enable: boolean) { diff --git a/packages/community-cli-plugin/src/commands/start/middleware.js b/packages/community-cli-plugin/src/commands/start/middleware.js index bd2235c3c7f5..db6b46bad276 100644 --- a/packages/community-cli-plugin/src/commands/start/middleware.js +++ b/packages/community-cli-plugin/src/commands/start/middleware.js @@ -12,7 +12,7 @@ import type {NextHandleFunction, Server} from 'connect'; import type {TerminalReportableEvent} from 'metro/src/lib/TerminalReporter'; -import {logger} from '../../utils/logger'; +const debug = require('debug')('ReactNative:CommunityCliPlugin'); type MiddlewareReturn = { middleware: Server, @@ -71,7 +71,7 @@ try { communityMiddlewareFallback.createDevServerMiddleware = community.createDevServerMiddleware; } catch { - logger.debug(`⚠️ Unable to find @react-native-community/cli-server-api + debug(`⚠️ Unable to find @react-native-community/cli-server-api Starting the server without the community middleware.`); } diff --git a/packages/community-cli-plugin/src/commands/start/runServer.js b/packages/community-cli-plugin/src/commands/start/runServer.js index 60829c6adfbe..b233b5a46d3a 100644 --- a/packages/community-cli-plugin/src/commands/start/runServer.js +++ b/packages/community-cli-plugin/src/commands/start/runServer.js @@ -14,9 +14,9 @@ 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'; import * as version from '../../utils/version'; import attachKeyHandlers from './attachKeyHandlers'; import { @@ -72,24 +72,24 @@ async function runServer( const protocol = args.https === true ? 'https' : 'http'; const devServerUrl = url.format({protocol, hostname, port}); - logger.info(`Welcome to React Native v${ctx.reactNativeVersion}`); + console.info(`Welcome to React Native v${ctx.reactNativeVersion}`); const serverStatus = await isDevServerRunning(devServerUrl, projectRoot); if (serverStatus === 'matched_server_running') { - logger.info( + console.info( `A dev server is already running for this project on port ${port}. Exiting.`, ); return; } else if (serverStatus === 'port_taken') { - logger.error( - `Another process is running on port ${port}. Please terminate this ` + + console.error( + `${chalk.red('error')}: Another process is running on port ${port}. Please terminate this ` + 'process and try again, or use another port with "--port".', ); return; } - logger.info(`Starting dev server on port ${chalk.bold(String(port))}...`); + console.info(`Starting dev server on port ${chalk.bold(String(port))}...`); if (args.assetPlugins) { // $FlowIgnore[cannot-write] Assigning to readonly property @@ -98,6 +98,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, @@ -111,13 +116,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); @@ -125,7 +126,11 @@ async function runServer( reportEvent(event); } if (args.interactive && event.type === 'initialize_done') { - logger.info('Dev server ready'); + terminalReporter.update({ + type: 'unstable_server_log', + level: 'info', + data: 'Dev server ready', + }); attachKeyHandlers({ cliConfig: ctx, devServerUrl, @@ -168,7 +173,7 @@ async function runServer( // serverInstance.keepAliveTimeout = 30000; - await version.logIfUpdateAvailable(ctx.root); + await version.logIfUpdateAvailable(ctx.root, terminalReporter); } function getReporterImpl(customLogReporterPath?: string): TerminalReporter { diff --git a/packages/community-cli-plugin/src/utils/createDevMiddlewareLogger.js b/packages/community-cli-plugin/src/utils/createDevMiddlewareLogger.js new file mode 100644 index 000000000000..2bbc0487b231 --- /dev/null +++ b/packages/community-cli-plugin/src/utils/createDevMiddlewareLogger.js @@ -0,0 +1,44 @@ +/** + * 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'; + +type LoggerFn = (...message: $ReadOnlyArray) => void; + +/** + * Create a dev-middleware logger object that will emit logs via Metro's + * terminal reporter. + */ +export default function createDevMiddlewareLogger( + reporter: TerminalReporter, +): $ReadOnly<{ + info: LoggerFn, + error: LoggerFn, + warn: LoggerFn, +}> { + return { + info: makeLogger(reporter, 'info'), + warn: makeLogger(reporter, 'warn'), + error: makeLogger(reporter, 'error'), + }; +} + +function makeLogger( + reporter: TerminalReporter, + level: 'info' | 'warn' | 'error', +): LoggerFn { + return (...data: Array) => + reporter.update({ + type: 'unstable_server_log', + level, + data, + }); +} diff --git a/packages/community-cli-plugin/src/utils/loadMetroConfig.js b/packages/community-cli-plugin/src/utils/loadMetroConfig.js index 3d116c4946b6..6a6c7a4b7f5a 100644 --- a/packages/community-cli-plugin/src/utils/loadMetroConfig.js +++ b/packages/community-cli-plugin/src/utils/loadMetroConfig.js @@ -13,11 +13,12 @@ import type {Config} from '@react-native-community/cli-types'; import type {ConfigT, InputConfigT, YargArguments} from 'metro-config'; import {CLIError} from './errors'; -import {logger} from './logger'; import {reactNativePlatformResolver} from './metroPlatformResolver'; import {loadConfig, mergeConfig, resolveConfig} from 'metro-config'; import path from 'path'; +const debug = require('debug')('ReactNative:CommunityCliPlugin'); + export type {Config}; export type ConfigLoadingContext = $ReadOnly<{ @@ -92,7 +93,7 @@ export default async function loadMetroConfig( throw new CLIError(`No Metro config found in ${cwd}`); } - logger.debug(`Reading Metro config from ${projectConfig.filepath}`); + debug(`Reading Metro config from ${projectConfig.filepath}`); if (!global.__REACT_NATIVE_METRO_CONFIG_LOADED) { const warning = ` @@ -105,7 +106,7 @@ This warning will be removed in future (https://github.com/facebook/metro/issues `; for (const line of warning.trim().split('\n')) { - logger.warn(line); + console.warn(line); } } diff --git a/packages/community-cli-plugin/src/utils/logger.js b/packages/community-cli-plugin/src/utils/logger.js deleted file mode 100644 index 6b30a5287ebe..000000000000 --- a/packages/community-cli-plugin/src/utils/logger.js +++ /dev/null @@ -1,112 +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 chalk from 'chalk'; - -const SEPARATOR = ', '; - -let verbose: boolean = process.argv.includes('--verbose'); -let disabled: boolean = false; -let hidden: boolean = false; - -const formatMessages = (messages: Array) => - chalk.reset(messages.join(SEPARATOR)); - -const success = (...messages: Array) => { - if (!disabled) { - console.log(`${chalk.green.bold('success')} ${formatMessages(messages)}`); - } -}; - -const info = (...messages: Array) => { - if (!disabled) { - console.log(`${chalk.cyan.bold('info')} ${formatMessages(messages)}`); - } -}; - -const warn = (...messages: Array) => { - if (!disabled) { - console.warn(`${chalk.yellow.bold('warn')} ${formatMessages(messages)}`); - } -}; - -const error = (...messages: Array) => { - if (!disabled) { - console.error(`${chalk.red.bold('error')} ${formatMessages(messages)}`); - } -}; - -const debug = (...messages: Array) => { - if (verbose && !disabled) { - console.log(`${chalk.gray.bold('debug')} ${formatMessages(messages)}`); - } else { - hidden = true; - } -}; - -const log = (...messages: Array) => { - if (!disabled) { - console.log(`${formatMessages(messages)}`); - } -}; - -const setVerbose = (level: boolean) => { - verbose = level; -}; - -const isVerbose = (): boolean => verbose; - -const disable = () => { - disabled = true; -}; - -const enable = () => { - disabled = false; -}; - -const hasDebugMessages = (): boolean => hidden; - -let communityLogger; -try { - const {logger} = require('@react-native-community/cli-tools'); - logger.debug("Using @react-naive-community/cli-tools' logger"); - communityLogger = logger; -} catch { - // This is no longer a required dependency in react-native projects, but use it instead of - // our forked version if it's available. Fail silently otherwise. -} - -type Logger = $ReadOnly<{ - debug: (...message: Array) => void, - error: (...message: Array) => void, - log: (...message: Array) => void, - info: (...message: Array) => void, - warn: (...message: Array) => void, - ... -}>; - -export const logger: Logger = communityLogger ?? { - success, - info, - warn, - error, - debug, - log, - setVerbose, - isVerbose, - hasDebugMessages, - disable, - enable, -}; - -if (communityLogger == null) { - logger.debug("Using @react-native/communityu-cli-plugin's logger"); -} diff --git a/packages/community-cli-plugin/src/utils/version.js b/packages/community-cli-plugin/src/utils/version.js index bfbefc799c9a..ff278a7ab3c5 100644 --- a/packages/community-cli-plugin/src/utils/version.js +++ b/packages/community-cli-plugin/src/utils/version.js @@ -9,12 +9,15 @@ * @oncall react_native */ -import {logger} from './logger'; +import type TerminalReporter from 'metro/src/lib/TerminalReporter'; + import chalk from 'chalk'; import {readFileSync} from 'fs'; import path from 'path'; import semver from 'semver'; +const debug = require('debug')('ReactNative:CommunityCliPlugin'); + type Release = { // The current stable release stable: string, @@ -59,12 +62,10 @@ function getReactNativeVersion(projectRoot: string): string | void { const resolvedPath: string = require.resolve('react-native/package.json', { paths: [projectRoot], }); - logger.debug( - `Found 'react-native' from '${projectRoot}' -> '${resolvedPath}'`, - ); + debug(`Found 'react-native' from '${projectRoot}' -> '${resolvedPath}'`); return JSON.parse(readFileSync(resolvedPath, 'utf8')).version; } catch { - logger.debug("Couldn't read the version of 'react-native'"); + debug("Couldn't read the version of 'react-native'"); return; } } @@ -72,18 +73,23 @@ function getReactNativeVersion(projectRoot: string): string | void { /** * Logs out a message if the user's version is behind a stable version of React Native */ -export async function logIfUpdateAvailable(projectRoot: string): Promise { +export async function logIfUpdateAvailable( + projectRoot: string, + reporter: TerminalReporter, +): Promise { const versions = await latest(projectRoot); if (!versions?.upgrade) { return; } if (semver.gt(versions.upgrade.stable, versions.current)) { - logger.info( - `React Native v${versions.upgrade.stable} is now available (your project is running on v${versions.name}). + reporter.update({ + type: 'unstable_server_log', + level: 'info', + data: `React Native v${versions.upgrade.stable} is now available (your project is running on v${versions.name}). Changelog: ${chalk.dim.underline(versions.upgrade?.changelogUrl ?? 'none')} Diff: ${chalk.dim.underline(versions.upgrade?.diffUrl ?? 'none')} `, - ); + }); } } @@ -111,11 +117,11 @@ async function latest(projectRoot: string): Promise { } catch (e) { // We let the flow continue as this component is not vital for the rest of // the CLI. - logger.debug( + debug( 'Cannot detect current version of React Native, ' + 'skipping check for a newer release', ); - logger.debug(e); + debug(e); } } @@ -139,9 +145,9 @@ export default async function getLatestRelease( name: string, currentVersion: string, ): Promise { - logger.debug('Checking for a newer version of React Native'); + debug('Checking for a newer version of React Native'); try { - logger.debug(`Current version: ${currentVersion}`); + debug(`Current version: ${currentVersion}`); // if the version is a nightly/canary build, we want to bail // since they are nightlies or unreleased versions @@ -149,14 +155,14 @@ export default async function getLatestRelease( return; } - logger.debug('Checking for newer releases on GitHub'); + debug('Checking for newer releases on GitHub'); const latestVersion = await getLatestRnDiffPurgeVersion(name); if (latestVersion == null) { - logger.debug('Failed to get latest release'); + debug('Failed to get latest release'); return; } const {stable, candidate} = latestVersion; - logger.debug(`Latest release: ${stable} (${candidate ?? ''})`); + debug(`Latest release: ${stable} (${candidate ?? ''})`); if (semver.compare(stable, currentVersion) >= 0) { return { @@ -167,10 +173,8 @@ export default async function getLatestRelease( }; } } catch (e) { - logger.debug( - 'Something went wrong with remote version checking, moving on', - ); - logger.debug(e); + debug('Something went wrong with remote version checking, moving on'); + debug(e); } }