diff --git a/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js b/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js index e8aecf06c1550..320bb1ef7c551 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js +++ b/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js @@ -108,6 +108,7 @@ type BaseResource = { flushed: boolean, }; +export type LinkTagResource = PreloadResource | StyleResource | LinkResource; export type Resource = PreloadResource | StyleResource | ScriptResource; export type HeadResource = | TitleResource diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js b/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js index 830db39970308..3b55c8f2ef3ef 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js @@ -10,17 +10,12 @@ import { clientRenderBoundary, completeBoundaryWithStyles, + completeContainerWithStyles, completeBoundary, + completeContainer, completeSegment, } from './fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime'; -if (!window.$RC) { - // TODO: Eventually remove, we currently need to set these globals for - // compatibility with ReactDOMFizzInstructionSet - window.$RC = completeBoundary; - window.$RM = new Map(); -} - if (document.readyState === 'loading') { if (document.body != null) { installFizzInstrObserver(document.body); @@ -74,34 +69,47 @@ function installFizzInstrObserver(target /*: Node */) { } function handleNode(node_ /*: Node */) { - // $FlowFixMe[incompatible-cast] - if (node_.nodeType !== 1 || !(node_ /*: HTMLElement*/).dataset) { + if ( + node_.nodeType !== 1 || + // $FlowFixMe[incompatible-cast] + !(node_ /*: HTMLElement*/).dataset + ) { return; } // $FlowFixMe[incompatible-cast] const node = (node_ /*: HTMLElement*/); const dataset = node.dataset; - if (dataset['rxi'] != null) { - clientRenderBoundary( - dataset['bid'], + + if (dataset['ix'] != null) { + node.remove(); + return clientRenderBoundary( + dataset['ix'], dataset['dgst'], dataset['msg'], dataset['stck'], ); + } else if (dataset['is'] != null) { + node.remove(); + return completeSegment(dataset['is'], dataset['p']); + } else if (dataset['ir'] != null) { node.remove(); - } else if (dataset['rri'] != null) { - // Convert styles here, since its type is Array> - completeBoundaryWithStyles( - dataset['bid'], - dataset['sid'], - JSON.parse(dataset['sty']), + return completeBoundaryWithStyles( + dataset['ir'], + dataset['s'], + JSON.parse(dataset['r']), ); + } else if (dataset['ib'] != null) { node.remove(); - } else if (dataset['rci'] != null) { - completeBoundary(dataset['bid'], dataset['sid']); + return completeBoundary(dataset['ib'], dataset['s']); + } else if (dataset['ik'] != null) { node.remove(); - } else if (dataset['rsi'] != null) { - completeSegment(dataset['sid'], dataset['pid']); + return completeContainerWithStyles( + dataset['ik'], + dataset['s'], + JSON.parse(dataset['r']), + ); + } else if (dataset['ic'] != null) { node.remove(); + return completeContainer(dataset['ic'], dataset['s']); } } diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js index 921424e852e3f..a2cfe93023e8e 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js @@ -8,7 +8,11 @@ */ import type {ReactNodeList} from 'shared/ReactTypes'; -import type {Resources, BoundaryResources} from './ReactDOMFloatServer'; +import type { + Resources, + BoundaryResources, + LinkTagResource, +} from './ReactDOMFloatServer'; export type {Resources, BoundaryResources}; import { @@ -80,10 +84,11 @@ export { } from './ReactDOMFloatServer'; import { - clientRenderBoundary as clientRenderFunction, - completeBoundary as completeBoundaryFunction, - completeBoundaryWithStyles as styleInsertionFunction, - completeSegment as completeSegmentFunction, + clientRenderBoundary as clientRenderFunctionString, + completeBoundary as completeBoundaryFunctionString, + completeContainer as completeContainerFunctionString, + completeBoundaryWithStyles as styleInsertionFunctionString, + completeSegment as completeSegmentFunctionString, } from './fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings'; import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; @@ -117,6 +122,7 @@ const DataStreamingFormat: StreamingFormat = 1; // Per response, global state that is not contextual to the rendering subtree. export type ResponseState = { bootstrapChunks: Array, + fallbackBootstrapChunks: void | Array, placeholderPrefix: PrecomputedChunk, segmentPrefix: PrecomputedChunk, boundaryPrefix: string, @@ -127,6 +133,7 @@ export type ResponseState = { startInlineScript: PrecomputedChunk, sentCompleteSegmentFunction: boolean, sentCompleteBoundaryFunction: boolean, + sentCompleteContainerFunction: boolean, sentClientRenderFunction: boolean, sentStyleInsertionFunction: boolean, // state for data streaming format @@ -182,6 +189,13 @@ export function createResponseState( bootstrapScriptContent: string | void, bootstrapScripts: $ReadOnlyArray | void, bootstrapModules: $ReadOnlyArray | void, + fallbackBootstrapScriptContent: string | void, + fallbackBootstrapScripts: $ReadOnlyArray< + string | BootstrapScriptDescriptor, + > | void, + fallbackBootstrapModules: $ReadOnlyArray< + string | BootstrapScriptDescriptor, + > | void, externalRuntimeConfig: string | BootstrapScriptDescriptor | void, ): ResponseState { const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix; @@ -259,8 +273,66 @@ export function createResponseState( bootstrapChunks.push(endAsyncScript); } } + + const fallbackBootstrapChunks = []; + if (fallbackBootstrapScriptContent !== undefined) { + fallbackBootstrapChunks.push( + inlineScriptWithNonce, + stringToChunk( + escapeBootstrapScriptContent(fallbackBootstrapScriptContent), + ), + endInlineScript, + ); + } + // We intentionally omit the rizz runtime for fallback bootstrap even if configured. + // Even if it is configured the fallback bootstrap only executes if React errors at some Root + // Boundary and in these cases there will be no instructions for the runtime to execute + if (fallbackBootstrapScripts !== undefined) { + for (let i = 0; i < fallbackBootstrapScripts.length; i++) { + const scriptConfig = fallbackBootstrapScripts[i]; + const src = + typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src; + const integrity = + typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; + fallbackBootstrapChunks.push( + startScriptSrc, + stringToChunk(escapeTextForBrowser(src)), + ); + if (integrity) { + fallbackBootstrapChunks.push( + scriptIntegirty, + stringToChunk(escapeTextForBrowser(integrity)), + ); + } + fallbackBootstrapChunks.push(endAsyncScript); + } + } + if (fallbackBootstrapModules !== undefined) { + for (let i = 0; i < fallbackBootstrapModules.length; i++) { + const scriptConfig = fallbackBootstrapModules[i]; + const src = + typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src; + const integrity = + typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; + fallbackBootstrapChunks.push( + startModuleSrc, + stringToChunk(escapeTextForBrowser(src)), + ); + if (integrity) { + fallbackBootstrapChunks.push( + scriptIntegirty, + stringToChunk(escapeTextForBrowser(integrity)), + ); + } + fallbackBootstrapChunks.push(endAsyncScript); + } + } + return { bootstrapChunks: bootstrapChunks, + fallbackBootstrapChunks: fallbackBootstrapChunks.length + ? fallbackBootstrapChunks + : undefined, placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'), segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'), boundaryPrefix: idPrefix + 'B:', @@ -270,6 +342,7 @@ export function createResponseState( startInlineScript: inlineScriptWithNonce, sentCompleteSegmentFunction: false, sentCompleteBoundaryFunction: false, + sentCompleteContainerFunction: false, sentClientRenderFunction: false, sentStyleInsertionFunction: false, externalRuntimeConfig: externalRuntimeDesc, @@ -415,6 +488,10 @@ export function assignSuspenseBoundaryID( ); } +export function createRootBoundaryID(containerID: string): SuspenseBoundaryID { + return stringToPrecomputedChunk(containerID); +} + export function makeId( responseState: ResponseState, treeId: string, @@ -2126,6 +2203,24 @@ export function writeCompletedRoot( return true; } +export function writeErroredRoot( + destination: Destination, + responseState: ResponseState, +): boolean { + // If fallback bootstrap scripts were provided and we errored at the Root Boundary + // then use those. Use the normal bootstrapChunks if no fallbacks were provided + const bootstrapChunks = + responseState.fallbackBootstrapChunks || responseState.bootstrapChunks; + let i = 0; + for (; i < bootstrapChunks.length - 1; i++) { + writeChunk(destination, bootstrapChunks[i]); + } + if (i < bootstrapChunks.length) { + return writeChunkAndReturn(destination, bootstrapChunks[i]); + } + return true; +} + // Structural Nodes // A placeholder is a node inside a hidden partial tree that can be filled in later, but before @@ -2408,16 +2503,29 @@ export function writeEndSegment( } const completeSegmentScript1Full = stringToPrecomputedChunk( - completeSegmentFunction + ';$RS("', + completeSegmentFunctionString + '$RS("', ); const completeSegmentScript1Partial = stringToPrecomputedChunk('$RS("'); const completeSegmentScript2 = stringToPrecomputedChunk('","'); const completeSegmentScriptEnd = stringToPrecomputedChunk('")'); -const completeSegmentData1 = stringToPrecomputedChunk( - '