main
js 366 lines 10.3 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} from 'shared/ReactTypes';
11 import type {
12 BootstrapScriptDescriptor,
13 HeadersDescriptor,
14 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
15 import type {PostponedState, ErrorInfo} from 'react-server/src/ReactFizzServer';
16 import type {ImportMap} from '../shared/ReactDOMTypes';
17
18 import {Writable, Readable} from 'stream';
19
20 import ReactVersion from 'shared/ReactVersion';
21
22 import {
23 createPrerenderRequest,
24 resumeAndPrerenderRequest,
25 startWork,
26 startFlowing,
27 stopFlowing,
28 abort,
29 attachAbortSignal,
30 getPostponedState,
31 } from 'react-server/src/ReactFizzServer';
32
33 import {
34 createResumableState,
35 createRenderState,
36 resumeRenderState,
37 createRootFormatContext,
38 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
39
40 import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
41
42 import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
43 ensureCorrectIsomorphicReactVersion();
44
45 type NonceOption =
46 | string
47 | {
48 script?: string,
49 style?: string,
50 };
51
52 type Options = {
53 identifierPrefix?: string,
54 namespaceURI?: string,
55 bootstrapScriptContent?: string,
56 bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
57 bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
58 progressiveChunkSize?: number,
59 signal?: AbortSignal,
60 onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
61 onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void,
62 unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
63 importMap?: ImportMap,
64 onHeaders?: (headers: HeadersDescriptor) => void,
65 maxHeadersLength?: number,
66 };
67
68 type StaticResult = {
69 postponed: null | PostponedState,
70 prelude: Readable,
71 };
72
73 function createFakeWritableFromReadableStreamController(
74 controller: ReadableStreamController,
75 ): Writable {
76 // The current host config expects a Writable so we create
77 // a fake writable for now to push into the Readable.
78 return {
79 write(chunk: string | Uint8Array) {
80 if (typeof chunk === 'string') {
81 chunk = textEncoder.encode(chunk);
82 }
83 controller.enqueue(chunk);
84 // in web streams there is no backpressure so we can alwas write more
85 return true;
86 },
87 end() {
88 controller.close();
89 },
90 destroy(error) {
91 // $FlowFixMe[method-unbinding]
92 if (typeof controller.error === 'function') {
93 // $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.
94 controller.error(error);
95 } else {
96 controller.close();
97 }
98 },
99 } as any;
100 }
101
102 function createFakeWritableFromReadable(readable: any): Writable {
103 // The current host config expects a Writable so we create
104 // a fake writable for now to push into the Readable.
105 return {
106 write(chunk) {
107 return readable.push(chunk);
108 },
109 end() {
110 readable.push(null);
111 },
112 destroy(error) {
113 readable.destroy(error);
114 },
115 } as any;
116 }
117
118 function prerenderToNodeStream(
119 children: ReactNodeList,
120 options?: Options,
121 ): Promise<StaticResult> {
122 return new Promise((resolve, reject) => {
123 const onFatalError = reject;
124
125 function onAllReady() {
126 const readable: Readable = new Readable({
127 read() {
128 startFlowing(request, writable);
129 },
130 });
131 const writable = createFakeWritableFromReadable(readable);
132
133 const result: StaticResult = {
134 postponed: getPostponedState(request),
135 prelude: readable,
136 };
137 resolve(result);
138 }
139 const resumableState = createResumableState(
140 options ? options.identifierPrefix : undefined,
141 options ? options.unstable_externalRuntimeSrc : undefined,
142 options ? options.bootstrapScriptContent : undefined,
143 options ? options.bootstrapScripts : undefined,
144 options ? options.bootstrapModules : undefined,
145 );
146 const request = createPrerenderRequest(
147 children,
148 resumableState,
149 createRenderState(
150 resumableState,
151 undefined, // nonce is not compatible with prerendered bootstrap scripts
152 options ? options.unstable_externalRuntimeSrc : undefined,
153 options ? options.importMap : undefined,
154 options ? options.onHeaders : undefined,
155 options ? options.maxHeadersLength : undefined,
156 ),
157 createRootFormatContext(options ? options.namespaceURI : undefined),
158 options ? options.progressiveChunkSize : undefined,
159 options ? options.onError : undefined,
160 options ? options.onBrowserBailout : undefined,
161 onAllReady,
162 undefined,
163 undefined,
164 onFatalError,
165 );
166 if (options && options.signal) {
167 attachAbortSignal(request, options.signal);
168 }
169 startWork(request);
170 });
171 }
172
173 function prerender(
174 children: ReactNodeList,
175 options?: Omit<Options, 'onHeaders'> & {
176 onHeaders?: (headers: Headers) => void,
177 },
178 ): Promise<{
179 postponed: null | PostponedState,
180 prelude: ReadableStream,
181 }> {
182 return new Promise((resolve, reject) => {
183 const onFatalError = reject;
184
185 function onAllReady() {
186 let writable: Writable;
187 const stream = new ReadableStream(
188 {
189 type: 'bytes',
190 start: (controller): ?Promise<void> => {
191 writable =
192 createFakeWritableFromReadableStreamController(controller);
193 },
194 pull: (controller): ?Promise<void> => {
195 startFlowing(request, writable);
196 },
197 cancel: (reason): ?Promise<void> => {
198 stopFlowing(request);
199 abort(request, reason);
200 },
201 },
202 // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
203 // $FlowFixMe[incompatible-type]
204 {highWaterMark: 0},
205 );
206
207 const result = {
208 postponed: getPostponedState(request),
209 prelude: stream,
210 };
211 resolve(result);
212 }
213
214 const onHeaders = options ? options.onHeaders : undefined;
215 let onHeadersImpl;
216 if (onHeaders) {
217 onHeadersImpl = (headersDescriptor: HeadersDescriptor) => {
218 onHeaders(new Headers(headersDescriptor));
219 };
220 }
221 const resources = createResumableState(
222 options ? options.identifierPrefix : undefined,
223 options ? options.unstable_externalRuntimeSrc : undefined,
224 options ? options.bootstrapScriptContent : undefined,
225 options ? options.bootstrapScripts : undefined,
226 options ? options.bootstrapModules : undefined,
227 );
228 const request = createPrerenderRequest(
229 children,
230 resources,
231 createRenderState(
232 resources,
233 undefined, // nonce is not compatible with prerendered bootstrap scripts
234 options ? options.unstable_externalRuntimeSrc : undefined,
235 options ? options.importMap : undefined,
236 onHeadersImpl,
237 options ? options.maxHeadersLength : undefined,
238 ),
239 createRootFormatContext(options ? options.namespaceURI : undefined),
240 options ? options.progressiveChunkSize : undefined,
241 options ? options.onError : undefined,
242 options ? options.onBrowserBailout : undefined,
243 onAllReady,
244 undefined,
245 undefined,
246 onFatalError,
247 );
248 if (options && options.signal) {
249 attachAbortSignal(request, options.signal);
250 }
251 startWork(request);
252 });
253 }
254
255 type ResumeOptions = {
256 nonce?: NonceOption,
257 signal?: AbortSignal,
258 onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
259 onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void,
260 };
261
262 function resumeAndPrerenderToNodeStream(
263 children: ReactNodeList,
264 postponedState: PostponedState,
265 options?: Omit<ResumeOptions, 'nonce'>,
266 ): Promise<StaticResult> {
267 return new Promise((resolve, reject) => {
268 const onFatalError = reject;
269
270 function onAllReady() {
271 const readable: Readable = new Readable({
272 read() {
273 startFlowing(request, writable);
274 },
275 });
276 const writable = createFakeWritableFromReadable(readable);
277
278 const result = {
279 postponed: getPostponedState(request),
280 prelude: readable,
281 };
282 resolve(result);
283 }
284 const request = resumeAndPrerenderRequest(
285 children,
286 postponedState,
287 resumeRenderState(postponedState.resumableState, undefined),
288 options ? options.onError : undefined,
289 options ? options.onBrowserBailout : undefined,
290 onAllReady,
291 undefined,
292 undefined,
293 onFatalError,
294 );
295 if (options && options.signal) {
296 attachAbortSignal(request, options.signal);
297 }
298 startWork(request);
299 });
300 }
301
302 function resumeAndPrerender(
303 children: ReactNodeList,
304 postponedState: PostponedState,
305 options?: Omit<ResumeOptions, 'nonce'>,
306 ): Promise<{
307 postponed: null | PostponedState,
308 prelude: ReadableStream,
309 }> {
310 return new Promise((resolve, reject) => {
311 const onFatalError = reject;
312
313 function onAllReady() {
314 let writable: Writable;
315 const stream = new ReadableStream(
316 {
317 type: 'bytes',
318 start: (controller): ?Promise<void> => {
319 writable =
320 createFakeWritableFromReadableStreamController(controller);
321 },
322 pull: (controller): ?Promise<void> => {
323 startFlowing(request, writable);
324 },
325 cancel: (reason): ?Promise<void> => {
326 stopFlowing(request);
327 abort(request, reason);
328 },
329 },
330 // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
331 // $FlowFixMe[incompatible-type]
332 {highWaterMark: 0},
333 );
334
335 const result = {
336 postponed: getPostponedState(request),
337 prelude: stream,
338 };
339 resolve(result);
340 }
341
342 const request = resumeAndPrerenderRequest(
343 children,
344 postponedState,
345 resumeRenderState(postponedState.resumableState, undefined),
346 options ? options.onError : undefined,
347 options ? options.onBrowserBailout : undefined,
348 onAllReady,
349 undefined,
350 undefined,
351 onFatalError,
352 );
353 if (options && options.signal) {
354 attachAbortSignal(request, options.signal);
355 }
356 startWork(request);
357 });
358 }
359
360 export {
361 prerender,
362 prerenderToNodeStream,
363 resumeAndPrerender,
364 resumeAndPrerenderToNodeStream,
365 ReactVersion as version,
366 };