main
js 234 lines 6.87 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 {LazyComponent} from 'react/src/ReactLazy';
12 import type {ErrorInfo} from 'react-server/src/ReactFizzServer';
13
14 import ReactVersion from 'shared/ReactVersion';
15
16 import ReactSharedInternalsServer from 'react-server/src/ReactSharedInternalsServer';
17 import ReactSharedInternalsClient from 'shared/ReactSharedInternals';
18
19 import {
20 createRequest as createFlightRequest,
21 startWork as startFlightWork,
22 startFlowing as startFlightFlowing,
23 abort as abortFlight,
24 attachAbortSignal as attachFlightAbortSignal,
25 } from 'react-server/src/ReactFlightServer';
26
27 import {
28 createResponse as createFlightResponse,
29 createStreamState as createFlightStreamState,
30 getRoot as getFlightRoot,
31 processStringChunk as processFlightStringChunk,
32 close as closeFlight,
33 } from 'react-client/src/ReactFlightClient';
34
35 import {
36 createRequest as createFizzRequest,
37 startWork as startFizzWork,
38 startFlowing as startFizzFlowing,
39 abort as abortFizz,
40 attachAbortSignal as attachFizzAbortSignal,
41 } from 'react-server/src/ReactFizzServer';
42
43 import {
44 createResumableState,
45 createRenderState,
46 createRootFormatContext,
47 } from './ReactFizzConfigMarkup';
48
49 type ReactMarkupNodeList =
50 // This is the intersection of ReactNodeList and ReactClientValue minus
51 // Client/ServerReferences.
52 | component(...props: any)
53 | LazyComponent<ReactMarkupNodeList, any>
54 | React$Element<string>
55 | string
56 | boolean
57 | number
58 | symbol
59 | null
60 | void
61 | bigint
62 | $AsyncIterable<ReactMarkupNodeList, ReactMarkupNodeList, void>
63 | $AsyncIterator<ReactMarkupNodeList, ReactMarkupNodeList, void>
64 | Iterable<ReactMarkupNodeList>
65 | Iterator<ReactMarkupNodeList>
66 | Array<ReactMarkupNodeList>
67 | Promise<ReactMarkupNodeList>; // Thenable<ReactMarkupNodeList>
68
69 type MarkupOptions = {
70 identifierPrefix?: string,
71 signal?: AbortSignal,
72 onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
73 };
74
75 function noServerCallOrFormAction() {
76 throw new Error(
77 'renderToHTML should not have emitted Server References. This is a bug in React.',
78 );
79 }
80
81 export function experimental_renderToHTML(
82 children: ReactMarkupNodeList,
83 options?: MarkupOptions,
84 ): Promise<string> {
85 return new Promise((resolve, reject) => {
86 const flightResponse = createFlightResponse(
87 null,
88 null,
89 null,
90 noServerCallOrFormAction,
91 noServerCallOrFormAction,
92 undefined,
93 undefined,
94 false,
95 undefined,
96 false,
97 undefined,
98 undefined,
99 undefined,
100 );
101 const streamState = createFlightStreamState(flightResponse, null);
102 const flightDestination = {
103 push(chunk: string | null): boolean {
104 if (chunk !== null) {
105 processFlightStringChunk(flightResponse, streamState, chunk);
106 } else {
107 closeFlight(flightResponse);
108 }
109 return true;
110 },
111 destroy(error: mixed): void {
112 abortFizz(fizzRequest, error);
113 reject(error);
114 },
115 };
116 let buffer = '';
117 const fizzDestination = {
118 // $FlowFixMe[missing-local-annot]
119 push(chunk) {
120 if (chunk !== null) {
121 buffer += chunk;
122 } else {
123 // null indicates that we finished
124 resolve(buffer);
125 }
126 return true;
127 },
128 // $FlowFixMe[missing-local-annot]
129 destroy(error) {
130 abortFlight(flightRequest, error);
131 reject(error);
132 },
133 };
134
135 let stashErrorIdx = 1;
136 const stashedErrors: Map<string, mixed> = new Map();
137
138 function handleFlightError(error: mixed): string {
139 // For Flight errors we don't immediately reject, because they might not matter
140 // to the output of the HTML. We stash the error with a digest in case we need
141 // to get to the original error from the Fizz render.
142 const id = '' + stashErrorIdx++;
143 stashedErrors.set(id, error);
144 return id;
145 }
146
147 function handleError(error: mixed, errorInfo: ErrorInfo) {
148 if (typeof error === 'object' && error !== null) {
149 const id = error.digest;
150 // Note that the original error might be `undefined` so we need a has check.
151 if (typeof id === 'string' && stashedErrors.has(id)) {
152 // Get the original error thrown inside Flight.
153 error = stashedErrors.get(id);
154 }
155 }
156
157 // Any error rejects the promise, regardless of where it happened.
158 // Unlike other React SSR we don't want to put Suspense boundaries into
159 // client rendering mode because there's no client rendering here.
160 reject(error);
161
162 const onError = options && options.onError;
163 if (onError) {
164 if (__DEV__) {
165 const prevGetCurrentStackImpl =
166 ReactSharedInternalsServer.getCurrentStack;
167 // We're inside a "client" callback from Fizz but we only have access to the
168 // "server" runtime so to get access to a stack trace within this callback we
169 // need to override it to get it from the client runtime.
170 ReactSharedInternalsServer.getCurrentStack =
171 ReactSharedInternalsClient.getCurrentStack;
172 try {
173 onError(error, errorInfo);
174 } finally {
175 ReactSharedInternalsServer.getCurrentStack =
176 prevGetCurrentStackImpl;
177 }
178 } else {
179 onError(error, errorInfo);
180 }
181 }
182 }
183 const flightRequest = createFlightRequest(
184 // $FlowFixMe[incompatible-type]: This should be a subtype but not everything is typed covariant.
185 children,
186 null,
187 handleFlightError,
188 options ? options.identifierPrefix : undefined,
189 undefined,
190 undefined,
191 'Markup',
192 undefined,
193 false,
194 );
195 const resumableState = createResumableState(
196 options ? options.identifierPrefix : undefined,
197 undefined,
198 );
199 const root = getFlightRoot<ReactNodeList>(flightResponse);
200 const fizzRequest = createFizzRequest(
201 // $FlowFixMe[incompatible-type]: Thenables as children are supported.
202 root,
203 resumableState,
204 createRenderState(
205 resumableState,
206 undefined,
207 undefined,
208 undefined,
209 undefined,
210 undefined,
211 ),
212 createRootFormatContext(),
213 Infinity,
214 handleError,
215 undefined,
216 undefined,
217 undefined,
218 undefined,
219 undefined,
220 undefined,
221 );
222 const signal = options ? options.signal : undefined;
223 if (signal) {
224 attachFlightAbortSignal(flightRequest, signal);
225 attachFizzAbortSignal(fizzRequest, signal);
226 }
227 startFlightWork(flightRequest);
228 startFlightFlowing(flightRequest, flightDestination);
229 startFizzWork(fizzRequest);
230 startFizzFlowing(fizzRequest, fizzDestination);
231 });
232 }
233
234 export {ReactVersion as version};