mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
[Flight] Transfer Debug Info in Server-to-Server Flight Requests (#28275)
A Flight Server can be a consumer of a stream from another Server. In this case the meta data is attached to debugInfo properties on lazy, Promises, Arrays or Elements that might in turn get forwarded to the next stream. In this case we want to forward this debug information to the client in the stream. I also added a DEV only `environmentName` option to the Flight Server. This lets you name the server that is producing the debug info so that you can trace the origin of where that component is executing. This defaults to `"server"`. DevTools could use this for badges or different colors.
This commit is contained in:
committed by
GitHub
parent
947e7962ad
commit
629541bcc0
@@ -77,7 +77,7 @@ const INITIALIZED = 'fulfilled';
|
||||
const ERRORED = 'rejected';
|
||||
|
||||
// Dev-only
|
||||
type ReactDebugInfo = Array<{+name?: string}>;
|
||||
type ReactDebugInfo = Array<{+name?: string, +env?: string}>;
|
||||
|
||||
type PendingChunk<T> = {
|
||||
status: 'pending',
|
||||
|
||||
@@ -187,7 +187,7 @@ describe('ReactFlight', () => {
|
||||
const rootModel = await ReactNoopFlightClient.read(transport);
|
||||
const greeting = rootModel.greeting;
|
||||
expect(greeting._debugInfo).toEqual(
|
||||
__DEV__ ? [{name: 'Greeting'}] : undefined,
|
||||
__DEV__ ? [{name: 'Greeting', env: 'server'}] : undefined,
|
||||
);
|
||||
ReactNoop.render(greeting);
|
||||
});
|
||||
@@ -214,7 +214,7 @@ describe('ReactFlight', () => {
|
||||
await act(async () => {
|
||||
const promise = ReactNoopFlightClient.read(transport);
|
||||
expect(promise._debugInfo).toEqual(
|
||||
__DEV__ ? [{name: 'Greeting'}] : undefined,
|
||||
__DEV__ ? [{name: 'Greeting', env: 'server'}] : undefined,
|
||||
);
|
||||
ReactNoop.render(await promise);
|
||||
});
|
||||
@@ -1806,4 +1806,68 @@ describe('ReactFlight', () => {
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(<div>Ba</div>);
|
||||
});
|
||||
|
||||
it('preserves debug info for server-to-server pass through', async () => {
|
||||
function ThirdPartyLazyComponent() {
|
||||
return <span>!</span>;
|
||||
}
|
||||
|
||||
const lazy = React.lazy(async () => ({
|
||||
default: <ThirdPartyLazyComponent />,
|
||||
}));
|
||||
|
||||
function ThirdPartyComponent() {
|
||||
return <span>stranger</span>;
|
||||
}
|
||||
|
||||
function ServerComponent({transport}) {
|
||||
// This is a Server Component that receives other Server Components from a third party.
|
||||
const children = ReactNoopFlightClient.read(transport);
|
||||
return <div>Hello, {children}</div>;
|
||||
}
|
||||
|
||||
const promiseComponent = Promise.resolve(<ThirdPartyComponent />);
|
||||
|
||||
const thirdPartyTransport = ReactNoopFlightServer.render(
|
||||
[promiseComponent, lazy],
|
||||
{
|
||||
environmentName: 'third-party',
|
||||
},
|
||||
);
|
||||
|
||||
// Wait for the lazy component to initialize
|
||||
await 0;
|
||||
|
||||
const transport = ReactNoopFlightServer.render(
|
||||
<ServerComponent transport={thirdPartyTransport} />,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const promise = ReactNoopFlightClient.read(transport);
|
||||
expect(promise._debugInfo).toEqual(
|
||||
__DEV__ ? [{name: 'ServerComponent', env: 'server'}] : undefined,
|
||||
);
|
||||
const result = await promise;
|
||||
const thirdPartyChildren = await result.props.children[1];
|
||||
// We expect the debug info to be transferred from the inner stream to the outer.
|
||||
expect(thirdPartyChildren[0]._debugInfo).toEqual(
|
||||
__DEV__
|
||||
? [{name: 'ThirdPartyComponent', env: 'third-party'}]
|
||||
: undefined,
|
||||
);
|
||||
expect(thirdPartyChildren[1]._debugInfo).toEqual(
|
||||
__DEV__
|
||||
? [{name: 'ThirdPartyLazyComponent', env: 'third-party'}]
|
||||
: undefined,
|
||||
);
|
||||
ReactNoop.render(result);
|
||||
});
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<div>
|
||||
Hello, <span>stranger</span>
|
||||
<span>!</span>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,8 +68,10 @@ const ReactNoopFlightServer = ReactFlightServer({
|
||||
});
|
||||
|
||||
type Options = {
|
||||
onError?: (error: mixed) => void,
|
||||
environmentName?: string,
|
||||
identifierPrefix?: string,
|
||||
onError?: (error: mixed) => void,
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
|
||||
function render(model: ReactClientValue, options?: Options): Destination {
|
||||
@@ -80,6 +82,8 @@ function render(model: ReactClientValue, options?: Options): Destination {
|
||||
bundlerConfig,
|
||||
options ? options.onError : undefined,
|
||||
options ? options.identifierPrefix : undefined,
|
||||
options ? options.onPostpone : undefined,
|
||||
options ? options.environmentName : undefined,
|
||||
);
|
||||
ReactNoopFlightServer.startWork(request);
|
||||
ReactNoopFlightServer.startFlowing(request, destination);
|
||||
|
||||
@@ -52,6 +52,7 @@ function createDrainHandler(destination: Destination, request: Request) {
|
||||
}
|
||||
|
||||
type Options = {
|
||||
environmentName?: string,
|
||||
onError?: (error: mixed) => void,
|
||||
onPostpone?: (reason: string) => void,
|
||||
identifierPrefix?: string,
|
||||
@@ -73,6 +74,7 @@ function renderToPipeableStream(
|
||||
options ? options.onError : undefined,
|
||||
options ? options.identifierPrefix : undefined,
|
||||
options ? options.onPostpone : undefined,
|
||||
options ? options.environmentName : undefined,
|
||||
);
|
||||
let hasStartedFlowing = false;
|
||||
startWork(request);
|
||||
|
||||
@@ -50,6 +50,8 @@ function renderToDestination(
|
||||
model,
|
||||
null,
|
||||
options ? options.onError : undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
startWork(request);
|
||||
startFlowing(request, destination);
|
||||
|
||||
@@ -34,6 +34,7 @@ export {
|
||||
} from './ReactFlightTurbopackReferences';
|
||||
|
||||
type Options = {
|
||||
environmentName?: string,
|
||||
identifierPrefix?: string,
|
||||
signal?: AbortSignal,
|
||||
onError?: (error: mixed) => void,
|
||||
@@ -51,6 +52,7 @@ function renderToReadableStream(
|
||||
options ? options.onError : undefined,
|
||||
options ? options.identifierPrefix : undefined,
|
||||
options ? options.onPostpone : undefined,
|
||||
options ? options.environmentName : undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -34,6 +34,7 @@ export {
|
||||
} from './ReactFlightTurbopackReferences';
|
||||
|
||||
type Options = {
|
||||
environmentName?: string,
|
||||
identifierPrefix?: string,
|
||||
signal?: AbortSignal,
|
||||
onError?: (error: mixed) => void,
|
||||
@@ -51,6 +52,7 @@ function renderToReadableStream(
|
||||
options ? options.onError : undefined,
|
||||
options ? options.identifierPrefix : undefined,
|
||||
options ? options.onPostpone : undefined,
|
||||
options ? options.environmentName : undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -49,6 +49,7 @@ function createDrainHandler(destination: Destination, request: Request) {
|
||||
}
|
||||
|
||||
type Options = {
|
||||
environmentName?: string,
|
||||
onError?: (error: mixed) => void,
|
||||
onPostpone?: (reason: string) => void,
|
||||
identifierPrefix?: string,
|
||||
@@ -70,6 +71,7 @@ function renderToPipeableStream(
|
||||
options ? options.onError : undefined,
|
||||
options ? options.identifierPrefix : undefined,
|
||||
options ? options.onPostpone : undefined,
|
||||
options ? options.environmentName : undefined,
|
||||
);
|
||||
let hasStartedFlowing = false;
|
||||
startWork(request);
|
||||
|
||||
@@ -38,6 +38,7 @@ export {
|
||||
} from './ReactFlightWebpackReferences';
|
||||
|
||||
type Options = {
|
||||
environmentName?: string,
|
||||
identifierPrefix?: string,
|
||||
signal?: AbortSignal,
|
||||
onError?: (error: mixed) => void,
|
||||
@@ -55,6 +56,7 @@ function renderToReadableStream(
|
||||
options ? options.onError : undefined,
|
||||
options ? options.identifierPrefix : undefined,
|
||||
options ? options.onPostpone : undefined,
|
||||
options ? options.environmentName : undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -38,6 +38,7 @@ export {
|
||||
} from './ReactFlightWebpackReferences';
|
||||
|
||||
type Options = {
|
||||
environmentName?: string,
|
||||
identifierPrefix?: string,
|
||||
signal?: AbortSignal,
|
||||
onError?: (error: mixed) => void,
|
||||
@@ -55,6 +56,7 @@ function renderToReadableStream(
|
||||
options ? options.onError : undefined,
|
||||
options ? options.identifierPrefix : undefined,
|
||||
options ? options.onPostpone : undefined,
|
||||
options ? options.environmentName : undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -61,6 +61,7 @@ function createCancelHandler(request: Request, reason: string) {
|
||||
}
|
||||
|
||||
type Options = {
|
||||
environmentName?: string,
|
||||
onError?: (error: mixed) => void,
|
||||
onPostpone?: (reason: string) => void,
|
||||
identifierPrefix?: string,
|
||||
@@ -82,6 +83,7 @@ function renderToPipeableStream(
|
||||
options ? options.onError : undefined,
|
||||
options ? options.identifierPrefix : undefined,
|
||||
options ? options.onPostpone : undefined,
|
||||
options ? options.environmentName : undefined,
|
||||
);
|
||||
let hasStartedFlowing = false;
|
||||
startWork(request);
|
||||
|
||||
@@ -286,7 +286,7 @@ describe('ReactFlightDOMEdge', () => {
|
||||
<ServerComponent recurse={20} />,
|
||||
);
|
||||
const serializedContent = await readResult(stream);
|
||||
const expectedDebugInfoSize = __DEV__ ? 30 * 20 : 0;
|
||||
const expectedDebugInfoSize = __DEV__ ? 42 * 20 : 0;
|
||||
expect(serializedContent.length).toBeLessThan(150 + expectedDebugInfoSize);
|
||||
});
|
||||
|
||||
|
||||
101
packages/react-server/src/ReactFlightServer.js
vendored
101
packages/react-server/src/ReactFlightServer.js
vendored
@@ -107,6 +107,9 @@ import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable';
|
||||
|
||||
initAsyncDebugInfo();
|
||||
|
||||
// Dev-only
|
||||
type ReactDebugInfo = Array<{+name?: string, +env?: string}>;
|
||||
|
||||
const ObjectPrototype = Object.prototype;
|
||||
|
||||
type JSONValue =
|
||||
@@ -199,6 +202,8 @@ export type Request = {
|
||||
taintCleanupQueue: Array<string | bigint>,
|
||||
onError: (error: mixed) => ?string,
|
||||
onPostpone: (reason: string) => void,
|
||||
// DEV-only
|
||||
environmentName: string,
|
||||
};
|
||||
|
||||
const {
|
||||
@@ -251,6 +256,7 @@ export function createRequest(
|
||||
onError: void | ((error: mixed) => ?string),
|
||||
identifierPrefix?: string,
|
||||
onPostpone: void | ((reason: string) => void),
|
||||
environmentName: void | string,
|
||||
): Request {
|
||||
if (
|
||||
ReactCurrentCache.current !== null &&
|
||||
@@ -270,7 +276,7 @@ export function createRequest(
|
||||
TaintRegistryPendingRequests.add(cleanupQueue);
|
||||
}
|
||||
const hints = createHints();
|
||||
const request: Request = {
|
||||
const request: Request = ({
|
||||
status: OPEN,
|
||||
flushScheduled: false,
|
||||
fatalError: null,
|
||||
@@ -295,7 +301,11 @@ export function createRequest(
|
||||
taintCleanupQueue: cleanupQueue,
|
||||
onError: onError === undefined ? defaultErrorHandler : onError,
|
||||
onPostpone: onPostpone === undefined ? defaultPostponeHandler : onPostpone,
|
||||
};
|
||||
}: any);
|
||||
if (__DEV__) {
|
||||
request.environmentName =
|
||||
environmentName === undefined ? 'server' : environmentName;
|
||||
}
|
||||
const rootTask = createTask(request, model, null, false, abortSet);
|
||||
pingedTasks.push(rootTask);
|
||||
return request;
|
||||
@@ -325,6 +335,14 @@ function serializeThenable(
|
||||
request.abortableTasks,
|
||||
);
|
||||
|
||||
if (__DEV__) {
|
||||
// If this came from Flight, forward any debug info into this new row.
|
||||
const debugInfo: ?ReactDebugInfo = (thenable: any)._debugInfo;
|
||||
if (debugInfo) {
|
||||
forwardDebugInfo(request, newTask.id, debugInfo);
|
||||
}
|
||||
}
|
||||
|
||||
switch (thenable.status) {
|
||||
case 'fulfilled': {
|
||||
// We have the resolved value, we can go ahead and schedule it for serialization.
|
||||
@@ -475,6 +493,10 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) {
|
||||
_payload: thenable,
|
||||
_init: readThenable,
|
||||
};
|
||||
if (__DEV__) {
|
||||
// If this came from React, transfer the debug info.
|
||||
lazyType._debugInfo = (thenable: any)._debugInfo || [];
|
||||
}
|
||||
return lazyType;
|
||||
}
|
||||
|
||||
@@ -504,7 +526,10 @@ function renderFunctionComponent<Props>(
|
||||
const componentName =
|
||||
(Component: any).displayName || Component.name || '';
|
||||
request.pendingChunks++;
|
||||
emitDebugChunk(request, debugID, {name: componentName});
|
||||
emitDebugChunk(request, debugID, {
|
||||
name: componentName,
|
||||
env: request.environmentName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,6 +577,22 @@ function renderFragment(
|
||||
task: Task,
|
||||
children: $ReadOnlyArray<ReactClientValue>,
|
||||
): ReactJSONValue {
|
||||
if (__DEV__) {
|
||||
const debugInfo: ?ReactDebugInfo = (children: any)._debugInfo;
|
||||
if (debugInfo) {
|
||||
// If this came from Flight, forward any debug info into this new row.
|
||||
if (debugID === null) {
|
||||
// We don't have a chunk to assign debug info. We need to outline this
|
||||
// component to assign it an ID.
|
||||
return outlineTask(request, task);
|
||||
} else {
|
||||
// Forward any debug info we have the first time we see it.
|
||||
// We do this after init so that we have received all the debug info
|
||||
// from the server by the time we emit it.
|
||||
forwardDebugInfo(request, debugID, debugInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!enableServerComponentKeys) {
|
||||
return children;
|
||||
}
|
||||
@@ -1210,6 +1251,22 @@ function renderModelDestructive(
|
||||
}
|
||||
|
||||
const element: React$Element<any> = (value: any);
|
||||
|
||||
if (__DEV__) {
|
||||
const debugInfo: ?ReactDebugInfo = (value: any)._debugInfo;
|
||||
if (debugInfo) {
|
||||
// If this came from Flight, forward any debug info into this new row.
|
||||
if (debugID === null) {
|
||||
// We don't have a chunk to assign debug info. We need to outline this
|
||||
// component to assign it an ID.
|
||||
return outlineTask(request, task);
|
||||
} else {
|
||||
// Forward any debug info we have the first time we see it.
|
||||
forwardDebugInfo(request, debugID, debugInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to render the Server Component.
|
||||
return renderElement(
|
||||
request,
|
||||
@@ -1222,9 +1279,30 @@ function renderModelDestructive(
|
||||
);
|
||||
}
|
||||
case REACT_LAZY_TYPE: {
|
||||
const payload = (value: any)._payload;
|
||||
const init = (value: any)._init;
|
||||
// Reset the task's thenable state before continuing. If there was one, it was
|
||||
// from suspending the lazy before.
|
||||
task.thenableState = null;
|
||||
|
||||
const lazy: LazyComponent<any, any> = (value: any);
|
||||
const payload = lazy._payload;
|
||||
const init = lazy._init;
|
||||
const resolvedModel = init(payload);
|
||||
if (__DEV__) {
|
||||
const debugInfo: ?ReactDebugInfo = lazy._debugInfo;
|
||||
if (debugInfo) {
|
||||
// If this came from Flight, forward any debug info into this new row.
|
||||
if (debugID === null) {
|
||||
// We don't have a chunk to assign debug info. We need to outline this
|
||||
// component to assign it an ID.
|
||||
return outlineTask(request, task);
|
||||
} else {
|
||||
// Forward any debug info we have the first time we see it.
|
||||
// We do this after init so that we have received all the debug info
|
||||
// from the server by the time we emit it.
|
||||
forwardDebugInfo(request, debugID, debugInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
return renderModelDestructive(
|
||||
request,
|
||||
task,
|
||||
@@ -1653,7 +1731,7 @@ function emitModelChunk(request: Request, id: number, json: string): void {
|
||||
function emitDebugChunk(
|
||||
request: Request,
|
||||
id: number,
|
||||
debugInfo: {name: string},
|
||||
debugInfo: {+name?: string, +env?: string},
|
||||
): void {
|
||||
if (!__DEV__) {
|
||||
// These errors should never make it into a build so we don't need to encode them in codes.json
|
||||
@@ -1669,6 +1747,17 @@ function emitDebugChunk(
|
||||
request.completedRegularChunks.push(processedChunk);
|
||||
}
|
||||
|
||||
function forwardDebugInfo(
|
||||
request: Request,
|
||||
id: number,
|
||||
debugInfo: ReactDebugInfo,
|
||||
) {
|
||||
for (let i = 0; i < debugInfo.length; i++) {
|
||||
request.pendingChunks++;
|
||||
emitDebugChunk(request, id, debugInfo[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const emptyRoot = {};
|
||||
|
||||
function retryTask(request: Request, task: Task): void {
|
||||
|
||||
@@ -46,7 +46,7 @@ export type LazyComponent<T, P> = {
|
||||
$$typeof: symbol | number,
|
||||
_payload: P,
|
||||
_init: (payload: P) => T,
|
||||
_debugInfo?: null | Array<{+name?: string}>,
|
||||
_debugInfo?: null | Array<{+name?: string, +env?: string}>,
|
||||
};
|
||||
|
||||
function lazyInitializer<T>(payload: Payload<T>): T {
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('ReactFetch', () => {
|
||||
const promise = render(Component);
|
||||
expect(await promise).toMatchInlineSnapshot(`"GET world []"`);
|
||||
expect(promise._debugInfo).toEqual(
|
||||
__DEV__ ? [{name: 'Component'}] : undefined,
|
||||
__DEV__ ? [{name: 'Component', env: 'server'}] : undefined,
|
||||
);
|
||||
expect(fetchCount).toBe(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user