diff --git a/packages/react-noop-renderer/src/ReactNoopFlightServer.js b/packages/react-noop-renderer/src/ReactNoopFlightServer.js index f080f27addfdc..bbace47e2eb4b 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightServer.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightServer.js @@ -36,12 +36,6 @@ const ReactNoopFlightServer = ReactFlightServer({ convertStringToBuffer(content: string): Uint8Array { return Buffer.from(content, 'utf8'); }, - formatChunkAsString(type: string, props: Object): string { - return JSON.stringify({type, props}); - }, - formatChunk(type: string, props: Object): Uint8Array { - return Buffer.from(JSON.stringify({type, props}), 'utf8'); - }, isModuleReference(reference: Object): boolean { return reference.$$typeof === Symbol.for('react.module.reference'); }, diff --git a/packages/react-noop-renderer/src/ReactNoopServer.js b/packages/react-noop-renderer/src/ReactNoopServer.js index 0f21bbd6dc626..3be95ab051692 100644 --- a/packages/react-noop-renderer/src/ReactNoopServer.js +++ b/packages/react-noop-renderer/src/ReactNoopServer.js @@ -16,7 +16,40 @@ import ReactFizzServer from 'react-server'; -type Destination = Array; +type Instance = {| + type: string, + children: Array, + prop: any, + hidden: boolean, +|}; + +type TextInstance = {| + text: string, + hidden: boolean, +|}; + +type SuspenseInstance = {| + state: 'pending' | 'complete' | 'client-render', + children: Array, +|}; + +type Placeholder = { + parent: Instance | SuspenseInstance, + index: number, +}; + +type Segment = { + children: null | Instance | TextInstance | SuspenseInstance, +}; + +type Destination = { + root: null | Instance | TextInstance | SuspenseInstance, + placeholders: Map, + segments: Map, + stack: Array, +}; + +const POP = Buffer.from('/', 'utf8'); const ReactNoopServer = ReactFizzServer({ scheduleWork(callback: () => void) { @@ -24,24 +57,165 @@ const ReactNoopServer = ReactFizzServer({ }, beginWriting(destination: Destination): void {}, writeChunk(destination: Destination, buffer: Uint8Array): void { - destination.push(JSON.parse(Buffer.from((buffer: any)).toString('utf8'))); + const stack = destination.stack; + if (buffer === POP) { + stack.pop(); + return; + } + // We assume one chunk is one instance. + const instance = JSON.parse(Buffer.from((buffer: any)).toString('utf8')); + if (stack.length === 0) { + destination.root = instance; + } else { + const parent = stack[stack.length - 1]; + parent.children.push(instance); + } + stack.push(instance); }, completeWriting(destination: Destination): void {}, close(destination: Destination): void {}, flushBuffered(destination: Destination): void {}, - convertStringToBuffer(content: string): Uint8Array { - return Buffer.from(content, 'utf8'); + + createResponseState(): null { + return null; }, - formatChunkAsString(type: string, props: Object): string { - return JSON.stringify({type, props}); + createSuspenseBoundaryID(): SuspenseInstance { + // The ID is a pointer to the boundary itself. + return {state: 'pending', children: []}; + }, + + pushTextInstance(target: Array, text: string): void { + const textInstance: TextInstance = { + text, + hidden: false, + }; + target.push(Buffer.from(JSON.stringify(textInstance), 'utf8'), POP); + }, + pushStartInstance( + target: Array, + type: string, + props: Object, + ): void { + const instance: Instance = { + type: type, + children: [], + prop: props.prop, + hidden: false, + }; + target.push(Buffer.from(JSON.stringify(instance), 'utf8')); + }, + + pushEndInstance( + target: Array, + type: string, + props: Object, + ): void { + target.push(POP); + }, + + writePlaceholder(destination: Destination, id: number): boolean { + const parent = destination.stack[destination.stack.length - 1]; + destination.placeholders.set(id, { + parent: parent, + index: parent.children.length, + }); }, - formatChunk(type: string, props: Object): Uint8Array { - return Buffer.from(JSON.stringify({type, props}), 'utf8'); + + writeStartCompletedSuspenseBoundary( + destination: Destination, + suspenseInstance: SuspenseInstance, + ): boolean { + suspenseInstance.state = 'complete'; + const parent = destination.stack[destination.stack.length - 1]; + parent.children.push(suspenseInstance); + destination.stack.push(suspenseInstance); + }, + writeStartPendingSuspenseBoundary( + destination: Destination, + suspenseInstance: SuspenseInstance, + ): boolean { + suspenseInstance.state = 'pending'; + const parent = destination.stack[destination.stack.length - 1]; + parent.children.push(suspenseInstance); + destination.stack.push(suspenseInstance); + }, + writeStartClientRenderedSuspenseBoundary( + destination: Destination, + suspenseInstance: SuspenseInstance, + ): boolean { + suspenseInstance.state = 'client-render'; + const parent = destination.stack[destination.stack.length - 1]; + parent.children.push(suspenseInstance); + destination.stack.push(suspenseInstance); + }, + writeEndSuspenseBoundary(destination: Destination): boolean { + destination.stack.pop(); + }, + + writeStartSegment(destination: Destination, id: number): boolean { + const segment = { + children: [], + }; + destination.segments.set(id, segment); + if (destination.stack.length > 0) { + throw new Error('Segments are only expected at the root of the stack.'); + } + destination.stack.push(segment); + }, + writeEndSegment(destination: Destination): boolean { + destination.stack.pop(); + }, + + writeCompletedSegmentInstruction( + destination: Destination, + responseState: ResponseState, + contentSegmentID: number, + ): boolean { + const segment = destination.segments.get(contentSegmentID); + if (!segment) { + throw new Error('Missing segment.'); + } + const placeholder = destination.placeholders.get(contentSegmentID); + if (!placeholder) { + throw new Error('Missing placeholder.'); + } + placeholder.parent.children.splice( + placeholder.index, + 0, + ...segment.children, + ); + }, + + writeCompletedBoundaryInstruction( + destination: Destination, + responseState: ResponseState, + boundary: SuspenseInstance, + contentSegmentID: number, + ): boolean { + const segment = destination.segments.get(contentSegmentID); + if (!segment) { + throw new Error('Missing segment.'); + } + boundary.children = segment.children; + boundary.state = 'complete'; + }, + + writeClientRenderBoundaryInstruction( + destination: Destination, + responseState: ResponseState, + boundary: SuspenseInstance, + ): boolean { + boundary.status = 'client-render'; }, }); function render(children: React$Element): Destination { - const destination: Destination = []; + const destination: Destination = { + root: null, + placeholders: new Map(), + segments: new Map(), + stack: [], + }; const request = ReactNoopServer.createRequest(children, destination); ReactNoopServer.startWork(request); return destination; diff --git a/packages/react-server/src/ReactDOMServerFormatConfig.js b/packages/react-server/src/ReactDOMServerFormatConfig.js index 1e36890e995da..104834ca88ecd 100644 --- a/packages/react-server/src/ReactDOMServerFormatConfig.js +++ b/packages/react-server/src/ReactDOMServerFormatConfig.js @@ -7,17 +7,368 @@ * @flow */ -import {convertStringToBuffer} from 'react-server/src/ReactServerStreamConfig'; +import type {Destination} from 'react-server/src/ReactServerStreamConfig'; -export function formatChunkAsString(type: string, props: Object): string { - let str = '<' + type + '>'; - if (typeof props.children === 'string') { - str += props.children; +import { + writeChunk, + convertStringToBuffer, +} from 'react-server/src/ReactServerStreamConfig'; + +import invariant from 'shared/invariant'; + +// Per response, +export type ResponseState = { + sentCompleteSegmentFunction: boolean, + sentCompleteBoundaryFunction: boolean, + sentClientRenderFunction: boolean, +}; + +// Allows us to keep track of what we've already written so we can refer back to it. +export function createResponseState(): ResponseState { + return { + sentCompleteSegmentFunction: false, + sentCompleteBoundaryFunction: false, + sentClientRenderFunction: false, + }; +} + +// This object is used to lazily reuse the ID of the first generated node, or assign one. +// We can't assign an ID up front because the node we're attaching it to might already +// have one. So we need to lazily use that if it's available. +export type SuspenseBoundaryID = { + id: null | string, +}; + +export function createSuspenseBoundaryID( + responseState: ResponseState, +): SuspenseBoundaryID { + return {id: null}; +} + +function encodeHTMLIDAttribute(value: string): string { + // TODO: This needs to be encoded for security purposes. + return value; +} + +function encodeHTMLTextNode(text: string): string { + // TOOD: This needs to be encoded for security purposes. + return text; +} + +export function pushTextInstance( + target: Array, + text: string, +): void { + target.push(convertStringToBuffer(encodeHTMLTextNode(text))); +} + +const startTag1 = convertStringToBuffer('<'); +const startTag2 = convertStringToBuffer('>'); + +export function pushStartInstance( + target: Array, + type: string, + props: Object, +): void { + // TODO: Figure out if it's self closing and everything else. + target.push(startTag1, convertStringToBuffer(type), startTag2); +} + +const endTag1 = convertStringToBuffer(''); + +export function pushEndInstance( + target: Array, + type: string, + props: Object, +): void { + // TODO: Figure out if it was self closing. + target.push(endTag1, convertStringToBuffer(type), endTag2); +} + +// Structural Nodes + +// A placeholder is a node inside a hidden partial tree that can be filled in later, but before +// display. It's never visible to users. +const placeholder1 = convertStringToBuffer(''); +export function writePlaceholder( + destination: Destination, + id: number, +): boolean { + // TODO: This needs to be contextually aware and switch tag since not all parents allow for spans like + //