Skip to content

Commit c027406

Browse files
authored
[Flight] Prefix Replayed Console Logs with a Badge (#28403)
Builds on top of #28384. This prefixes each log with a badge similar to how we badge built-ins like "ForwardRef" and "Memo" in the React DevTools. The idea is that we can add such badges in DevTools for Server Components too to carry on the consistency. This puts the "environment" name in the badge which defaults to "Server". So you know which source it is coming from. We try to use the same styling as the React DevTools. We use light-dark mode where available to support the two different color styles, but if it's not available I use a fixed background so that it's always readable even in dark mode. In Terminals, instead of hard coding colors that might not look good with some themes, I use the ANSI color code to flip background/foreground colors in that case. In earlier commits I had it on the end of the line similar to the DevTools badges but for multiline I found it better to prefix it. We could try various options tough. In most cases we can use both ANSI and the `%c` CSS color specifier, because node will only use ANSI and hide the other. Chrome supports both but the color overrides ANSI if it comes later (and Chrome doesn't support color inverting anyway). Safari/Firefox prints the ANSI, so it can only use CSS colors. Therefore in browser builds I exclude ANSI. On the server I support both so if you use Chrome inspector on the server, you get nice colors on both terminal and in the inspector. Since Bun uses WebKit inspector and it prints the ANSI we can't safely emit both there. However, we also can't emit just the color specifier because then it prints in the terminal. oven-sh/bun#9021 So we just use a plain string prefix for now with a bracket until that's fixed. Screen shots: <img width="758" alt="Screenshot 2024-02-21 at 12 56 02 AM" src="https://github.com/facebook/react/assets/63648/4f887ffe-fffe-4402-bf2a-b7890986d60c"> <img width="759" alt="Screenshot 2024-02-21 at 12 56 24 AM" src="https://github.com/facebook/react/assets/63648/f32d432f-f738-4872-a700-ea0a78e6c745"> <img width="514" alt="Screenshot 2024-02-21 at 12 57 10 AM" src="https://github.com/facebook/react/assets/63648/205d2e82-75b7-4e2b-9d9c-aa9e2cbedf39"> <img width="489" alt="Screenshot 2024-02-21 at 12 57 34 AM" src="https://github.com/facebook/react/assets/63648/ea52d1e4-b9fa-431d-ae9e-ccb87631f399"> <img width="516" alt="Screenshot 2024-02-21 at 12 58 23 AM" src="https://github.com/facebook/react/assets/63648/52b50fac-bec0-471d-a457-1a10d8df9172"> <img width="956" alt="Screenshot 2024-02-21 at 12 58 56 AM" src="https://github.com/facebook/react/assets/63648/0096ed61-5eff-4aa9-8a8a-2204e754bd1f">
1 parent 9a5b6bd commit c027406

22 files changed

+323
-5
lines changed

packages/react-client/src/ReactFlightClient.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
readFinalStringChunk,
5151
createStringDecoder,
5252
prepareDestinationForModule,
53+
printToConsole,
5354
} from './ReactFlightClientConfig';
5455

5556
import {registerServerReference} from './ReactFlightReplyClient';
@@ -1094,13 +1095,13 @@ function resolveConsoleEntry(
10941095
);
10951096
}
10961097

1097-
const payload: [string, string, mixed] = parseModel(response, value);
1098+
const payload: [string, string, string, mixed] = parseModel(response, value);
10981099
const methodName = payload[0];
10991100
// TODO: Restore the fake stack before logging.
11001101
// const stackTrace = payload[1];
1101-
const args = payload.slice(2);
1102-
// eslint-disable-next-line react-internal/no-production-logging
1103-
console[methodName].apply(console, args);
1102+
const env = payload[2];
1103+
const args = payload.slice(3);
1104+
printToConsole(methodName, args, env);
11041105
}
11051106

