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 SuspenseInstance = {
    
  34.   state: 'pending' | 'complete' | 'client-render',
    
  35.   children: Array<Instance | TextInstance | SuspenseInstance>,
    
  36. };
    
  37. 
    
  38. type Placeholder = {
    
  39.   parent: Instance | SuspenseInstance,
    
  40.   index: number,
    
  41. };
    
  42. 
    
  43. type Segment = {
    
  44.   children: null | Instance | TextInstance | SuspenseInstance,
    
  45. };
    
  46. 
    
  47. type Destination = {
    
  48.   root: null | Instance | TextInstance | SuspenseInstance,
    
  49.   placeholders: Map<number, Placeholder>,
    
  50.   segments: Map<number, Segment>,
    
  51.   stack: Array<Segment | Instance | SuspenseInstance>,
    
  52. };
    
  53. 
    
  54. type RenderState = null;
    
  55. type BoundaryResources = null;
    
  56. 
    
  57. const POP = Buffer.from('/', 'utf8');
    
  58. 
    
  59. function write(destination: Destination, buffer: Uint8Array): void {
    
  60.   const stack = destination.stack;
    
  61.   if (buffer === POP) {
    
  62.     stack.pop();
    
  63.     return;
    
  64.   }
    
  65.   // We assume one chunk is one instance.
    
  66.   const instance = JSON.parse(Buffer.from((buffer: any)).toString('utf8'));
    
  67.   if (stack.length === 0) {
    
  68.     destination.root = instance;
    
  69.   } else {
    
  70.     const parent = stack[stack.length - 1];
    
  71.     parent.children.push(instance);
    
  72.   }
    
  73.   stack.push(instance);
    
  74. }
    
  75. 
    
  76. const ReactNoopServer = ReactFizzServer({
    
  77.   scheduleWork(callback: () => void) {
    
  78.     callback();
    
  79.   },
    
  80.   beginWriting(destination: Destination): void {},
    
  81.   writeChunk(destination: Destination, buffer: Uint8Array): void {
    
  82.     write(destination, buffer);
    
  83.   },
    
  84.   writeChunkAndReturn(destination: Destination, buffer: Uint8Array): boolean {
    
  85.     write(destination, buffer);
    
  86.     return true;
    
  87.   },
    
  88.   completeWriting(destination: Destination): void {},
    
  89.   close(destination: Destination): void {},
    
  90.   closeWithError(destination: Destination, error: mixed): void {},
    
  91.   flushBuffered(destination: Destination): void {},
    
  92. 
    
  93.   getChildFormatContext(): null {
    
  94.     return null;
    
  95.   },
    
  96. 
    
  97.   resetResumableState(): void {},
    
  98. 
    
  99.   pushTextInstance(
    
  100.     target: Array<Uint8Array>,
    
  101.     text: string,
    
  102.     renderState: RenderState,
    
  103.     textEmbedded: boolean,
    
  104.   ): boolean {
    
  105.     const textInstance: TextInstance = {
    
  106.       text,
    
  107.       hidden: false,
    
  108.     };
    
  109.     target.push(Buffer.from(JSON.stringify(textInstance), 'utf8'), POP);
    
  110.     return false;
    
  111.   },
    
  112.   pushStartInstance(
    
  113.     target: Array<Uint8Array>,
    
  114.     type: string,
    
  115.     props: Object,
    
  116.   ): ReactNodeList {
    
  117.     const instance: Instance = {
    
  118.       type: type,
    
  119.       children: [],
    
  120.       prop: props.prop,
    
  121.       hidden: false,
    
  122.     };
    
  123.     target.push(Buffer.from(JSON.stringify(instance), 'utf8'));
    
  124.     return props.children;
    
  125.   },
    
  126. 
    
  127.   pushEndInstance(
    
  128.     target: Array<Uint8Array>,
    
  129.     type: string,
    
  130.     props: Object,
    
  131.   ): void {
    
  132.     target.push(POP);
    
  133.   },
    
  134. 
    
  135.   // This is a noop in ReactNoop
    
  136.   pushSegmentFinale(
    
  137.     target: Array<Uint8Array>,
    
  138.     renderState: RenderState,
    
  139.     lastPushedText: boolean,
    
  140.     textEmbedded: boolean,
    
  141.   ): void {},
    
  142. 
    
  143.   writeCompletedRoot(
    
  144.     destination: Destination,
    
  145.     renderState: RenderState,
    
  146.   ): boolean {
    
  147.     return true;
    
  148.   },
    
  149. 
    
  150.   writePlaceholder(
    
  151.     destination: Destination,
    
  152.     renderState: RenderState,
    
  153.     id: number,
    
  154.   ): boolean {
    
  155.     const parent = destination.stack[destination.stack.length - 1];
    
  156.     destination.placeholders.set(id, {
    
  157.       parent: parent,
    
  158.       index: parent.children.length,
    
  159.     });
    
  160.   },
    
  161. 
    
  162.   writeStartCompletedSuspenseBoundary(
    
  163.     destination: Destination,
    
  164.     renderState: RenderState,
    
  165.     suspenseInstance: SuspenseInstance,
    
  166.   ): boolean {
    
  167.     suspenseInstance.state = 'complete';
    
  168.     const parent = destination.stack[destination.stack.length - 1];
    
  169.     parent.children.push(suspenseInstance);
    
  170.     destination.stack.push(suspenseInstance);
    
  171.   },
    
  172.   writeStartPendingSuspenseBoundary(
    
  173.     destination: Destination,
    
  174.     renderState: RenderState,
    
  175.     suspenseInstance: SuspenseInstance,
    
  176.   ): boolean {
    
  177.     suspenseInstance.state = 'pending';
    
  178.     const parent = destination.stack[destination.stack.length - 1];
    
  179.     parent.children.push(suspenseInstance);
    
  180.     destination.stack.push(suspenseInstance);
    
  181.   },
    
  182.   writeStartClientRenderedSuspenseBoundary(
    
  183.     destination: Destination,
    
  184.     renderState: RenderState,
    
  185.     suspenseInstance: SuspenseInstance,
    
  186.   ): boolean {
    
  187.     suspenseInstance.state = 'client-render';
    
  188.     const parent = destination.stack[destination.stack.length - 1];
    
  189.     parent.children.push(suspenseInstance);
    
  190.     destination.stack.push(suspenseInstance);
    
  191.   },
    
  192.   writeEndCompletedSuspenseBoundary(destination: Destination): boolean {
    
  193.     destination.stack.pop();
    
  194.   },
    
  195.   writeEndPendingSuspenseBoundary(destination: Destination): boolean {
    
  196.     destination.stack.pop();
    
  197.   },
    
  198.   writeEndClientRenderedSuspenseBoundary(destination: Destination): boolean {
    
  199.     destination.stack.pop();
    
  200.   },
    
  201. 
    
  202.   writeStartSegment(
    
  203.     destination: Destination,
    
  204.     renderState: RenderState,
    
  205.     formatContext: null,
    
  206.     id: number,
    
  207.   ): boolean {
    
  208.     const segment = {
    
  209.       children: [],
    
  210.     };
    
  211.     destination.segments.set(id, segment);
    
  212.     if (destination.stack.length > 0) {
    
  213.       throw new Error('Segments are only expected at the root of the stack.');
    
  214.     }
    
  215.     destination.stack.push(segment);
    
  216.   },
    
  217.   writeEndSegment(destination: Destination, formatContext: null): boolean {
    
  218.     destination.stack.pop();
    
  219.   },
    
  220. 
    
  221.   writeCompletedSegmentInstruction(
    
  222.     destination: Destination,
    
  223.     renderState: RenderState,
    
  224.     contentSegmentID: number,
    
  225.   ): boolean {
    
  226.     const segment = destination.segments.get(contentSegmentID);
    
  227.     if (!segment) {
    
  228.       throw new Error('Missing segment.');
    
  229.     }
    
  230.     const placeholder = destination.placeholders.get(contentSegmentID);
    
  231.     if (!placeholder) {
    
  232.       throw new Error('Missing placeholder.');
    
  233.     }
    
  234.     placeholder.parent.children.splice(
    
  235.       placeholder.index,
    
  236.       0,
    
  237.       ...segment.children,
    
  238.     );
    
  239.   },
    
  240. 
    
  241.   writeCompletedBoundaryInstruction(
    
  242.     destination: Destination,
    
  243.     renderState: RenderState,
    
  244.     boundary: SuspenseInstance,
    
  245.     contentSegmentID: number,
    
  246.   ): boolean {
    
  247.     const segment = destination.segments.get(contentSegmentID);
    
  248.     if (!segment) {
    
  249.       throw new Error('Missing segment.');
    
  250.     }
    
  251.     boundary.children = segment.children;
    
  252.     boundary.state = 'complete';
    
  253.   },
    
  254. 
    
  255.   writeClientRenderBoundaryInstruction(
    
  256.     destination: Destination,
    
  257.     renderState: RenderState,
    
  258.     boundary: SuspenseInstance,
    
  259.   ): boolean {
    
  260.     boundary.status = 'client-render';
    
  261.   },
    
  262. 
    
  263.   writePreamble() {},
    
  264.   writeHoistables() {},
    
  265.   writePostamble() {},
    
  266. 
    
  267.   createBoundaryResources(): BoundaryResources {
    
  268.     return null;
    
  269.   },
    
  270. 
    
  271.   setCurrentlyRenderingBoundaryResourcesTarget(resources: BoundaryResources) {},
    
  272. 
    
  273.   prepareHostDispatcher() {},
    
  274. });
    
  275. 
    
  276. type Options = {
    
  277.   progressiveChunkSize?: number,
    
  278.   onShellReady?: () => void,
    
  279.   onAllReady?: () => void,
    
  280.   onError?: (error: mixed) => ?string,
    
  281. };
    
  282. 
    
  283. function render(children: React$Element<any>, options?: Options): Destination {
    
  284.   const destination: Destination = {
    
  285.     root: null,
    
  286.     placeholders: new Map(),
    
  287.     segments: new Map(),
    
  288.     stack: [],
    
  289.     abort() {
    
  290.       ReactNoopServer.abort(request);
    
  291.     },
    
  292.   };
    
  293.   const request = ReactNoopServer.createRequest(
    
  294.     children,
    
  295.     null,
    
  296.     null,
    
  297.     options ? options.progressiveChunkSize : undefined,
    
  298.     options ? options.onError : undefined,
    
  299.     options ? options.onAllReady : undefined,
    
  300.     options ? options.onShellReady : undefined,
    
  301.   );
    
  302.   ReactNoopServer.startWork(request);
    
  303.   ReactNoopServer.startFlowing(request, destination);
    
  304.   return destination;
    
  305. }
    
  306. 
    
  307. export {render};