Skip to content

Commit

Permalink
Add script to generate inline Fizz runtime (#25481)
Browse files Browse the repository at this point in the history
* Move Fizz inline instructions to unified module

Instead of a separate module per instruction, this exports all of them
from a unified module.

In the next step, I'll add a script to generate this new module.

* Add script to generate inline Fizz runtime

This adds a script to generate the inline Fizz runtime. Previously, the
runtime source was in an inline comment, and a compiled version of the
instructions were hardcoded as strings into the Fizz implementation,
where they are injected into the HTML stream.

I've moved the source for the instructions to a regular JavaScript
module. A script compiles the instructions with Closure, then generates
another module that exports the compiled instructions as strings.

Then the Fizz runtime imports the instructions from the
generated module.

To build the instructions, run:
  yarn generate-inline-fizz-runtime

In the next step, I'll add a CI check to verify that the generated files
are up to date.

* Check in CI if generated Fizz runtime is in sync

The generated Fizz runtime is checked into source. In CI, we'll ensure
it stays in sync by running the script and confirming nothing changed.
  • Loading branch information
acdlite authored Oct 15, 2022
1 parent 69c7246 commit 0eaca37
Show file tree
Hide file tree
Showing 14 changed files with 369 additions and 256 deletions.
16 changes: 16 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -494,6 +507,9 @@ workflows:
- sync_reconciler_forks:
requires:
- setup
- check_generated_fizz_runtime:
requires:
- setup
- yarn_lint:
requires:
- setup
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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);
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

This file was deleted.

Loading

2 comments on commit 0eaca37

@harry170305
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

@harry170305
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Please sign in to comment.