This is basically the implementation for the prerender pass. Instead of forking basically the whole implementation for prerender, I just add a conditional field on the request. If it's `null` it behaves like before. If it's non-`null` then instead of triggering client rendered boundaries it triggers those into a "postponed" state which is basically just a variant of "pending". It's supposed to be filled in later. It also builds up a serializable tree of which path can be followed to find the holes. This is basically a reverse `KeyPath` tree. It is unfortunate that this approach adds more code to the regular Fizz builds but in practice. It seems like this side is not going to add much code and we might instead just want to merge the builds so that it's smaller when you have `prerender` and `resume` in the same bundle - which I think will be common in practice. This just implements the prerender side, and not the resume side, which is why the tests have a TODO. That's in a follow up PR.
204 lines
5.8 KiB
JavaScript
204 lines
5.8 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 {Request, PostponedState} from 'react-server/src/ReactFizzServer';
|
|
import type {ReactNodeList} from 'shared/ReactTypes';
|
|
import type {Writable} from 'stream';
|
|
import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
|
import type {Destination} from 'react-server/src/ReactServerStreamConfigNode';
|
|
import type {ImportMap} from '../shared/ReactDOMTypes';
|
|
|
|
import ReactVersion from 'shared/ReactVersion';
|
|
|
|
import {
|
|
createRequest,
|
|
startRender,
|
|
startFlowing,
|
|
abort,
|
|
} from 'react-server/src/ReactFizzServer';
|
|
|
|
import {
|
|
createResumableState,
|
|
createRenderState,
|
|
createRootFormatContext,
|
|
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
|
|
|
function createDrainHandler(destination: Destination, request: Request) {
|
|
return () => startFlowing(request, destination);
|
|
}
|
|
|
|
function createAbortHandler(request: Request, reason: string) {
|
|
// eslint-disable-next-line react-internal/prod-error-codes
|
|
return () => abort(request, new Error(reason));
|
|
}
|
|
|
|
type Options = {
|
|
identifierPrefix?: string,
|
|
namespaceURI?: string,
|
|
nonce?: string,
|
|
bootstrapScriptContent?: string,
|
|
bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
|
|
bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
|
|
progressiveChunkSize?: number,
|
|
onShellReady?: () => void,
|
|
onShellError?: (error: mixed) => void,
|
|
onAllReady?: () => void,
|
|
onError?: (error: mixed) => ?string,
|
|
onPostpone?: (reason: string) => void,
|
|
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
|
|
importMap?: ImportMap,
|
|
};
|
|
|
|
type ResumeOptions = {
|
|
nonce?: string,
|
|
onShellReady?: () => void,
|
|
onShellError?: (error: mixed) => void,
|
|
onAllReady?: () => void,
|
|
onError?: (error: mixed) => ?string,
|
|
onPostpone?: (reason: string) => void,
|
|
};
|
|
|
|
type PipeableStream = {
|
|
// Cancel any pending I/O and put anything remaining into
|
|
// client rendered mode.
|
|
abort(reason: mixed): void,
|
|
pipe<T: Writable>(destination: T): T,
|
|
};
|
|
|
|
function createRequestImpl(children: ReactNodeList, options: void | Options) {
|
|
const resumableState = createResumableState(
|
|
options ? options.identifierPrefix : undefined,
|
|
options ? options.nonce : undefined,
|
|
options ? options.bootstrapScriptContent : undefined,
|
|
options ? options.bootstrapScripts : undefined,
|
|
options ? options.bootstrapModules : undefined,
|
|
options ? options.unstable_externalRuntimeSrc : undefined,
|
|
);
|
|
return createRequest(
|
|
children,
|
|
resumableState,
|
|
createRenderState(
|
|
resumableState,
|
|
options ? options.nonce : undefined,
|
|
options ? options.importMap : undefined,
|
|
),
|
|
createRootFormatContext(options ? options.namespaceURI : undefined),
|
|
options ? options.progressiveChunkSize : undefined,
|
|
options ? options.onError : undefined,
|
|
options ? options.onAllReady : undefined,
|
|
options ? options.onShellReady : undefined,
|
|
options ? options.onShellError : undefined,
|
|
undefined,
|
|
options ? options.onPostpone : undefined,
|
|
);
|
|
}
|
|
|
|
function renderToPipeableStream(
|
|
children: ReactNodeList,
|
|
options?: Options,
|
|
): PipeableStream {
|
|
const request = createRequestImpl(children, options);
|
|
let hasStartedFlowing = false;
|
|
startRender(request);
|
|
return {
|
|
pipe<T: Writable>(destination: T): T {
|
|
if (hasStartedFlowing) {
|
|
throw new Error(
|
|
'React currently only supports piping to one writable stream.',
|
|
);
|
|
}
|
|
hasStartedFlowing = true;
|
|
startFlowing(request, destination);
|
|
destination.on('drain', createDrainHandler(destination, request));
|
|
destination.on(
|
|
'error',
|
|
createAbortHandler(
|
|
request,
|
|
'The destination stream errored while writing data.',
|
|
),
|
|
);
|
|
destination.on(
|
|
'close',
|
|
createAbortHandler(request, 'The destination stream closed early.'),
|
|
);
|
|
return destination;
|
|
},
|
|
abort(reason: mixed) {
|
|
abort(request, reason);
|
|
},
|
|
};
|
|
}
|
|
|
|
function resumeRequestImpl(
|
|
children: ReactNodeList,
|
|
postponedState: PostponedState,
|
|
options: void | ResumeOptions,
|
|
) {
|
|
return createRequest(
|
|
children,
|
|
postponedState.resumableState,
|
|
createRenderState(
|
|
postponedState.resumableState,
|
|
options ? options.nonce : undefined,
|
|
undefined, // importMap
|
|
),
|
|
postponedState.rootFormatContext,
|
|
postponedState.progressiveChunkSize,
|
|
options ? options.onError : undefined,
|
|
options ? options.onAllReady : undefined,
|
|
options ? options.onShellReady : undefined,
|
|
options ? options.onShellError : undefined,
|
|
undefined,
|
|
options ? options.onPostpone : undefined,
|
|
);
|
|
}
|
|
|
|
function resumeToPipeableStream(
|
|
children: ReactNodeList,
|
|
postponedState: PostponedState,
|
|
options?: ResumeOptions,
|
|
): PipeableStream {
|
|
const request = resumeRequestImpl(children, postponedState, options);
|
|
let hasStartedFlowing = false;
|
|
startRender(request);
|
|
return {
|
|
pipe<T: Writable>(destination: T): T {
|
|
if (hasStartedFlowing) {
|
|
throw new Error(
|
|
'React currently only supports piping to one writable stream.',
|
|
);
|
|
}
|
|
hasStartedFlowing = true;
|
|
startFlowing(request, destination);
|
|
destination.on('drain', createDrainHandler(destination, request));
|
|
destination.on(
|
|
'error',
|
|
createAbortHandler(
|
|
request,
|
|
'The destination stream errored while writing data.',
|
|
),
|
|
);
|
|
destination.on(
|
|
'close',
|
|
createAbortHandler(request, 'The destination stream closed early.'),
|
|
);
|
|
return destination;
|
|
},
|
|
abort(reason: mixed) {
|
|
abort(request, reason);
|
|
},
|
|
};
|
|
}
|
|
|
|
export {
|
|
renderToPipeableStream,
|
|
resumeToPipeableStream,
|
|
ReactVersion as version,
|
|
};
|