diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js b/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js index 63f5b64d93c8c..bc9e0ccb32624 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js @@ -9,17 +9,12 @@ // and by rollup in jest unit tests import { clientRenderBoundary, - completeBoundaryWithStyles, + completeBoundaryWithStylesInlineLocals, completeBoundary, completeSegment, } from './fizz-instruction-set/ReactDOMFizzInstructionSet'; -if (!window.$RC) { - // TODO: Eventually remove, we currently need to set these globals for - // compatibility with ReactDOMFizzInstructionSet - window.$RC = completeBoundary; - window.$RM = new Map(); -} +const resourceMap = new Map(); if (document.readyState === 'loading') { if (document.body != null) { @@ -91,7 +86,8 @@ function handleNode(node_ /*: Node */) { node.remove(); } else if (dataset['rri'] != null) { // Convert styles here, since its type is Array> - completeBoundaryWithStyles( + completeBoundaryWithStylesInlineLocals( + resourceMap, dataset['bid'], dataset['sid'], JSON.parse(dataset['sty']), diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundaryWithStyles.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundaryWithStyles.js index 62760ee543b5d..0d07636b63772 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundaryWithStyles.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundaryWithStyles.js @@ -1,7 +1,7 @@ -import {completeBoundaryWithStyles} from './ReactDOMFizzInstructionSet'; +import {completeBoundaryWithStylesInlineGlobals} from './ReactDOMFizzInstructionSet'; // This is a string so Closure's advanced compilation mode doesn't mangle it. // eslint-disable-next-line dot-notation window['$RM'] = new Map(); // eslint-disable-next-line dot-notation -window['$RR'] = completeBoundaryWithStyles; +window['$RR'] = completeBoundaryWithStylesInlineGlobals; diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSet.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSet.js index 4abe722309f74..eea5c457b4efb 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSet.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSet.js @@ -42,16 +42,121 @@ export function clientRenderBoundary( } } -export function completeBoundaryWithStyles( +// The following two functions should be almost identical. The only differences +// are the values used for `completeBoundaryImpl` and `resourceMap`. +// completeBoundaryWithStylesInlineLocals is used by the Fizz external runtime, which +// bundles together all Fizz instruction functions (and is able to reference / rename +// completeBoundary and resourceMap as locals). +// completeBoundaryWithStylesInlineGlobals is used by the Fizz inline script writer, +// which sends Fizz instruction functions on an as-needed basis. This version needs +// to reference completeBoundary($RC) and resourceMap($RM) as globals. +// +// Ideally, Closure would take care of inlining a shared implementation, but I couldn't +// figure out a zero-overhead inline due to lack of a @inline compiler directive. +export function completeBoundaryWithStylesInlineLocals( + resourceMap, + suspenseBoundaryID, + contentID, + styles, +) { + const completeBoundaryImpl = completeBoundary; + const precedences = new Map(); + const thisDocument = document; + let lastResource, node; + + // Seed the precedence list with existing resources + const nodes = thisDocument.querySelectorAll( + 'link[data-precedence],style[data-precedence]', + ); + for (let i = 0; (node = nodes[i++]); ) { + precedences.set(node.dataset['precedence'], (lastResource = node)); + } + + let i = 0; + const dependencies = []; + let style, href, precedence, attr, loadingState, resourceEl; + + function setStatus(s) { + this['s'] = s; + } + + while ((style = styles[i++])) { + let j = 0; + href = style[j++]; + // We check if this resource is already in our resourceMap and reuse it if so. + // If it is already loaded we don't return it as a depenendency since there is nothing + // to wait for + loadingState = resourceMap.get(href); + if (loadingState) { + if (loadingState['s'] !== 'l') { + dependencies.push(loadingState); + } + continue; + } + + // We construct our new resource element, looping over remaining attributes if any + // setting them to the Element. + resourceEl = thisDocument.createElement('link'); + resourceEl.href = href; + resourceEl.rel = 'stylesheet'; + resourceEl.dataset['precedence'] = precedence = style[j++]; + while ((attr = style[j++])) { + resourceEl.setAttribute(attr, style[j++]); + } + + // We stash a pending promise in our map by href which will resolve or reject + // when the underlying resource loads or errors. We add it to the dependencies + // array to be returned. + loadingState = resourceEl['_p'] = new Promise((re, rj) => { + resourceEl.onload = re; + resourceEl.onerror = rj; + }); + loadingState.then( + setStatus.bind(loadingState, LOADED), + setStatus.bind(loadingState, ERRORED), + ); + resourceMap.set(href, loadingState); + dependencies.push(loadingState); + + // The prior style resource is the last one placed at a given + // precedence or the last resource itself which may be null. + // We grab this value and then update the last resource for this + // precedence to be the inserted element, updating the lastResource + // pointer if needed. + const prior = precedences.get(precedence) || lastResource; + if (prior === lastResource) { + lastResource = resourceEl; + } + precedences.set(precedence, resourceEl); + + // Finally, we insert the newly constructed instance at an appropriate location + // in the Document. + if (prior) { + prior.parentNode.insertBefore(resourceEl, prior.nextSibling); + } else { + const head = thisDocument.head; + head.insertBefore(resourceEl, head.firstChild); + } + } + + Promise.all(dependencies).then( + completeBoundaryImpl.bind(null, suspenseBoundaryID, contentID, ''), + completeBoundaryImpl.bind( + null, + suspenseBoundaryID, + contentID, + 'Resource failed to load', + ), + ); +} + +export function completeBoundaryWithStylesInlineGlobals( suspenseBoundaryID, contentID, styles, ) { - // TODO: In the non-inline version of the runtime, these don't need to be read - // from the global scope. const completeBoundaryImpl = window['$RC']; const resourceMap = window['$RM']; - const precedences = new Map(); const thisDocument = document; let lastResource, node;