Files
react/packages/react-markup/src/ReactMarkupServer.js
2024-08-15 08:50:56 +02: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 './ReactFizzConfigMarkup';
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(
'renderToHTML should not have emitted Server References. This is a bug in React.',
);
}
export function experimental_renderToHTML(
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};