main
js 150 lines 4.68 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes';
11 import type {
12 BootstrapScriptDescriptor,
13 HeadersDescriptor,
14 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
15 import type {ImportMap} from '../shared/ReactDOMTypes';
16 import type {ErrorInfo} from 'react-server/src/ReactFizzServer';
17
18 import ReactVersion from 'shared/ReactVersion';
19
20 import {
21 createRequest,
22 startWork,
23 startFlowing,
24 stopFlowing,
25 abort,
26 attachAbortSignal,
27 } from 'react-server/src/ReactFizzServer';
28
29 import {
30 createResumableState,
31 createRenderState,
32 createRootFormatContext,
33 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
34
35 import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
36 ensureCorrectIsomorphicReactVersion();
37
38 type Options = {
39 identifierPrefix?: string,
40 namespaceURI?: string,
41 nonce?:
42 | string
43 | {
44 script?: string,
45 style?: string,
46 },
47 bootstrapScriptContent?: string,
48 bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
49 bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
50 progressiveChunkSize?: number,
51 signal?: AbortSignal,
52 onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
53 onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void,
54 unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
55 importMap?: ImportMap,
56 formState?: ReactFormState<any, any> | null,
57 onHeaders?: (headers: Headers) => void,
58 maxHeadersLength?: number,
59 };
60
61 // TODO: Move to sub-classing ReadableStream.
62 type ReactDOMServerReadableStream = ReadableStream & {
63 allReady: Promise<void>,
64 };
65
66 function renderToReadableStream(
67 children: ReactNodeList,
68 options?: Options,
69 ): Promise<ReactDOMServerReadableStream> {
70 return new Promise((resolve, reject) => {
71 let onFatalError;
72 let onAllReady;
73 const allReady = new Promise<void>((res, rej) => {
74 onAllReady = res;
75 onFatalError = rej;
76 });
77
78 function onShellReady() {
79 const stream: ReactDOMServerReadableStream = new ReadableStream(
80 {
81 type: 'direct',
82 pull: (controller): ?Promise<void> => {
83 // $FlowFixMe[incompatible-type]
84 startFlowing(request, controller);
85 },
86 cancel: (reason): ?Promise<void> => {
87 stopFlowing(request);
88 abort(request, reason);
89 },
90 },
91 // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
92 // $FlowFixMe[incompatible-type]
93 {highWaterMark: 2048},
94 ) as any;
95 // TODO: Move to sub-classing ReadableStream.
96 stream.allReady = allReady;
97 resolve(stream);
98 }
99 function onShellError(error: mixed) {
100 // If the shell errors the caller of `renderToReadableStream` won't have access to `allReady`.
101 // However, `allReady` will be rejected by `onFatalError` as well.
102 // So we need to catch the duplicate, uncatchable fatal error in `allReady` to prevent a `UnhandledPromiseRejection`.
103 allReady.catch(() => {});
104 reject(error);
105 }
106
107 const onHeaders = options ? options.onHeaders : undefined;
108 let onHeadersImpl;
109 if (onHeaders) {
110 onHeadersImpl = (headersDescriptor: HeadersDescriptor) => {
111 onHeaders(new Headers(headersDescriptor));
112 };
113 }
114
115 const resumableState = createResumableState(
116 options ? options.identifierPrefix : undefined,
117 options ? options.unstable_externalRuntimeSrc : undefined,
118 options ? options.bootstrapScriptContent : undefined,
119 options ? options.bootstrapScripts : undefined,
120 options ? options.bootstrapModules : undefined,
121 );
122 const request = createRequest(
123 children,
124 resumableState,
125 createRenderState(
126 resumableState,
127 options ? options.nonce : undefined,
128 options ? options.unstable_externalRuntimeSrc : undefined,
129 options ? options.importMap : undefined,
130 onHeadersImpl,
131 options ? options.maxHeadersLength : undefined,
132 ),
133 createRootFormatContext(options ? options.namespaceURI : undefined),
134 options ? options.progressiveChunkSize : undefined,
135 options ? options.onError : undefined,
136 options ? options.onBrowserBailout : undefined,
137 onAllReady,
138 onShellReady,
139 onShellError,
140 onFatalError,
141 options ? options.formState : undefined,
142 );
143 if (options && options.signal) {
144 attachAbortSignal(request, options.signal);
145 }
146 startWork(request);
147 });
148 }
149
150 export {renderToReadableStream, ReactVersion as version};