main
js 198 lines 5.58 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 ReactVersion from 'shared/ReactVersion';
19
20 import {
21 createPrerenderRequest,
22 resumeAndPrerenderRequest,
23 startWork,
24 startFlowing,
25 stopFlowing,
26 abort,
27 attachAbortSignal,
28 getPostponedState,
29 } from 'react-server/src/ReactFizzServer';
30
31 import {
32 createResumableState,
33 createRenderState,
34 resumeRenderState,
35 createRootFormatContext,
36 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
37
38 import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
39 ensureCorrectIsomorphicReactVersion();
40
41 type NonceOption =
42 | string
43 | {
44 script?: string,
45 style?: string,
46 };
47
48 type Options = {
49 identifierPrefix?: string,
50 namespaceURI?: string,
51 bootstrapScriptContent?: string,
52 bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
53 bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
54 progressiveChunkSize?: number,
55 signal?: AbortSignal,
56 onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
57 onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void,
58 unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
59 importMap?: ImportMap,
60 onHeaders?: (headers: Headers) => void,
61 maxHeadersLength?: number,
62 };
63
64 type StaticResult = {
65 postponed: null | PostponedState,
66 prelude: ReadableStream,
67 };
68
69 function prerender(
70 children: ReactNodeList,
71 options?: Options,
72 ): Promise<StaticResult> {
73 return new Promise((resolve, reject) => {
74 const onFatalError = reject;
75
76 function onAllReady() {
77 const stream = new ReadableStream(
78 {
79 type: 'bytes',
80 pull: (controller): ?Promise<void> => {
81 startFlowing(request, controller);
82 },
83 cancel: (reason): ?Promise<void> => {
84 stopFlowing(request);
85 abort(request, reason);
86 },
87 },
88 // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
89 // $FlowFixMe[incompatible-type]
90 {highWaterMark: 0},
91 );
92
93 const result: StaticResult = {
94 postponed: getPostponedState(request),
95 prelude: stream,
96 };
97 resolve(result);
98 }
99
100 const onHeaders = options ? options.onHeaders : undefined;
101 let onHeadersImpl;
102 if (onHeaders) {
103 onHeadersImpl = (headersDescriptor: HeadersDescriptor) => {
104 onHeaders(new Headers(headersDescriptor));
105 };
106 }
107 const resources = createResumableState(
108 options ? options.identifierPrefix : undefined,
109 options ? options.unstable_externalRuntimeSrc : undefined,
110 options ? options.bootstrapScriptContent : undefined,
111 options ? options.bootstrapScripts : undefined,
112 options ? options.bootstrapModules : undefined,
113 );
114 const request = createPrerenderRequest(
115 children,
116 resources,
117 createRenderState(
118 resources,
119 undefined, // nonce is not compatible with prerendered bootstrap scripts
120 options ? options.unstable_externalRuntimeSrc : undefined,
121 options ? options.importMap : undefined,
122 onHeadersImpl,
123 options ? options.maxHeadersLength : undefined,
124 ),
125 createRootFormatContext(options ? options.namespaceURI : undefined),
126 options ? options.progressiveChunkSize : undefined,
127 options ? options.onError : undefined,
128 options ? options.onBrowserBailout : undefined,
129 onAllReady,
130 undefined,
131 undefined,
132 onFatalError,
133 );
134 if (options && options.signal) {
135 attachAbortSignal(request, options.signal);
136 }
137 startWork(request);
138 });
139 }
140
141 type ResumeOptions = {
142 nonce?: NonceOption,
143 signal?: AbortSignal,
144 onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
145 onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void,
146 };
147
148 function resumeAndPrerender(
149 children: ReactNodeList,
150 postponedState: PostponedState,
151 options?: Omit<ResumeOptions, 'nonce'>,
152 ): Promise<StaticResult> {
153 return new Promise((resolve, reject) => {
154 const onFatalError = reject;
155
156 function onAllReady() {
157 const stream = new ReadableStream(
158 {
159 type: 'bytes',
160 pull: (controller): ?Promise<void> => {
161 startFlowing(request, controller);
162 },
163 cancel: (reason): ?Promise<void> => {
164 stopFlowing(request);
165 abort(request, reason);
166 },
167 },
168 // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
169 // $FlowFixMe[incompatible-type]
170 {highWaterMark: 0},
171 );
172
173 const result = {
174 postponed: getPostponedState(request),
175 prelude: stream,
176 };
177 resolve(result);
178 }
179
180 const request = resumeAndPrerenderRequest(
181 children,
182 postponedState,
183 resumeRenderState(postponedState.resumableState, undefined),
184 options ? options.onError : undefined,
185 options ? options.onBrowserBailout : undefined,
186 onAllReady,
187 undefined,
188 undefined,
189 onFatalError,
190 );
191 if (options && options.signal) {
192 attachAbortSignal(request, options.signal);
193 }
194 startWork(request);
195 });
196 }
197
198 export {prerender, resumeAndPrerender, ReactVersion as version};