diff --git a/.circleci/config.yml b/.circleci/config.yml index f9ae56ca58..bc7c90086f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -367,6 +367,19 @@ jobs: command: | yarn extract-errors git diff --quiet || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false) + + check_generated_fizz_runtime: + docker: *docker + environment: *environment + steps: + - checkout + - attach_workspace: *attach_workspace + - *restore_node_modules + - run: + name: Confirm generated inline Fizz runtime is up to date + command: | + yarn generate-inline-fizz-runtime + git diff --quiet || (echo "There was a change to the Fizz runtime. Run `yarn generate-inline-fizz-runtime` and check in the result." && false) yarn_test: docker: *docker @@ -494,6 +507,9 @@ workflows: - sync_reconciler_forks: requires: - setup + - check_generated_fizz_runtime: + requires: + - setup - yarn_lint: requires: - setup diff --git a/package.json b/package.json index b3288286d7..12c3022c2a 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,8 @@ "download-build": "node ./scripts/release/download-experimental-build.js", "download-build-for-head": "node ./scripts/release/download-experimental-build.js --commit=$(git rev-parse HEAD)", "download-build-in-codesandbox-ci": "cd scripts/release && yarn install && cd ../../ && yarn download-build-for-head || yarn build-combined --type=node react/index react-dom/index react-dom/src/server react-dom/test-utils scheduler/index react/jsx-runtime react/jsx-dev-runtime", - "check-release-dependencies": "node ./scripts/release/check-release-dependencies" + "check-release-dependencies": "node ./scripts/release/check-release-dependencies", + "generate-inline-fizz-runtime": "node ./scripts/rollup/generate-inline-fizz-runtime.js" }, "resolutions": { "react-is": "npm:react-is" diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js index ae5ccf75e7..f5c95eaa37 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js @@ -74,10 +74,12 @@ export { hoistResourcesToRoot, } from './ReactDOMFloatServer'; -import completeSegmentFunction from './fizz-instruction-set/completeSegmentFunctionString'; -import completeBoundaryFunction from './fizz-instruction-set/completeBoundaryFunctionString'; -import styleInsertionFunction from './fizz-instruction-set/styleInsertionFunctionString'; -import clientRenderFunction from './fizz-instruction-set/clientRenderFunctionString'; +import { + clientRenderBoundary as clientRenderFunction, + completeBoundary as completeBoundaryFunction, + completeBoundaryWithStyles as styleInsertionFunction, + completeSegment as completeSegmentFunction, +} from './fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings'; import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher; diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineClientRenderBoundary.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineClientRenderBoundary.js new file mode 100644 index 0000000000..96f750a8a4 --- /dev/null +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineClientRenderBoundary.js @@ -0,0 +1,5 @@ +import {clientRenderBoundary} from './ReactDOMFizzInstructionSet'; + +// This is a string so Closure's advanced compilation mode doesn't mangle it. +// eslint-disable-next-line dot-notation +window['$RX'] = clientRenderBoundary; diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundary.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundary.js new file mode 100644 index 0000000000..ed85f4e70a --- /dev/null +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundary.js @@ -0,0 +1,5 @@ +import {completeBoundary} from './ReactDOMFizzInstructionSet'; + +// This is a string so Closure's advanced compilation mode doesn't mangle it. +// eslint-disable-next-line dot-notation +window['$RC'] = completeBoundary; 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 new file mode 100644 index 0000000000..62760ee543 --- /dev/null +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundaryWithStyles.js @@ -0,0 +1,7 @@ +import {completeBoundaryWithStyles} 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; diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteSegment.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteSegment.js new file mode 100644 index 0000000000..dbccb338b5 --- /dev/null +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteSegment.js @@ -0,0 +1,5 @@ +import {completeSegment} from './ReactDOMFizzInstructionSet'; + +// This is a string so Closure's advanced compilation mode doesn't mangle it. +// eslint-disable-next-line dot-notation +window['$RS'] = completeSegment; 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 new file mode 100644 index 0000000000..9f7ad8bd03 --- /dev/null +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSet.js @@ -0,0 +1,224 @@ +/* eslint-disable dot-notation */ + +const COMMENT_NODE = 8; +const SUSPENSE_START_DATA = '$'; +const SUSPENSE_END_DATA = '/$'; +const SUSPENSE_PENDING_START_DATA = '$?'; +const SUSPENSE_FALLBACK_START_DATA = '$!'; +const LOADED = 'l'; +const ERRORED = 'e'; + +// TODO: Symbols that are referenced outside this module use dynamic accessor +// notation instead of dot notation to prevent Closure's advanced compilation +// mode from renaming. We could use extern files instead, but I couldn't get it +// working. Closure converts it to a dot access anyway, though, so it's not an +// urgent issue. + +export function clientRenderBoundary( + suspenseBoundaryID, + errorDigest, + errorMsg, + errorComponentStack, +) { + // Find the fallback's first element. + const suspenseIdNode = document.getElementById(suspenseBoundaryID); + if (!suspenseIdNode) { + // The user must have already navigated away from this tree. + // E.g. because the parent was hydrated. + return; + } + // Find the boundary around the fallback. This is always the previous node. + const suspenseNode = suspenseIdNode.previousSibling; + // Tag it to be client rendered. + suspenseNode.data = SUSPENSE_FALLBACK_START_DATA; + // assign error metadata to first sibling + const dataset = suspenseIdNode.dataset; + if (errorDigest) dataset['dgst'] = errorDigest; + if (errorMsg) dataset['msg'] = errorMsg; + if (errorComponentStack) dataset['stck'] = errorComponentStack; + // Tell React to retry it if the parent already hydrated. + if (suspenseNode['_reactRetry']) { + suspenseNode['_reactRetry'](); + } +} + +export function completeBoundaryWithStyles( + 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; + + // Seed the precedence list with existing resources + const nodes = thisDocument.querySelectorAll('link[data-rprec]'); + for (let i = 0; (node = nodes[i++]); ) { + precedences.set(node.dataset['rprec'], (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['rprec'] = 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 completeBoundary(suspenseBoundaryID, contentID, errorDigest) { + const contentNode = document.getElementById(contentID); + // We'll detach the content node so that regardless of what happens next we don't leave in the tree. + // This might also help by not causing recalcing each time we move a child from here to the target. + contentNode.parentNode.removeChild(contentNode); + + // Find the fallback's first element. + const suspenseIdNode = document.getElementById(suspenseBoundaryID); + if (!suspenseIdNode) { + // The user must have already navigated away from this tree. + // E.g. because the parent was hydrated. That's fine there's nothing to do + // but we have to make sure that we already deleted the container node. + return; + } + // Find the boundary around the fallback. This is always the previous node. + const suspenseNode = suspenseIdNode.previousSibling; + + if (!errorDigest) { + // Clear all the existing children. This is complicated because + // there can be embedded Suspense boundaries in the fallback. + // This is similar to clearSuspenseBoundary in ReactDOMHostConfig. + // TODO: We could avoid this if we never emitted suspense boundaries in fallback trees. + // They never hydrate anyway. However, currently we support incrementally loading the fallback. + const parentInstance = suspenseNode.parentNode; + let node = suspenseNode.nextSibling; + let depth = 0; + do { + if (node && node.nodeType === COMMENT_NODE) { + const data = node.data; + if (data === SUSPENSE_END_DATA) { + if (depth === 0) { + break; + } else { + depth--; + } + } else if ( + data === SUSPENSE_START_DATA || + data === SUSPENSE_PENDING_START_DATA || + data === SUSPENSE_FALLBACK_START_DATA + ) { + depth++; + } + } + + const nextNode = node.nextSibling; + parentInstance.removeChild(node); + node = nextNode; + } while (node); + + const endOfBoundary = node; + + // Insert all the children from the contentNode between the start and end of suspense boundary. + while (contentNode.firstChild) { + parentInstance.insertBefore(contentNode.firstChild, endOfBoundary); + } + + suspenseNode.data = SUSPENSE_START_DATA; + } else { + suspenseNode.data = SUSPENSE_FALLBACK_START_DATA; + suspenseIdNode.setAttribute('data-dgst', errorDigest); + } + + if (suspenseNode['_reactRetry']) { + suspenseNode['_reactRetry'](); + } +} + +export function completeSegment(containerID, placeholderID) { + const segmentContainer = document.getElementById(containerID); + const placeholderNode = document.getElementById(placeholderID); + // We always expect both nodes to exist here because, while we might + // have navigated away from the main tree, we still expect the detached + // tree to exist. + segmentContainer.parentNode.removeChild(segmentContainer); + while (segmentContainer.firstChild) { + placeholderNode.parentNode.insertBefore( + segmentContainer.firstChild, + placeholderNode, + ); + } + placeholderNode.parentNode.removeChild(placeholderNode); +} diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js new file mode 100644 index 0000000000..6862f77651 --- /dev/null +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js @@ -0,0 +1,11 @@ +// This is a generated file. The source files are in react-dom-bindings/src/server/fizz-instruction-set. +// The build script is at scripts/rollup/generate-inline-fizz-runtime.js. +// Run `yarn generate-inline-fizz-runtime` to generate. +export const clientRenderBoundary = + '$RX=function(b,c,d,e){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.dgst=c),d&&(a.msg=d),e&&(a.stck=e),b._reactRetry&&b._reactRetry())};'; +export const completeBoundary = + '$RC=function(b,c,e){c=document.getElementById(c);c.parentNode.removeChild(c);var a=document.getElementById(b);if(a){b=a.previousSibling;if(e)b.data="$!",a.setAttribute("data-dgst",e);else{e=b.parentNode;a=b.nextSibling;var f=0;do{if(a&&8===a.nodeType){var d=a.data;if("/$"===d)if(0===f)break;else f--;else"$"!==d&&"$?"!==d&&"$!"!==d||f++}d=a.nextSibling;e.removeChild(a);a=d}while(a);for(;c.firstChild;)e.insertBefore(c.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}};'; +export const completeBoundaryWithStyles = + '$RM=new Map;\n$RR=function(p,q,v){function r(l){this.s=l}for(var t=$RC,u=$RM,m=new Map,n=document,g,e,f=n.querySelectorAll("link[data-rprec]"),d=0;e=f[d++];)m.set(e.dataset.rprec,g=e);e=0;f=[];for(var c,h,b,a;c=v[e++];){var k=0;h=c[k++];if(b=u.get(h))"l"!==b.s&&f.push(b);else{a=n.createElement("link");a.href=h;a.rel="stylesheet";for(a.dataset.rprec=d=c[k++];b=c[k++];)a.setAttribute(b,c[k++]);b=a._p=new Promise(function(l,w){a.onload=l;a.onerror=w});b.then(r.bind(b,"l"),r.bind(b,"e"));u.set(h,\nb);f.push(b);c=m.get(d)||g;c===g&&(g=a);m.set(d,a);c?c.parentNode.insertBefore(a,c.nextSibling):(d=n.head,d.insertBefore(a,d.firstChild))}}Promise.all(f).then(t.bind(null,p,q,""),t.bind(null,p,q,"Resource failed to load"))};'; +export const completeSegment = + '$RS=function(a,b){a=document.getElementById(a);b=document.getElementById(b);for(a.parentNode.removeChild(a);a.firstChild;)b.parentNode.insertBefore(a.firstChild,b);b.parentNode.removeChild(b)};'; diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/clientRenderFunctionString.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/clientRenderFunctionString.js deleted file mode 100644 index d2c2a4b8ef..0000000000 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/clientRenderFunctionString.js +++ /dev/null @@ -1,38 +0,0 @@ -// Instruction Set - -// The following code is the source scripts that we then minify and inline below, -// with renamed function names that we hope don't collide: - -// const COMMENT_NODE = 8; -// const SUSPENSE_START_DATA = '$'; -// const SUSPENSE_END_DATA = '/$'; -// const SUSPENSE_PENDING_START_DATA = '$?'; -// const SUSPENSE_FALLBACK_START_DATA = '$!'; -// const LOADED = 'l'; -// const ERRORED = 'e'; - -// function clientRenderBoundary(suspenseBoundaryID, errorDigest, errorMsg, errorComponentStack) { -// // Find the fallback's first element. -// const suspenseIdNode = document.getElementById(suspenseBoundaryID); -// if (!suspenseIdNode) { -// // The user must have already navigated away from this tree. -// // E.g. because the parent was hydrated. -// return; -// } -// // Find the boundary around the fallback. This is always the previous node. -// const suspenseNode = suspenseIdNode.previousSibling; -// // Tag it to be client rendered. -// suspenseNode.data = SUSPENSE_FALLBACK_START_DATA; -// // assign error metadata to first sibling -// let dataset = suspenseIdNode.dataset; -// if (errorDigest) dataset.dgst = errorDigest; -// if (errorMsg) dataset.msg = errorMsg; -// if (errorComponentStack) dataset.stck = errorComponentStack; -// // Tell React to retry it if the parent already hydrated. -// if (suspenseNode._reactRetry) { -// suspenseNode._reactRetry(); -// } -// } - -// TODO: Generate this file with a build step. -export default 'function $RX(b,c,d,e){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.dgst=c),d&&(a.msg=d),e&&(a.stck=e),b._reactRetry&&b._reactRetry())}'; diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/completeBoundaryFunctionString.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/completeBoundaryFunctionString.js deleted file mode 100644 index cd1def26fc..0000000000 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/completeBoundaryFunctionString.js +++ /dev/null @@ -1,82 +0,0 @@ -// Instruction Set - -// The following code is the source scripts that we then minify and inline below, -// with renamed function names that we hope don't collide: - -// const COMMENT_NODE = 8; -// const SUSPENSE_START_DATA = '$'; -// const SUSPENSE_END_DATA = '/$'; -// const SUSPENSE_PENDING_START_DATA = '$?'; -// const SUSPENSE_FALLBACK_START_DATA = '$!'; -// const LOADED = 'l'; -// const ERRORED = 'e'; - -// function completeBoundary(suspenseBoundaryID, contentID, errorDigest) { -// const contentNode = document.getElementById(contentID); -// // We'll detach the content node so that regardless of what happens next we don't leave in the tree. -// // This might also help by not causing recalcing each time we move a child from here to the target. -// contentNode.parentNode.removeChild(contentNode); - -// // Find the fallback's first element. -// const suspenseIdNode = document.getElementById(suspenseBoundaryID); -// if (!suspenseIdNode) { -// // The user must have already navigated away from this tree. -// // E.g. because the parent was hydrated. That's fine there's nothing to do -// // but we have to make sure that we already deleted the container node. -// return; -// } -// // Find the boundary around the fallback. This is always the previous node. -// const suspenseNode = suspenseIdNode.previousSibling; - -// if (!errorDigest) { -// // Clear all the existing children. This is complicated because -// // there can be embedded Suspense boundaries in the fallback. -// // This is similar to clearSuspenseBoundary in ReactDOMHostConfig. -// // TODO: We could avoid this if we never emitted suspense boundaries in fallback trees. -// // They never hydrate anyway. However, currently we support incrementally loading the fallback. -// const parentInstance = suspenseNode.parentNode; -// let node = suspenseNode.nextSibling; -// let depth = 0; -// do { -// if (node && node.nodeType === COMMENT_NODE) { -// const data = node.data; -// if (data === SUSPENSE_END_DATA) { -// if (depth === 0) { -// break; -// } else { -// depth--; -// } -// } else if ( -// data === SUSPENSE_START_DATA || -// data === SUSPENSE_PENDING_START_DATA || -// data === SUSPENSE_FALLBACK_START_DATA -// ) { -// depth++; -// } -// } - -// const nextNode = node.nextSibling; -// parentInstance.removeChild(node); -// node = nextNode; -// } while (node); - -// const endOfBoundary = node; - -// // Insert all the children from the contentNode between the start and end of suspense boundary. -// while (contentNode.firstChild) { -// parentInstance.insertBefore(contentNode.firstChild, endOfBoundary); -// } - -// suspenseNode.data = SUSPENSE_START_DATA; -// } else { -// suspenseNode.data = SUSPENSE_FALLBACK_START_DATA; -// suspenseIdNode.setAttribute('data-dgst', errorDigest) -// } - -// if (suspenseNode._reactRetry) { -// suspenseNode._reactRetry(); -// } -// } - -// TODO: Generate this file with a build step. -export default 'function $RC(b,c,d){c=document.getElementById(c);c.parentNode.removeChild(c);var a=document.getElementById(b);if(a){b=a.previousSibling;if(d)b.data="$!",a.setAttribute("data-dgst",d);else{d=b.parentNode;a=b.nextSibling;var e=0;do{if(a&&a.nodeType===8){var h=a.data;if(h==="/$")if(0===e)break;else e--;else h!=="$"&&h!=="$?"&&h!=="$!"||e++}h=a.nextSibling;d.removeChild(a);a=h}while(a);for(;c.firstChild;)d.insertBefore(c.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}}'; diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/completeSegmentFunctionString.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/completeSegmentFunctionString.js deleted file mode 100644 index d88785341b..0000000000 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/completeSegmentFunctionString.js +++ /dev/null @@ -1,31 +0,0 @@ -// Instruction Set - -// The following code is the source scripts that we then minify and inline below, -// with renamed function names that we hope don't collide: - -// const COMMENT_NODE = 8; -// const SUSPENSE_START_DATA = '$'; -// const SUSPENSE_END_DATA = '/$'; -// const SUSPENSE_PENDING_START_DATA = '$?'; -// const SUSPENSE_FALLBACK_START_DATA = '$!'; -// const LOADED = 'l'; -// const ERRORED = 'e'; - -// function completeSegment(containerID, placeholderID) { -// const segmentContainer = document.getElementById(containerID); -// const placeholderNode = document.getElementById(placeholderID); -// // We always expect both nodes to exist here because, while we might -// // have navigated away from the main tree, we still expect the detached -// // tree to exist. -// segmentContainer.parentNode.removeChild(segmentContainer); -// while (segmentContainer.firstChild) { -// placeholderNode.parentNode.insertBefore( -// segmentContainer.firstChild, -// placeholderNode, -// ); -// } -// placeholderNode.parentNode.removeChild(placeholderNode); -// } - -// TODO: Generate this file with a build step. -export default 'function $RS(a,b){a=document.getElementById(a);b=document.getElementById(b);for(a.parentNode.removeChild(a);a.firstChild;)b.parentNode.insertBefore(a.firstChild,b);b.parentNode.removeChild(b)}'; diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/styleInsertionFunctionString.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/styleInsertionFunctionString.js deleted file mode 100644 index d143400aa0..0000000000 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/styleInsertionFunctionString.js +++ /dev/null @@ -1,100 +0,0 @@ -// Instruction Set - -// The following code is the source scripts that we then minify and inline below, -// with renamed function names that we hope don't collide: - -// const COMMENT_NODE = 8; -// const SUSPENSE_START_DATA = '$'; -// const SUSPENSE_END_DATA = '/$'; -// const SUSPENSE_PENDING_START_DATA = '$?'; -// const SUSPENSE_FALLBACK_START_DATA = '$!'; -// const LOADED = 'l'; -// const ERRORED = 'e'; - -// resourceMap = new Map(); -// function completeBoundaryWithStyles(suspenseBoundaryID, contentID, styles) { -// const precedences = new Map(); -// const thisDocument = document; -// let lastResource, node; - -// // Seed the precedence list with existing resources -// let nodes = thisDocument.querySelectorAll('link[data-rprec]'); -// for (let i = 0;node = nodes[i++];) { -// precedences.set(node.dataset.rprec, lastResource = node); -// } - -// let i = 0; -// let 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.rprec = 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. -// let 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 { -// let head = thisDocument.head; -// head.insertBefore(resourceEl, head.firstChild); -// } -// } - -// Promise.all(dependencies).then( -// completeBoundary.bind(null, suspenseBoundaryID, contentID, ''), -// completeBoundary.bind(null, suspenseBoundaryID, contentID, "Resource failed to load") -// ); -// } - -// TODO: Generate this file with a build step. -export default '$RM=new Map;function $RR(p,q,t){function r(l){this.s=l}for(var m=new Map,n=document,g,e,f=n.querySelectorAll("link[data-rprec]"),d=0;e=f[d++];)m.set(e.dataset.rprec,g=e);e=0;f=[];for(var c,h,b,a;c=t[e++];){var k=0;h=c[k++];if(b=$RM.get(h))"l"!==b.s&&f.push(b);else{a=n.createElement("link");a.href=h;a.rel="stylesheet";for(a.dataset.rprec=d=c[k++];b=c[k++];)a.setAttribute(b,c[k++]);b=a._p=new Promise(function(l,u){a.onload=l;a.onerror=u});b.then(r.bind(b,"l"),r.bind(b,"e"));$RM.set(h,b);f.push(b);c=m.get(d)||g;c===g&&(g=a);m.set(d,a);c?c.parentNode.insertBefore(a,c.nextSibling):(d=n.head,d.insertBefore(a,d.firstChild))}}Promise.all(f).then($RC.bind(null,p,q,""),$RC.bind(null,p,q,"Resource failed to load"))}'; diff --git a/scripts/rollup/generate-inline-fizz-runtime.js b/scripts/rollup/generate-inline-fizz-runtime.js new file mode 100644 index 0000000000..f798a6f5b0 --- /dev/null +++ b/scripts/rollup/generate-inline-fizz-runtime.js @@ -0,0 +1,88 @@ +'use strict'; + +const fs = require('fs'); +const ClosureCompiler = require('google-closure-compiler').compiler; +const prettier = require('prettier'); + +const instructionDir = + './packages/react-dom-bindings/src/server/fizz-instruction-set'; + +// This is the name of the generated file that exports the inline instruction +// set as strings. +const inlineCodeStringsFilename = + instructionDir + '/ReactDOMFizzInstructionSetInlineCodeStrings.js'; + +const config = [ + { + entry: 'ReactDOMFizzInlineClientRenderBoundary.js', + exportName: 'clientRenderBoundary', + }, + { + entry: 'ReactDOMFizzInlineCompleteBoundary.js', + exportName: 'completeBoundary', + }, + { + entry: 'ReactDOMFizzInlineCompleteBoundaryWithStyles.js', + exportName: 'completeBoundaryWithStyles', + }, + { + entry: 'ReactDOMFizzInlineCompleteSegment.js', + exportName: 'completeSegment', + }, +]; + +const prettierConfig = require('../../.prettierrc.js'); + +async function main() { + const exportStatements = await Promise.all( + config.map(async ({entry, exportName}) => { + const fullEntryPath = instructionDir + '/' + entry; + const compiler = new ClosureCompiler({ + entry_point: fullEntryPath, + js: [fullEntryPath, instructionDir + '/ReactDOMFizzInstructionSet.js'], + compilation_level: 'ADVANCED', + module_resolution: 'NODE', + // This is necessary to prevent Closure from inlining a Promise polyfill + rewrite_polyfills: false, + }); + + const code = await new Promise((resolve, reject) => { + compiler.run((exitCode, stdOut, stdErr) => { + if (exitCode !== 0) { + reject(new Error(stdErr)); + } else { + resolve(stdOut); + } + }); + }); + + return `export const ${exportName} = ${JSON.stringify(code.trim())};`; + }) + ); + + let outputCode = [ + '// This is a generated file. The source files are in react-dom-bindings/src/server/fizz-instruction-set.', + '// The build script is at scripts/rollup/generate-inline-fizz-runtime.js.', + '// Run `yarn generate-inline-fizz-runtime` to generate.', + ...exportStatements, + ].join('\n'); + + // This replaces "window.$globalVar" with "$globalVar". There's probably a + // better way to do this with Closure, with externs or something, but I + // couldn't figure it out. Good enough for now. This only affects the inline + // Fizz runtime, and should break immediately if there were a mistake, so I'm + // not too worried about it. + outputCode = outputCode.replaceAll( + /window\.(\$[A-z0-9_]*)/g, + (_, variableName) => variableName + ); + + const prettyOutputCode = prettier.format(outputCode, prettierConfig); + + fs.writeFileSync(inlineCodeStringsFilename, prettyOutputCode, 'utf8'); +} + +main().catch(err => { + console.error(err); + process.exit(1); +});