Files
react/packages/react-html/src/ReactHTMLServer.js
T
Sebastian Markbåge 4b62400765 [Flight] Add filterStackFrame options to the RSC Server (#30447)
This lets you customize the filter, for example allowing node_modules or
filter out additional functions that you don't want to include when
sending the stack to the client.

Notably this doesn't filter out Server Components out of the parent
stack. Those are just like a view of the tree by name. Not virtual stack
frames.
2024-07-25 10:50:56 -04:00

234 lines
6.9 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {ReactNodeList} from 'shared/ReactTypes';
import type {LazyComponent} from 'react/src/ReactLazy';
import type {ErrorInfo} from 'react-server/src/ReactFizzServer';
import ReactVersion from 'shared/ReactVersion';
import ReactSharedInternalsServer from 'react-server/src/ReactSharedInternalsServer';
import ReactSharedInternalsClient from 'shared/ReactSharedInternals';
import {
createRequest as createFlightRequest,
startWork as startFlightWork,
startFlowing as startFlightFlowing,
abort as abortFlight,
} from 'react-server/src/ReactFlightServer';
import {
createResponse as createFlightResponse,
getRoot as getFlightRoot,
processStringChunk as processFlightStringChunk,
close as closeFlight,
} from 'react-client/src/ReactFlightClient';
import {
createRequest as createFizzRequest,
startWork as startFizzWork,
startFlowing as startFizzFlowing,
abort as abortFizz,
} from 'react-server/src/ReactFizzServer';
import {
createResumableState,
createRenderState,
createRootFormatContext,
} from './ReactFizzConfigHTML';
type ReactMarkupNodeList =
// This is the intersection of ReactNodeList and ReactClientValue minus
// Client/ServerReferences.
| React$Element<React$AbstractComponent<any, any>>
| LazyComponent<ReactMarkupNodeList, any>
| React$Element<string>
| string
| boolean
| number
| symbol
| null
| void
| bigint
| $AsyncIterable<ReactMarkupNodeList, ReactMarkupNodeList, void>
| $AsyncIterator<ReactMarkupNodeList, ReactMarkupNodeList, void>
| Iterable<ReactMarkupNodeList>
| Iterator<ReactMarkupNodeList>
| Array<ReactMarkupNodeList>
| Promise<ReactMarkupNodeList>; // Thenable<ReactMarkupNodeList>
type MarkupOptions = {
identifierPrefix?: string,
signal?: AbortSignal,
onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
};
function noServerCallOrFormAction() {
throw new Error(
'renderToMarkup should not have emitted Server References. This is a bug in React.',
);
}
export function renderToMarkup(
children: ReactMarkupNodeList,
options?: MarkupOptions,
): Promise<string> {
return new Promise((resolve, reject) => {
const flightDestination = {
push(chunk: string | null): boolean {
if (chunk !== null) {
processFlightStringChunk(flightResponse, chunk);
} else {
closeFlight(flightResponse);
}
return true;
},
destroy(error: mixed): void {
abortFizz(fizzRequest, error);
reject(error);
},
};
let buffer = '';
const fizzDestination = {
// $FlowFixMe[missing-local-annot]
push(chunk) {
if (chunk !== null) {
buffer += chunk;
} else {
// null indicates that we finished
resolve(buffer);
}
return true;
},
// $FlowFixMe[missing-local-annot]
destroy(error) {
abortFlight(flightRequest, error);
reject(error);
},
};
let stashErrorIdx = 1;
const stashedErrors: Map<string, mixed> = new Map();
function handleFlightError(error: mixed): string {
// For Flight errors we don't immediately reject, because they might not matter
// to the output of the HTML. We stash the error with a digest in case we need
// to get to the original error from the Fizz render.
const id = '' + stashErrorIdx++;
stashedErrors.set(id, error);
return id;
}
function handleError(error: mixed, errorInfo: ErrorInfo) {
if (typeof error === 'object' && error !== null) {
const id = error.digest;
// Note that the original error might be `undefined` so we need a has check.
if (typeof id === 'string' && stashedErrors.has(id)) {
// Get the original error thrown inside Flight.
error = stashedErrors.get(id);
}
}
// Any error rejects the promise, regardless of where it happened.
// Unlike other React SSR we don't want to put Suspense boundaries into
// client rendering mode because there's no client rendering here.
reject(error);
const onError = options && options.onError;
if (onError) {
if (__DEV__) {
const prevGetCurrentStackImpl =
ReactSharedInternalsServer.getCurrentStack;
// We're inside a "client" callback from Fizz but we only have access to the
// "server" runtime so to get access to a stack trace within this callback we
// need to override it to get it from the client runtime.
ReactSharedInternalsServer.getCurrentStack =
ReactSharedInternalsClient.getCurrentStack;
try {
onError(error, errorInfo);
} finally {
ReactSharedInternalsServer.getCurrentStack =
prevGetCurrentStackImpl;
}
} else {
onError(error, errorInfo);
}
}
}
const flightRequest = createFlightRequest(
// $FlowFixMe: This should be a subtype but not everything is typed covariant.
children,
null,
handleFlightError,
options ? options.identifierPrefix : undefined,
undefined,
undefined,
'Markup',
undefined,
);
const flightResponse = createFlightResponse(
null,
null,
noServerCallOrFormAction,
noServerCallOrFormAction,
undefined,
undefined,
undefined,
false,
);
const resumableState = createResumableState(
options ? options.identifierPrefix : undefined,
undefined,
);
const root = getFlightRoot<ReactNodeList>(flightResponse);
const fizzRequest = createFizzRequest(
// $FlowFixMe: Thenables as children are supported.
root,
resumableState,
createRenderState(
resumableState,
undefined,
undefined,
undefined,
undefined,
undefined,
),
createRootFormatContext(),
Infinity,
handleError,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abortFlight(flightRequest, (signal: any).reason);
abortFizz(fizzRequest, (signal: any).reason);
} else {
const listener = () => {
abortFlight(flightRequest, (signal: any).reason);
abortFizz(fizzRequest, (signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
startFlightWork(flightRequest);
startFlightFlowing(flightRequest, flightDestination);
startFizzWork(fizzRequest);
startFizzFlowing(fizzRequest, fizzDestination);
});
}
export {ReactVersion as version};