Skip to content

Commit 62da24a

Browse files
committed
Emit debug info for a Server Component
1 parent a1ace9d commit 62da24a

File tree

10 files changed

+228
-11
lines changed

10 files changed

+228
-11
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,53 +76,63 @@ const RESOLVED_MODULE = 'resolved_module';
7676
const INITIALIZED = 'fulfilled';
7777
const ERRORED = 'rejected';
7878

79+
// Dev-only
80+
type ReactDebugInfo = Array<{+name?: string}>;
81+
7982
type PendingChunk<T> = {
8083
status: 'pending',
8184
value: null | Array<(T) => mixed>,
8285
reason: null | Array<(mixed) => mixed>,
8386
_response: Response,
87+
_debugInfo?: null | ReactDebugInfo,
8488
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
8589
};
8690
type BlockedChunk<T> = {
8791
status: 'blocked',
8892
value: null | Array<(T) => mixed>,
8993
reason: null | Array<(mixed) => mixed>,
9094
_response: Response,
95+
_debugInfo?: null | ReactDebugInfo,
9196
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
9297
};
9398
type CyclicChunk<T> = {
9499
status: 'cyclic',
95100
value: null | Array<(T) => mixed>,
96101
reason: null | Array<(mixed) => mixed>,
97102
_response: Response,
103+
_debugInfo?: null | ReactDebugInfo,
98104
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
99105
};
100106
type ResolvedModelChunk<T> = {
101107
status: 'resolved_model',
102108
value: UninitializedModel,
103109
reason: null,
104110
_response: Response,
111+
_debugInfo?: null | ReactDebugInfo,
105112
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
106113
};
107114
type ResolvedModuleChunk<T> = {
108115
status: 'resolved_module',
109116
value: ClientReference<T>,
110117
reason: null,
111118
_response: Response,
119+
_debugInfo?: null | ReactDebugInfo,
112120
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
113121
};
114122
type InitializedChunk<T> = {
115123
status: 'fulfilled',
116124
value: T,
117125
reason: null,
118126
_response: Response,
127+
_debugInfo?: null | ReactDebugInfo,
119128
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
120129
};
121130
type ErroredChunk<T> = {
122131
status: 'rejected',
123132
value: null,
124133
reason: mixed,
125134
_response: Response,
135+
_debugInfo?: null | ReactDebugInfo,
126136
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
127137
};
128138
type SomeChunk<T> =
@@ -140,6 +150,9 @@ function Chunk(status: any, value: any, reason: any, response: Response) {
140150
this.value = value;
141151
this.reason = reason;
142152
this._response = response;
153+
if (__DEV__) {
154+
this._debugInfo = null;
155+
}
143156
}
144157
// We subclass Promise.prototype so that we get other methods like .catch
145158
Chunk.prototype = (Object.create(Promise.prototype): any);
@@ -475,6 +488,14 @@ function createElement(
475488
writable: true,
476489
value: true, // This element has already been validated on the server.
477490
});
491+
// debugInfo contains Server Component debug information.
492+
Object.defineProperty(element, '_debugInfo', {
493+
configurable: false,
494+
enumerable: false,
495+
writable: true,
496+
value: null,
497+
});
498+
// self and source are DEV only properties.
478499
Object.defineProperty(element, '_self', {
479500
configurable: false,
480501
enumerable: false,
@@ -499,6 +520,12 @@ function createLazyChunkWrapper<T>(
499520
_payload: chunk,
500521
_init: readChunk,
501522
};
523+
if (__DEV__) {
524+
// Ensure we have a live array to track future debug info.
525+
const chunkDebugInfo: ReactDebugInfo =
526+
chunk._debugInfo || (chunk._debugInfo = []);
527+
lazyType._debugInfo = chunkDebugInfo;
528+
}
502529
return lazyType;
503530
}
504531

@@ -694,7 +721,33 @@ function parseModelString(
694721
// The status might have changed after initialization.
695722
switch (chunk.status) {
696723
case INITIALIZED:
697-
return chunk.value;
724+
const chunkValue = chunk.value;
725+
if (__DEV__ && chunk._debugInfo) {
726+
// If we have a direct reference to an object that was rendered by a synchronous
727+
// server component, it might have some debug info about how it was rendered.
728+
// We forward this to the underlying object. This might be a React Element or
729+
// an Array fragment.
730+
// If this was a string / number return value we lose the debug info. We choose
731+
// that tradeoff to allow sync server components to return plain values and not
732+
// use them as React Nodes necessarily. We could otherwise wrap them in a Lazy.
733+
if (
734+
typeof chunkValue === 'object' &&
735+
chunkValue !== null &&
736+
(Array.isArray(chunkValue) ||
737+
chunkValue.$$typeof === REACT_ELEMENT_TYPE) &&
738+
!chunkValue._debugInfo
739+
) {
740+
// We should maybe use a unique symbol for arrays but this is a React owned array.
741+
// $FlowFixMe[prop-missing]: This should be added to elements.
742+
Object.defineProperty(chunkValue, '_debugInfo', {
743+
configurable: false,
744+
enumerable: false,
745+
writable: true,
746+
value: chunk._debugInfo,
747+
});
748+
}
749+
}
750+
return chunkValue;
698751
case PENDING:
699752
case BLOCKED:
700753
case CYCLIC:
@@ -971,6 +1024,24 @@ function resolveHint<Code: HintCode>(
9711024
dispatchHint(code, hintModel);
9721025
}
9731026

1027+
function resolveDebugInfo(
1028+
response: Response,
1029+
id: number,
1030+
debugInfo: {name: string},
1031+
): void {
1032+
if (!__DEV__) {
1033+
// These errors should never make it into a build so we don't need to encode them in codes.json
1034+
// eslint-disable-next-line react-internal/prod-error-codes
1035+
throw new Error(
1036+
'resolveDebugInfo should never be called in production mode. This is a bug in React.',
1037+
);
1038+
}
1039+
const chunk = getChunk(response, id);
1040+
const chunkDebugInfo: ReactDebugInfo =
1041+
chunk._debugInfo || (chunk._debugInfo = []);
1042+
chunkDebugInfo.push(debugInfo);
1043+
}
1044+
9741045
function mergeBuffer(
9751046
buffer: Array<Uint8Array>,
9761047
lastChunk: Uint8Array,
@@ -1064,7 +1135,7 @@ function processFullRow(
10641135
case 70 /* "F" */:
10651136
resolveTypedArray(response, id, buffer, chunk, Float32Array, 4);
10661137
return;
1067-
case 68 /* "D" */:
1138+
case 100 /* "d" */:
10681139
resolveTypedArray(response, id, buffer, chunk, Float64Array, 8);
10691140
return;
10701141
case 78 /* "N" */:
@@ -1114,6 +1185,18 @@ function processFullRow(
11141185
resolveText(response, id, row);
11151186
return;
11161187
}
1188+
case 68 /* "D" */: {
1189+
if (__DEV__) {
1190+
const debugInfo = JSON.parse(row);
1191+
resolveDebugInfo(response, id, debugInfo);
1192+
return;
1193+
}
1194+
throw new Error(
1195+
'Failed to read a RSC payload created by a development version of React ' +
1196+
'on the server while using a production version on the client. Always use ' +
1197+
'matching versions on the server and the client.',
1198+
);
1199+
}
11171200
case 80 /* "P" */: {
11181201
if (enablePostpone) {
11191202
if (__DEV__) {
@@ -1177,7 +1260,7 @@ export function processBinaryChunk(
11771260
resolvedRowTag === 76 /* "L" */ ||
11781261
resolvedRowTag === 108 /* "l" */ ||
11791262
resolvedRowTag === 70 /* "F" */ ||
1180-
resolvedRowTag === 68 /* "D" */ ||
1263+
resolvedRowTag === 100 /* "d" */ ||
11811264
resolvedRowTag === 78 /* "N" */ ||
11821265
resolvedRowTag === 109 /* "m" */ ||
11831266
resolvedRowTag === 86)) /* "V" */

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,12 +186,42 @@ describe('ReactFlight', () => {
186186
await act(async () => {
187187
const rootModel = await ReactNoopFlightClient.read(transport);
188188
const greeting = rootModel.greeting;
189+
expect(greeting._debugInfo).toEqual(
190+
__DEV__ ? [{name: 'Greeting'}] : undefined,
191+
);
189192
ReactNoop.render(greeting);
190193
});
191194

192195
expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb Smith</span>);
193196
});
194197

198+
it('can render a shared forwardRef Component', async () => {
199+
const Greeting = React.forwardRef(function Greeting(
200+
{firstName, lastName},
201+
ref,
202+
) {
203+
return (
204+
<span ref={ref}>
205+
Hello, {firstName} {lastName}
206+
</span>
207+
);
208+
});
209+
210+
const root = <Greeting firstName="Seb" lastName="Smith" />;
211+
212+
const transport = ReactNoopFlightServer.render(root);
213+
214+
await act(async () => {
215+
const promise = ReactNoopFlightClient.read(transport);
216+
expect(promise._debugInfo).toEqual(
217+
__DEV__ ? [{name: 'Greeting'}] : undefined,
218+
);
219+
ReactNoop.render(await promise);
220+
});
221+
222+
expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb Smith</span>);
223+
});
224+
195225
it('can render an iterable as an array', async () => {
196226
function ItemListClient(props) {
197227
return <span>{props.items}</span>;

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,8 @@ describe('ReactFlightDOMEdge', () => {
286286
<ServerComponent recurse={20} />,
287287
);
288288
const serializedContent = await readResult(stream);
289-
expect(serializedContent.length).toBeLessThan(150);
289+
const expectedDebugInfoSize = __DEV__ ? 30 * 20 : 0;
290+
expect(serializedContent.length).toBeLessThan(150 + expectedDebugInfoSize);
290291
});
291292

292293
// @gate enableBinaryFlight

packages/react-server/src/ReactFlightHooks.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,11 @@ export function prepareToUseHooksForComponent(
3737
thenableState = prevThenableState;
3838
}
3939

40-
export function getThenableStateAfterSuspending(): null | ThenableState {
41-
const state = thenableState;
40+
export function getThenableStateAfterSuspending(): ThenableState {
41+
// If you use() to Suspend this should always exist but if you throw a Promise instead,
42+
// which is not really supported anymore, it will be empty. We use the empty set as a
43+
// marker to know if this was a replay of the same component or first attempt.
44+
const state = thenableState || createThenableState();
4245
thenableState = null;
4346
return state;
4447
}

0 commit comments

Comments
 (0)