Restore Metro log streaming via CLI flag (#49356)

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
This commit is contained in:
Alex Hunt
2025-02-12 15:17:13 +00:00
committed by GitHub
parent 475f797a51
commit 969eb3f007
11 changed files with 77 additions and 38 deletions
+3 -2
View File
@@ -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 <path>` | Specify path to a custom SSL cert. |
| `--config <string>` | 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 <path> [options]
npx @react-native-community/cli bundle --entry-file <path> [options]
```
#### Options
@@ -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,
},
],
};
@@ -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);
@@ -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);
},
};
}
@@ -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;
}
}
@@ -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',
@@ -77,6 +77,9 @@ export type ReportableEvent =
| 'PROTOCOL_ERROR',
>,
}
| {
type: 'fusebox_console_notice',
}
| {
type: 'proxy_error',
status: 'error',
@@ -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',
-28
View File
@@ -26,7 +26,6 @@ let hmrUnavailableReason: string | null = null;
let currentCompileErrorMessage: string | null = null;
let didConnect: boolean = false;
let pendingLogs: Array<[LogLevel, $ReadOnlyArray<mixed>]> = [];
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;
}
@@ -25,7 +25,6 @@ const HMRClientProdShim: HMRClientNativeInterface = {
disable() {},
registerBundle() {},
log() {},
unstable_notifyFuseboxConsoleEnabled() {},
};
module.exports = HMRClientProdShim;
@@ -8942,7 +8942,6 @@ export type HMRClientNativeInterface = {|
isEnabled: boolean,
scheme?: string
): void,
unstable_notifyFuseboxConsoleEnabled(): void,
|};
declare const HMRClient: HMRClientNativeInterface;
declare module.exports: HMRClient;