11061107
function mergeBuffer(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
const badgeFormat = '%c%s%c ';
11+
// Same badge styling as DevTools.
12+
const badgeStyle =
13+
// We use a fixed background if light-dark is not supported, otherwise
14+
// we use a transparent background.
15+
'background: #e6e6e6;' +
16+
'background: light-dark(rgba(0,0,0,0.1), rgba(255,255,255,0.25));' +
17+
'color: #000000;' +
18+
'color: light-dark(#000000, #ffffff);' +
19+
'border-radius: 2px';
20+
const resetStyle = '';
21+
const pad = ' ';
22+
23+
export function printToConsole(
24+
methodName: string,
25+
args: Array<any>,
26+
badgeName: string,
27+
): void {
28+
let offset = 0;
29+
switch (methodName) {
30+
case 'dir':
31+
case 'dirxml':
32+
case 'groupEnd':
33+
case 'table': {
34+
// These methods cannot be colorized because they don't take a formatting string.
35+
// eslint-disable-next-line react-internal/no-production-logging
36+
console[methodName].apply(console, args);
37+
return;
38+
}
39+
case 'assert': {
40+
// assert takes formatting options as the second argument.
41+
offset = 1;
42+
}
43+
}
44+
45+
const newArgs = args.slice(0);
46+
if (typeof newArgs[offset] === 'string') {
47+
newArgs.splice(
48+
offset,
49+
1,
50+
badgeFormat + newArgs[offset],
51+
badgeStyle,
52+
pad + badgeName + pad,
53+
resetStyle,
54+
);
55+
} else {
56+
newArgs.splice(
57+
offset,
58+
0,
59+
badgeFormat,
60+
badgeStyle,
61+
pad + badgeName + pad,
62+
resetStyle,
63+
);
64+
}
65+
66+
// eslint-disable-next-line react-internal/no-production-logging
67+
console[methodName].apply(console, newArgs);
68+
return;
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
const badgeFormat = '[%s] ';
11+
const pad = ' ';
12+
13+
export function printToConsole(
14+
methodName: string,
15+
args: Array<any>,
16+
badgeName: string,
17+
): void {
18+
let offset = 0;
19+
switch (methodName) {
20+
case 'dir':
21+
case 'dirxml':
22+
case 'groupEnd':
23+
case 'table': {
24+
// These methods cannot be colorized because they don't take a formatting string.
25+
// eslint-disable-next-line react-internal/no-production-logging
26+
console[methodName].apply(console, args);
27+
return;
28+
}
29+
case 'assert': {
30+
// assert takes formatting options as the second argument.
31+
offset = 1;
32+
}
33+
}
34+
35+
const newArgs = args.slice(0);
36+
if (typeof newArgs[offset] === 'string') {
37+
newArgs.splice(
38+
offset,
39+
1,
40+
badgeFormat + newArgs[offset],
41+
pad + badgeName + pad,
42+
);
43+
} else {
44+
newArgs.splice(offset, 0, badgeFormat, pad + badgeName + pad);
45+
}
46+
47+
// eslint-disable-next-line react-internal/no-production-logging
48+
console[methodName].apply(console, newArgs);
49+
return;
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
// This flips color using ANSI, then sets a color styling, then resets.
11+
const badgeFormat = '\x1b[0m\x1b[7m%c%s\x1b[0m%c ';
12+
// Same badge styling as DevTools.
13+
const badgeStyle =
14+
// We use a fixed background if light-dark is not supported, otherwise
15+
// we use a transparent background.
16+
'background: #e6e6e6;' +
17+
'background: light-dark(rgba(0,0,0,0.1), rgba(255,255,255,0.25));' +
18+
'color: #000000;' +
19+
'color: light-dark(#000000, #ffffff);' +
20+
'border-radius: 2px';
21+
const resetStyle = '';
22+
const pad = ' ';
23+
24+
export function printToConsole(
25+
methodName: string,
26+
args: Array<any>,
27+
badgeName: string,
28+
): void {
29+
let offset = 0;
30+
switch (methodName) {
31+
case 'dir':
32+
case 'dirxml':
33+
case 'groupEnd':
34+
case 'table': {
35+
// These methods cannot be colorized because they don't take a formatting string.
36+
// eslint-disable-next-line react-internal/no-production-logging
37+
console[methodName].apply(console, args);
38+
return;
39+
}
40+
case 'assert': {
41+
// assert takes formatting options as the second argument.
42+
offset = 1;
43+
}
44+
}
45+
46+
const newArgs = args.slice(0);
47+
if (typeof newArgs[offset] === 'string') {
48+
newArgs.splice(
49+
offset,
50+
1,
51+
badgeFormat + newArgs[offset],
52+
badgeStyle,
53+
pad + badgeName + pad,
54+
resetStyle,
55+
);
56+
} else {
57+
newArgs.splice(
58+
offset,
59+
0,
60+
badgeFormat,
61+
badgeStyle,
62+
pad + badgeName + pad,
63+
resetStyle,
64+
);
65+
}
66+
67+
// eslint-disable-next-line react-internal/no-production-logging
68+
console[methodName].apply(console, newArgs);
69+
return;
70+
}

packages/react-client/src/forks/ReactFlightClientConfig.custom.js

+2
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,5 @@ export opaque type StringDecoder = mixed; // eslint-disable-line no-undef
4747
export const createStringDecoder = $$$config.createStringDecoder;
4848
export const readPartialStringChunk = $$$config.readPartialStringChunk;
4949
export const readFinalStringChunk = $$$config.readFinalStringChunk;
50+
51+
export const printToConsole = $$$config.printToConsole;

packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11+
export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser';
1112
export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM';
1213
export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser';
1314
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11+
export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser';
1112
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack';
1213
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser';
1314
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser';

packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11+
export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser';
1112
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
1213
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackBrowser';
1314
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackBrowser';

packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11+
export * from 'react-client/src/ReactFlightClientConsoleConfigPlain';
1112
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
1213

1314
export type Response = any;

packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11+
export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
1112
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack';
1213
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer';
1314
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer';

packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11+
export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
1112
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
1213
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer';
1314
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer';

packages/react-client/src/forks/ReactFlightClientConfig.dom-fb-experimental.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11+
export * from 'react-client/src/ReactFlightClientConsoleConfigPlain';
1112
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
1213
export * from 'react-server-dom-fb/src/ReactFlightClientConfigFBBundler';
1314

packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11+
export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser';
1112
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
1213

1314
export type Response = any;

packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
11+
export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
1112
export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM';
1213
export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer';
1314
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
11+
export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
1112
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack';
1213
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer';
1314
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer';

packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
11+
export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
1112
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode';
1213
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer';
1314
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
11+
export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
1112
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
1213
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer';
1314
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer';

packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
11+
export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
1112
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode';
1213
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer';
1314
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

packages/react-noop-renderer/src/ReactNoopFlightClient.js

+4
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ const {createResponse, processBinaryChunk, getRoot, close} = ReactFlightClient({
4343
parseModel(response: Response, json) {
4444
return JSON.parse(json, response._fromJSON);
4545
},
46+
printToConsole(methodName, args, badgeName) {
47+
// eslint-disable-next-line react-internal/no-production-logging
48+
console[methodName].apply(console, args);
49+
},
4650
});
4751

4852
function read<T>(source: Source): Thenable<T> {

packages/react-server/src/ReactFlightServer.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -2213,7 +2213,9 @@ function emitConsoleChunk(
22132213
}
22142214
}
22152215

2216-
const payload = [methodName, stackTrace];
2216+
// TODO: Don't double badge if this log came from another Flight Client.
2217+
const env = request.environmentName;
2218+
const payload = [methodName, stackTrace, env];
22172219
// $FlowFixMe[method-unbinding]
22182220
payload.push.apply(payload, args);
22192221
// $FlowFixMe[incompatible-type] stringify can return null

0 commit comments

Comments
 (0)