From 453a2b84af9e507f149e3024f1f8c9979fb4056e Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Wed, 12 Feb 2025 04:13:41 -0800 Subject: [PATCH] Restore Metro log streaming via CLI flag Summary: This change adds an opt-in to restore JavaScript log streaming via the Metro dev server, [removed from React Native core in 0.77](https://reactnative.dev/blog/2025/01/21/version-0.77#removal-of-consolelog-streaming-in-metro). Users can opt into this legacy behaviour by adding the `--client-logs` flag to `npx react-native-community/cli start`. - The default experience remains without streamed JS logs. - The existing "JavaScript logs have moved! ..." notice is printed in all cases, and we do not advertise the new flag for new users. - Under non-Community CLI dev servers (i.e. Expo), log streaming is restored implicitly. We will clean up this functionality again when we eventually remove JS log streaming over `HMRClient`, tasked in T214991636. **Implementation notes** - Logs are always sent over `HMRClient` (previous status quo), even with log streaming off in the dev server. This is a necessary evil to be able to flag this functionality in a user-accessible place, and to move fast for 0.78. - Necessarily, emitting `fusebox_console_notice` moves to the dev server itself, on first device (Fusebox) connection. Changelog: [General][Added] - Add opt in for legacy Metro log streaming via `--client-logs` flag --- packages/community-cli-plugin/README.md | 5 ++- .../src/commands/start/index.js | 8 ++++ .../src/commands/start/runServer.js | 6 +++ .../dev-middleware/src/createDevMiddleware.js | 39 +++++++++++++++++-- .../src/inspector-proxy/Device.js | 13 +++++++ .../inspector-proxy/DeviceEventReporter.js | 6 +++ .../dev-middleware/src/types/EventReporter.js | 3 ++ .../Libraries/Core/setUpDeveloperTools.js | 5 +-- .../Libraries/Utilities/HMRClient.js | 28 ------------- .../Libraries/Utilities/HMRClientProdShim.js | 1 - .../__snapshots__/public-api-test.js.snap | 1 - 11 files changed, 77 insertions(+), 38 deletions(-) diff --git a/packages/community-cli-plugin/README.md b/packages/community-cli-plugin/README.md index 12fcffb9c026..dc4c687208c3 100644 --- a/packages/community-cli-plugin/README.md +++ b/packages/community-cli-plugin/README.md @@ -15,7 +15,7 @@ Start the React Native development server. #### Usage ```sh -npx react-native start [options] +npx @react-native-community/cli start [options] ``` #### Options @@ -37,6 +37,7 @@ npx react-native start [options] | `--cert ` | Specify path to a custom SSL cert. | | `--config ` | Path to the CLI configuration file. | | `--no-interactive` | Disable interactive mode. | +| `--client-logs` | **[Deprecated]** Enable plain text JavaScript log streaming for all connected apps. | ### `bundle` @@ -45,7 +46,7 @@ Build the bundle for the provided JavaScript entry file. #### Usage ```sh -npx react-native bundle --entry-file [options] +npx @react-native-community/cli bundle --entry-file [options] ``` #### Options diff --git a/packages/community-cli-plugin/src/commands/start/index.js b/packages/community-cli-plugin/src/commands/start/index.js index cd438b2115fe..7b5ba161dd3c 100644 --- a/packages/community-cli-plugin/src/commands/start/index.js +++ b/packages/community-cli-plugin/src/commands/start/index.js @@ -95,6 +95,14 @@ const startCommand: Command = { name: '--no-interactive', description: 'Disables interactive mode', }, + { + name: '--client-logs', + description: + '[Deprecated] Enable plain text JavaScript log streaming for all ' + + 'connected apps. This feature is deprecated and will be removed in ' + + 'future.', + default: false, + }, ], }; diff --git a/packages/community-cli-plugin/src/commands/start/runServer.js b/packages/community-cli-plugin/src/commands/start/runServer.js index ce4f7bcd9dea..186237dfa3d4 100644 --- a/packages/community-cli-plugin/src/commands/start/runServer.js +++ b/packages/community-cli-plugin/src/commands/start/runServer.js @@ -44,6 +44,7 @@ export type StartCommandArgs = { config?: string, projectRoot?: string, interactive: boolean, + clientLogs: boolean, }; async function runServer( @@ -96,6 +97,11 @@ async function runServer( require.resolve(plugin), ); } + // TODO(T214991636): Remove legacy Metro log forwarding + if (!args.clientLogs) { + // $FlowIgnore[cannot-write] Assigning to readonly property + metroConfig.server.forwardClientLogs = false; + } let reportEvent: (event: TerminalReportableEvent) => void; const terminal = new Terminal(process.stdout); diff --git a/packages/dev-middleware/src/createDevMiddleware.js b/packages/dev-middleware/src/createDevMiddleware.js index ac66be3fbcac..b22522666be1 100644 --- a/packages/dev-middleware/src/createDevMiddleware.js +++ b/packages/dev-middleware/src/createDevMiddleware.js @@ -11,7 +11,7 @@ import type {CreateCustomMessageHandlerFn} from './inspector-proxy/CustomMessageHandler'; import type {BrowserLauncher} from './types/BrowserLauncher'; -import type {EventReporter} from './types/EventReporter'; +import type {EventReporter, ReportableEvent} from './types/EventReporter'; import type {Experiments, ExperimentsConfig} from './types/Experiments'; import type {Logger} from './types/Logger'; import type {NextHandleFunction} from 'connect'; @@ -81,11 +81,15 @@ export default function createDevMiddleware({ unstable_customInspectorMessageHandler, }: Options): DevMiddlewareAPI { const experiments = getExperiments(experimentConfig); + const eventReporter = createWrappedEventReporter( + unstable_eventReporter, + logger, + ); const inspectorProxy = new InspectorProxy( projectRoot, serverBaseUrl, - unstable_eventReporter, + eventReporter, experiments, unstable_customInspectorMessageHandler, ); @@ -97,7 +101,7 @@ export default function createDevMiddleware({ serverBaseUrl, inspectorProxy, browserLauncher: unstable_browserLauncher, - eventReporter: unstable_eventReporter, + eventReporter, experiments, logger, }), @@ -129,3 +133,32 @@ function getExperiments(config: ExperimentsConfig): Experiments { enableNetworkInspector: config.enableNetworkInspector ?? false, }; } + +/** + * Creates a wrapped EventReporter that locally intercepts events to + * log to the terminal. + */ +function createWrappedEventReporter( + reporter: ?EventReporter, + logger: ?Logger, +): EventReporter { + return { + logEvent(event: ReportableEvent) { + switch (event.type) { + case 'fusebox_console_notice': + logger?.info( + '\n' + + '\u001B[7m' + + ' \u001B[1m💡 JavaScript logs have moved!\u001B[22m They can now be ' + + 'viewed in React Native DevTools. Tip: Type \u001B[1mj\u001B[22m in ' + + 'the terminal to open (requires Google Chrome or Microsoft Edge).' + + '\u001B[27m' + + '\n', + ); + break; + } + + reporter?.logEvent(event); + }, + }; +} diff --git a/packages/dev-middleware/src/inspector-proxy/Device.js b/packages/dev-middleware/src/inspector-proxy/Device.js index 88978fea4392..b4159110cadb 100644 --- a/packages/dev-middleware/src/inspector-proxy/Device.js +++ b/packages/dev-middleware/src/inspector-proxy/Device.js @@ -41,6 +41,8 @@ const PAGES_POLLING_INTERVAL = 1000; // more details. const FILE_PREFIX = 'file://'; +let fuseboxConsoleNoticeLogged = false; + type DebuggerConnection = { // Debugger web socket connection socket: WS, @@ -513,6 +515,7 @@ export default class Device { // created instead of manually checking this on every getPages result. for (const page of this.#pages.values()) { if (this.#pageHasCapability(page, 'nativePageReloads')) { + this.#logFuseboxConsoleNotice(); continue; } @@ -1067,4 +1070,14 @@ export default class Device { dangerouslyGetSocket(): WS { return this.#deviceSocket; } + + // TODO(T214991636): Remove notice + #logFuseboxConsoleNotice() { + if (fuseboxConsoleNoticeLogged) { + return; + } + + this.#deviceEventReporter?.logFuseboxConsoleNotice(); + fuseboxConsoleNoticeLogged = true; + } } diff --git a/packages/dev-middleware/src/inspector-proxy/DeviceEventReporter.js b/packages/dev-middleware/src/inspector-proxy/DeviceEventReporter.js index b210abf12efa..e6b77d4d44a1 100644 --- a/packages/dev-middleware/src/inspector-proxy/DeviceEventReporter.js +++ b/packages/dev-middleware/src/inspector-proxy/DeviceEventReporter.js @@ -212,6 +212,12 @@ class DeviceEventReporter { }); } + logFuseboxConsoleNotice(): void { + this.#eventReporter.logEvent({ + type: 'fusebox_console_notice', + }); + } + #logExpiredCommand(pendingCommand: PendingCommand): void { this.#eventReporter.logEvent({ type: 'debugger_command', diff --git a/packages/dev-middleware/src/types/EventReporter.js b/packages/dev-middleware/src/types/EventReporter.js index 8c4bc2b52aed..59cb9e6aedf8 100644 --- a/packages/dev-middleware/src/types/EventReporter.js +++ b/packages/dev-middleware/src/types/EventReporter.js @@ -77,6 +77,9 @@ export type ReportableEvent = | 'PROTOCOL_ERROR', >, } + | { + type: 'fusebox_console_notice', + } | { type: 'proxy_error', status: 'error', diff --git a/packages/react-native/Libraries/Core/setUpDeveloperTools.js b/packages/react-native/Libraries/Core/setUpDeveloperTools.js index 5cc39eae9c12..57f4f5916c26 100644 --- a/packages/react-native/Libraries/Core/setUpDeveloperTools.js +++ b/packages/react-native/Libraries/Core/setUpDeveloperTools.js @@ -42,9 +42,8 @@ if (__DEV__) { if (!Platform.isTesting) { const HMRClient = require('../Utilities/HMRClient'); - if (global.__FUSEBOX_HAS_FULL_CONSOLE_SUPPORT__) { - HMRClient.unstable_notifyFuseboxConsoleEnabled(); - } else if (console._isPolyfilled) { + // TODO(T214991636): Remove legacy Metro log forwarding + if (console._isPolyfilled) { // We assume full control over the console and send JavaScript logs to Metro. [ 'trace', diff --git a/packages/react-native/Libraries/Utilities/HMRClient.js b/packages/react-native/Libraries/Utilities/HMRClient.js index c7832f7bb469..9e715ddc19b3 100644 --- a/packages/react-native/Libraries/Utilities/HMRClient.js +++ b/packages/react-native/Libraries/Utilities/HMRClient.js @@ -26,7 +26,6 @@ let hmrUnavailableReason: string | null = null; let currentCompileErrorMessage: string | null = null; let didConnect: boolean = false; let pendingLogs: Array<[LogLevel, $ReadOnlyArray]> = []; -let pendingFuseboxConsoleNotification = false; type LogLevel = | 'trace' @@ -52,7 +51,6 @@ export type HMRClientNativeInterface = {| isEnabled: boolean, scheme?: string, ): void, - unstable_notifyFuseboxConsoleEnabled(): void, |}; /** @@ -142,29 +140,6 @@ const HMRClient: HMRClientNativeInterface = { } }, - unstable_notifyFuseboxConsoleEnabled() { - if (!hmrClient) { - pendingFuseboxConsoleNotification = true; - return; - } - hmrClient.send( - JSON.stringify({ - type: 'log', - level: 'info', - data: [ - '\n' + - '\u001B[7m' + - ' \u001B[1m💡 JavaScript logs have moved!\u001B[22m They can now be ' + - 'viewed in React Native DevTools. Tip: Type \u001B[1mj\u001B[22m in ' + - 'the terminal to open (requires Google Chrome or Microsoft Edge).' + - '\u001B[27m' + - '\n', - ], - }), - ); - pendingFuseboxConsoleNotification = false; - }, - // Called once by the bridge on startup, even if Fast Refresh is off. // It creates the HMR client but doesn't actually set up the socket yet. setup( @@ -341,9 +316,6 @@ function flushEarlyLogs(client: MetroHMRClient) { pendingLogs.forEach(([level, data]) => { HMRClient.log(level, data); }); - if (pendingFuseboxConsoleNotification) { - HMRClient.unstable_notifyFuseboxConsoleEnabled(); - } } finally { pendingLogs.length = 0; } diff --git a/packages/react-native/Libraries/Utilities/HMRClientProdShim.js b/packages/react-native/Libraries/Utilities/HMRClientProdShim.js index c4492b845454..4d36db2bc525 100644 --- a/packages/react-native/Libraries/Utilities/HMRClientProdShim.js +++ b/packages/react-native/Libraries/Utilities/HMRClientProdShim.js @@ -25,7 +25,6 @@ const HMRClientProdShim: HMRClientNativeInterface = { disable() {}, registerBundle() {}, log() {}, - unstable_notifyFuseboxConsoleEnabled() {}, }; module.exports = HMRClientProdShim; diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index 3eb7e7f8b8f3..f134c74246f6 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -8942,7 +8942,6 @@ export type HMRClientNativeInterface = {| isEnabled: boolean, scheme?: string ): void, - unstable_notifyFuseboxConsoleEnabled(): void, |}; declare const HMRClient: HMRClientNativeInterface; declare module.exports: HMRClient;