/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import type { Destination, Chunk, PrecomputedChunk, } from 'react-server/src/ReactServerStreamConfig'; import { writeChunk, stringToChunk, stringToPrecomputedChunk, } from 'react-server/src/ReactServerStreamConfig'; import escapeTextForBrowser from './escapeTextForBrowser'; import invariant from 'shared/invariant'; // Per response, export type ResponseState = { placeholderPrefix: PrecomputedChunk, segmentPrefix: PrecomputedChunk, boundaryPrefix: string, opaqueIdentifierPrefix: PrecomputedChunk, nextSuspenseID: number, 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( identifierPrefix: string = '', ): ResponseState { return { placeholderPrefix: stringToPrecomputedChunk(identifierPrefix + 'P:'), segmentPrefix: stringToPrecomputedChunk(identifierPrefix + 'S:'), boundaryPrefix: identifierPrefix + 'B:', opaqueIdentifierPrefix: stringToPrecomputedChunk(identifierPrefix + 'R:'), nextSuspenseID: 0, 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 = { formattedID: null | PrecomputedChunk, }; export function createSuspenseBoundaryID( responseState: ResponseState, ): SuspenseBoundaryID { return {formattedID: null}; } function encodeHTMLIDAttribute(value: string): string { return escapeTextForBrowser(value); } function encodeHTMLTextNode(text: string): string { return escapeTextForBrowser(text); } function assignAnID( responseState: ResponseState, id: SuspenseBoundaryID, ): PrecomputedChunk { // TODO: This approach doesn't yield deterministic results since this is assigned during render. const generatedID = responseState.nextSuspenseID++; return (id.formattedID = stringToPrecomputedChunk( responseState.boundaryPrefix + generatedID.toString(16), )); } const dummyNode1 = stringToPrecomputedChunk(''); function pushDummyNodeWithID( target: Array, responseState: ResponseState, assignID: SuspenseBoundaryID, ): void { const id = assignAnID(responseState, assignID); target.push(dummyNode1, id, dummyNode2); } export function pushEmpty( target: Array, responseState: ResponseState, assignID: null | SuspenseBoundaryID, ): void { if (assignID !== null) { pushDummyNodeWithID(target, responseState, assignID); } } const textSeparator = stringToPrecomputedChunk(''); export function pushTextInstance( target: Array, text: string, responseState: ResponseState, assignID: null | SuspenseBoundaryID, ): void { if (assignID !== null) { pushDummyNodeWithID(target, responseState, assignID); } if (text === '') { // Empty text doesn't have a DOM node representation and the hydration is aware of this. return; } // TODO: Avoid adding a text separator in common cases. target.push(stringToChunk(encodeHTMLTextNode(text)), textSeparator); } const startTag1 = stringToPrecomputedChunk('<'); const startTag2 = stringToPrecomputedChunk('>'); const idAttr = stringToPrecomputedChunk(' id="'); const attrEnd = stringToPrecomputedChunk('"'); export function pushStartInstance( target: Array, type: string, props: Object, responseState: ResponseState, assignID: null | SuspenseBoundaryID, ): void { // TODO: Figure out if it's self closing and everything else. if (assignID !== null) { let encodedID; if (typeof props.id === 'string') { // We can reuse the existing ID for our purposes. encodedID = assignID.formattedID = stringToPrecomputedChunk( encodeHTMLIDAttribute(props.id), ); } else { encodedID = assignAnID(responseState, assignID); } target.push( startTag1, stringToChunk(type), idAttr, encodedID, attrEnd, startTag2, ); } else { target.push(startTag1, stringToChunk(type), startTag2); } } const endTag1 = stringToPrecomputedChunk(''); export function pushEndInstance( target: Array, type: string, props: Object, ): void { // TODO: Figure out if it was self closing. target.push(endTag1, stringToChunk(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 = stringToPrecomputedChunk(''); export function writePlaceholder( destination: Destination, responseState: ResponseState, id: number, ): boolean { // TODO: This needs to be contextually aware and switch tag since not all parents allow for spans like //