main
js 397 lines 9.84 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 /**
11 * This is a renderer of React that doesn't have a render target output.
12 * It is useful to demonstrate the internals of the reconciler in isolation
13 * and for testing semantics of reconciliation separate from the host
14 * environment.
15 */
16
17 import type {ReactNodeList} from 'shared/ReactTypes';
18
19 import ReactFizzServer from 'react-server';
20
21 type Instance = {
22 type: string,
23 children: Array<Instance | TextInstance | SuspenseInstance>,
24 prop: any,
25 hidden: boolean,
26 };
27
28 type TextInstance = {
29 text: string,
30 hidden: boolean,
31 };
32
33 type ActivityInstance = {
34 children: Array<Instance | TextInstance | SuspenseInstance>,
35 };
36
37 type SuspenseInstance = {
38 state: 'pending' | 'complete' | 'client-render',
39 children: Array<Instance | TextInstance | SuspenseInstance>,
40 };
41
42 type Placeholder = {
43 parent: Instance | Segment | SuspenseInstance,
44 index: number,
45 };
46
47 type Segment = {
48 children: Array<Instance | TextInstance | SuspenseInstance>,
49 };
50
51 type Destination = {
52 root: null | Instance | TextInstance | SuspenseInstance,
53 placeholders: Map<number, Placeholder>,
54 segments: Map<number, Segment>,
55 stack: Array<Segment | Instance | SuspenseInstance>,
56 };
57
58 type ResumableState = null;
59 type RenderState = null;
60 type HoistableState = null;
61 type PreambleState = null;
62
63 const POP = Buffer.from('/', 'utf8');
64
65 function write(destination: Destination, buffer: Uint8Array): void {
66 const stack = destination.stack;
67 if (buffer === POP) {
68 stack.pop();
69 return;
70 }
71 // We assume one chunk is one instance.
72 const instance = JSON.parse(Buffer.from(buffer as any).toString('utf8'));
73 if (stack.length === 0) {
74 destination.root = instance;
75 } else {
76 const parent = stack[stack.length - 1];
77 parent.children.push(instance);
78 }
79 stack.push(instance);
80 }
81
82 // $FlowFixMe[prop-missing]
83 // $FlowFixMe[incompatible-type]
84 const ReactNoopServer = ReactFizzServer({
85 scheduleMicrotask(callback: () => void) {
86 callback();
87 },
88 scheduleWork(callback: () => void) {
89 callback();
90 },
91 beginWriting(destination: Destination): void {},
92 writeChunk(destination: Destination, buffer: Uint8Array): void {
93 write(destination, buffer);
94 },
95 writeChunkAndReturn(destination: Destination, buffer: Uint8Array): boolean {
96 write(destination, buffer);
97 return true;
98 },
99 completeWriting(destination: Destination): void {},
100 close(destination: Destination): void {},
101 closeWithError(destination: Destination, error: mixed): void {},
102 flushBuffered(destination: Destination): void {},
103
104 byteLengthOfChunk: null,
105
106 getChildFormatContext(): null {
107 return null;
108 },
109 getSuspenseFallbackFormatContext(): null {
110 return null;
111 },
112 getSuspenseContentFormatContext(): null {
113 return null;
114 },
115
116 getViewTransitionFormatContext(): null {
117 return null;
118 },
119
120 resetResumableState(): void {},
121 completeResumableState(): void {},
122
123 pushTextInstance(
124 target: Array<Uint8Array>,
125 text: string,
126 renderState: RenderState,
127 textEmbedded: boolean,
128 ): boolean {
129 const textInstance: TextInstance = {
130 text,
131 hidden: false,
132 };
133 target.push(Buffer.from(JSON.stringify(textInstance), 'utf8'), POP);
134 return false;
135 },
136 pushStartInstance(
137 target: Array<Uint8Array>,
138 type: string,
139 props: Object,
140 ): ReactNodeList {
141 const instance: Instance = {
142 type: type,
143 children: [],
144 prop: props.prop,
145 hidden: false,
146 };
147 target.push(Buffer.from(JSON.stringify(instance), 'utf8'));
148 return props.children;
149 },
150
151 pushEndInstance(
152 target: Array<Uint8Array>,
153 type: string,
154 props: Object,
155 ): void {
156 target.push(POP);
157 },
158
159 // This is a noop in ReactNoop
160 pushSegmentFinale(
161 target: Array<Uint8Array>,
162 renderState: RenderState,
163 lastPushedText: boolean,
164 textEmbedded: boolean,
165 ): void {},
166
167 writeCompletedRoot(
168 destination: Destination,
169 resumableState: ResumableState,
170 renderState: RenderState,
171 isComplete: boolean,
172 ): boolean {
173 return true;
174 },
175
176 writePlaceholder(
177 destination: Destination,
178 renderState: RenderState,
179 id: number,
180 // $FlowFixMe[incompatible-type]
181 ): boolean {
182 const parent = destination.stack[destination.stack.length - 1];
183 destination.placeholders.set(id, {
184 parent: parent,
185 index: parent.children.length,
186 });
187 },
188
189 pushStartActivityBoundary(
190 target: Array<Uint8Array>,
191 renderState: RenderState,
192 ): void {
193 const activityInstance: ActivityInstance = {
194 children: [],
195 };
196 target.push(Buffer.from(JSON.stringify(activityInstance), 'utf8'));
197 },
198
199 pushEndActivityBoundary(
200 target: Array<Uint8Array>,
201 renderState: RenderState,
202 ): void {
203 target.push(POP);
204 },
205
206 writeStartCompletedSuspenseBoundary(
207 destination: Destination,
208 renderState: RenderState,
209 ): boolean {
210 const suspenseInstance: SuspenseInstance = {
211 state: 'complete',
212 children: [],
213 };
214 const parent = destination.stack[destination.stack.length - 1];
215 parent.children.push(suspenseInstance);
216 destination.stack.push(suspenseInstance);
217 return true;
218 },
219 writeStartPendingSuspenseBoundary(
220 destination: Destination,
221 renderState: RenderState,
222 ): boolean {
223 const suspenseInstance: SuspenseInstance = {
224 state: 'pending',
225 children: [],
226 };
227 const parent = destination.stack[destination.stack.length - 1];
228 parent.children.push(suspenseInstance);
229 destination.stack.push(suspenseInstance);
230 return true;
231 },
232 writeStartClientRenderedSuspenseBoundary(
233 destination: Destination,
234 renderState: RenderState,
235 ): boolean {
236 const suspenseInstance: SuspenseInstance = {
237 state: 'client-render',
238 children: [],
239 };
240 const parent = destination.stack[destination.stack.length - 1];
241 parent.children.push(suspenseInstance);
242 destination.stack.push(suspenseInstance);
243 return true;
244 },
245 writeEndCompletedSuspenseBoundary(destination: Destination): boolean {
246 destination.stack.pop();
247 return true;
248 },
249 writeEndPendingSuspenseBoundary(destination: Destination): boolean {
250 destination.stack.pop();
251 return true;
252 },
253 writeEndClientRenderedSuspenseBoundary(destination: Destination): boolean {
254 destination.stack.pop();
255 return true;
256 },
257
258 writeStartSegment(
259 destination: Destination,
260 renderState: RenderState,
261 formatContext: null,
262 id: number,
263 ): boolean {
264 const segment: Segment = {
265 children: [],
266 };
267 destination.segments.set(id, segment);
268 if (destination.stack.length > 0) {
269 throw new Error('Segments are only expected at the root of the stack.');
270 }
271 destination.stack.push(segment);
272 return true;
273 },
274 writeEndSegment(destination: Destination, formatContext: null): boolean {
275 destination.stack.pop();
276 return true;
277 },
278
279 writeCompletedSegmentInstruction(
280 destination: Destination,
281 renderState: RenderState,
282 contentSegmentID: number,
283 ): boolean {
284 const segment = destination.segments.get(contentSegmentID);
285 if (!segment) {
286 throw new Error('Missing segment.');
287 }
288 const placeholder = destination.placeholders.get(contentSegmentID);
289 if (!placeholder) {
290 throw new Error('Missing placeholder.');
291 }
292 placeholder.parent.children.splice(
293 placeholder.index,
294 0,
295 ...segment.children,
296 );
297 return true;
298 },
299
300 writeCompletedBoundaryInstruction(
301 destination: Destination,
302 renderState: RenderState,
303 boundary: SuspenseInstance,
304 contentSegmentID: number,
305 ): boolean {
306 const segment = destination.segments.get(contentSegmentID);
307 if (!segment) {
308 throw new Error('Missing segment.');
309 }
310 boundary.children = segment.children;
311 boundary.state = 'complete';
312 return true;
313 },
314
315 writeClientRenderBoundaryInstruction(
316 destination: Destination,
317 renderState: RenderState,
318 boundary: SuspenseInstance,
319 ): boolean {
320 // $FlowFixMe[prop-missing]
321 boundary.status = 'client-render';
322 return true;
323 },
324
325 writePreambleStart() {},
326 writePreambleEnd() {},
327 writeHoistables() {},
328 writeHoistablesForBoundary() {},
329 writePostamble() {},
330 hoistHoistables(parent: HoistableState, child: HoistableState) {},
331 hasSuspenseyContent(
332 hoistableState: HoistableState,
333 flushingInShell: boolean,
334 ): boolean {
335 return false;
336 },
337 createHoistableState(): HoistableState {
338 return null;
339 },
340 emitEarlyPreloads() {},
341 createPreambleState(): PreambleState {
342 return null;
343 },
344 canHavePreamble() {
345 return false;
346 },
347 hoistPreambleState() {},
348 isPreambleReady() {
349 return true;
350 },
351 isPreambleContext() {
352 return false;
353 },
354 });
355
356 type Options = {
357 progressiveChunkSize?: number,
358 onShellReady?: () => void,
359 onAllReady?: () => void,
360 onError?: (error: mixed) => ?string,
361 onBrowserBailout?: (error: mixed) => void,
362 };
363
364 function render(children: React$Element<any>, options?: Options): Destination {
365 // $FlowFixMe[prop-missing]
366 // $FlowFixMe[incompatible-type]
367 const destination: Destination = {
368 root: null,
369 placeholders: new Map(),
370 segments: new Map(),
371 stack: [],
372 abort() {
373 ReactNoopServer.abort(request);
374 },
375 };
376 const request = ReactNoopServer.createRequest(
377 // $FlowFixMe[incompatible-type]
378 children,
379 // $FlowFixMe[incompatible-type]
380 null,
381 // $FlowFixMe[incompatible-type]
382 null,
383 // $FlowFixMe[incompatible-type]
384 null,
385 options ? options.progressiveChunkSize : undefined,
386 options ? options.onError : undefined,
387 options ? options.onBrowserBailout : undefined,
388 options ? options.onAllReady : undefined,
389 options ? options.onShellReady : undefined,
390 );
391 ReactNoopServer.startWork(request);
392 // $FlowFixMe[incompatible-type]
393 ReactNoopServer.startFlowing(request, destination);
394 return destination;
395 }
396
397 export {render};