main
js 200 lines 5.62 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
108 const resources = createResumableState(
109 options ? options.identifierPrefix : undefined,
110 options ? options.unstable_externalRuntimeSrc : undefined,
111 options ? options.bootstrapScriptContent : undefined,
112 options ? options.bootstrapScripts : undefined,
113 options ? options.bootstrapModules : undefined,
114 );
115 const request = createPrerenderRequest(
116 children,
117 resources,
118 createRenderState(
119 resources,
120 undefined, // nonce is not compatible with prerendered bootstrap scripts
121 options ? options.unstable_externalRuntimeSrc : undefined,
122 options ? options.importMap : undefined,
123 onHeadersImpl,
124 options ? options.maxHeadersLength : undefined,
125 ),
126 createRootFormatContext(options ? options.namespaceURI : undefined),
127 options ? options.progressiveChunkSize : undefined,
128 options ? options.onError : undefined,
129 options ? options.onBrowserBailout : undefined,
130 onAllReady,
131 undefined,
132 undefined,
133 onFatalError,
134 );
135 if (options && options.signal) {
136 attachAbortSignal(request, options.signal);
137 }
138 startWork(request);
139 });
140 }
141
142 type ResumeOptions = {
143 nonce?: NonceOption,
144 signal?: AbortSignal,
145 onError?: (error: mixed) => ?string,
146 onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void,
147 unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
148 };
149
150 function resumeAndPrerender(
151 children: ReactNodeList,
152 postponedState: PostponedState,
153 options?: Omit<ResumeOptions, 'nonce'>,
154 ): Promise<StaticResult> {
155 return new Promise((resolve, reject) => {
156 const onFatalError = reject;
157
158 function onAllReady() {
159 const stream = new ReadableStream(
160 {
161 type: 'bytes',
162 pull: (controller): ?Promise<void> => {
163 startFlowing(request, controller);
164 },
165 cancel: (reason): ?Promise<void> => {
166 stopFlowing(request);
167 abort(request, reason);
168 },
169 },
170 // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
171 // $FlowFixMe[incompatible-type]
172 {highWaterMark: 0},
173 );
174
175 const result = {
176 postponed: getPostponedState(request),
177 prelude: stream,
178 };
179 resolve(result);
180 }
181
182 const request = resumeAndPrerenderRequest(
183 children,
184 postponedState,
185 resumeRenderState(postponedState.resumableState, undefined),
186 options ? options.onError : undefined,
187 options ? options.onBrowserBailout : undefined,
188 onAllReady,
189 undefined,
190 undefined,
191 onFatalError,
192 );
193 if (options && options.signal) {
194 attachAbortSignal(request, options.signal);
195 }
196 startWork(request);
197 });
198 }
199
200 export {prerender, resumeAndPrerender, ReactVersion as version};