diff --git a/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js b/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js index 4af71458cb6a7..2805b629d4b48 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js +++ b/packages/react-dom-bindings/src/server/ReactDOMFloatServer.js @@ -275,7 +275,7 @@ type PreinitOptions = { crossOrigin?: string, integrity?: string, }; -function preinit(href: string, options: PreinitOptions) { +function preinit(href: string, options: PreinitOptions): void { if (!currentResources) { // While we expect that preinit calls are primarily going to be observed // during render because effects and events don't run on the server it is @@ -285,7 +285,17 @@ function preinit(href: string, options: PreinitOptions) { // simply return and do not warn. return; } - const resources = currentResources; + preinitImpl(currentResources, href, options); +} + +// On the server, preinit may be called outside of render when sending an +// external SSR runtime as part of the initial resources payload. Since this +// is an internal React call, we do not need to use the resources stack. +export function preinitImpl( + resources: Resources, + href: string, + options: PreinitOptions, +): void { if (__DEV__) { validatePreinitArguments(href, options); } diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js b/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js index 01f53697e2e5f..63f5b64d93c8c 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js @@ -3,6 +3,7 @@ * clients. Therefore, it should be fast and not have many external dependencies. * @flow */ +/* eslint-disable dot-notation */ // Imports are resolved statically by the closure compiler in release bundles // and by rollup in jest unit tests @@ -13,13 +14,94 @@ import { completeSegment, } from './fizz-instruction-set/ReactDOMFizzInstructionSet'; -// Intentionally does nothing. Implementation will be added in future PR. -// eslint-disable-next-line no-unused-vars -const observer = new MutationObserver(mutations => { - // These are only called so I can check what the module output looks like. The - // code is unreachable. - clientRenderBoundary(); - completeBoundaryWithStyles(); - completeBoundary(); - completeSegment(); -}); +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); + } else { + // body may not exist yet if the fizz runtime is sent in + // (e.g. as a preinit resource) + const domBodyObserver = new MutationObserver(() => { + // We expect the body node to be stable once parsed / created + if (document.body) { + if (document.readyState === 'loading') { + installFizzInstrObserver(document.body); + } + handleExistingNodes(); + domBodyObserver.disconnect(); + } + }); + // documentElement must already exist at this point + // $FlowFixMe[incompatible-call] + domBodyObserver.observe(document.documentElement, {childList: true}); + } +} + +handleExistingNodes(); + +function handleExistingNodes() { + const existingNodes = document.getElementsByTagName('template'); + for (let i = 0; i < existingNodes.length; i++) { + handleNode(existingNodes[i]); + } +} + +function installFizzInstrObserver(target /*: Node */) { + const fizzInstrObserver = new MutationObserver(mutations => { + for (let i = 0; i < mutations.length; i++) { + const addedNodes = mutations[i].addedNodes; + for (let j = 0; j < addedNodes.length; j++) { + if (addedNodes.item(j).parentNode) { + handleNode(addedNodes.item(j)); + } + } + } + }); + // We assume that instruction data nodes are eventually appended to the + // body, even if Fizz is streaming to a shell / subtree. + fizzInstrObserver.observe(target, { + childList: true, + }); + window.addEventListener('DOMContentLoaded', () => { + fizzInstrObserver.disconnect(); + }); +} + +function handleNode(node_ /*: Node */) { + // $FlowFixMe[incompatible-cast] + if (node_.nodeType !== 1 || !(node_ /*: HTMLElement*/).dataset) { + return; + } + // $FlowFixMe[incompatible-cast] + const node = (node_ /*: HTMLElement*/); + const dataset = node.dataset; + if (dataset['rxi'] != null) { + clientRenderBoundary( + dataset['bid'], + dataset['dgst'], + dataset['msg'], + dataset['stck'], + ); + node.remove(); + } else if (dataset['rri'] != null) { + // Convert styles here, since its type is Array> + completeBoundaryWithStyles( + dataset['bid'], + dataset['sid'], + JSON.parse(dataset['sty']), + ); + node.remove(); + } else if (dataset['rci'] != null) { + completeBoundary(dataset['bid'], dataset['sid']); + node.remove(); + } else if (dataset['rsi'] != null) { + completeSegment(dataset['sid'], dataset['pid']); + node.remove(); + } +} diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js index 91d241e4e7929..c66292d801835 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js @@ -63,6 +63,7 @@ import sanitizeURL from '../shared/sanitizeURL'; import isArray from 'shared/isArray'; import { + preinitImpl, prepareToRenderResources, finishRenderingResources, resourcesFromElement, @@ -105,22 +106,33 @@ export function cleanupAfterRender(previousDispatcher: mixed) { // E.g. this can be used to distinguish legacy renderers from this modern one. export const isPrimaryRenderer = true; +export type StreamingFormat = 0 | 1; +const ScriptStreamingFormat: StreamingFormat = 0; +const DataStreamingFormat: StreamingFormat = 1; + // Per response, global state that is not contextual to the rendering subtree. export type ResponseState = { bootstrapChunks: Array, - startInlineScript: PrecomputedChunk, placeholderPrefix: PrecomputedChunk, segmentPrefix: PrecomputedChunk, boundaryPrefix: string, idPrefix: string, nextSuspenseID: number, + streamingFormat: StreamingFormat, + // state for script streaming format, unused if using external runtime / data + startInlineScript: PrecomputedChunk, sentCompleteSegmentFunction: boolean, sentCompleteBoundaryFunction: boolean, sentClientRenderFunction: boolean, - sentStyleInsertionFunction: boolean, // We allow the legacy renderer to extend this object. + sentStyleInsertionFunction: boolean, + // state for data streaming format + externalRuntimeConfig: BootstrapScriptDescriptor | null, + // We allow the legacy renderer to extend this object. ... }; +const dataElementQuotedEnd = stringToPrecomputedChunk('">'); + const startInlineScript = stringToPrecomputedChunk(''); @@ -154,6 +166,8 @@ export type BootstrapScriptDescriptor = { integrity?: string, }; // Allows us to keep track of what we've already written so we can refer back to it. +// if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag +// is set, the server will send instructions via data attributes (instead of inline scripts) export function createResponseState( identifierPrefix: string | void, nonce: string | void, @@ -170,6 +184,8 @@ export function createResponseState( ''); +const completeSegmentScriptEnd = stringToPrecomputedChunk('")'); + +const completeSegmentData1 = stringToPrecomputedChunk( + '