Skip to content

Commit 3ea6c73

Browse files
committed
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.
1 parent 2c65dd0 commit 3ea6c73

12 files changed

+341
-254
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@
145145
"download-build": "node ./scripts/release/download-experimental-build.js",
146146
"download-build-for-head": "node ./scripts/release/download-experimental-build.js --commit=$(git rev-parse HEAD)",
147147
"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",
148-
"check-release-dependencies": "node ./scripts/release/check-release-dependencies"
148+
"check-release-dependencies": "node ./scripts/release/check-release-dependencies",
149+
"generate-inline-fizz-runtime": "node ./scripts/rollup/generate-inline-fizz-runtime.js"
149150
},
150151
"resolutions": {
151152
"react-is": "npm:react-is"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {clientRenderBoundary} from './ReactDOMFizzInstructionSet';
2+
3+
// This is a string so Closure's advanced compilation mode doesn't mangle it.
4+
// eslint-disable-next-line dot-notation
5+
window['$RX'] = clientRenderBoundary;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {completeBoundary} from './ReactDOMFizzInstructionSet';
2+
3+
// This is a string so Closure's advanced compilation mode doesn't mangle it.
4+
// eslint-disable-next-line dot-notation
5+
window['$RC'] = completeBoundary;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {completeBoundaryWithStyles} from './ReactDOMFizzInstructionSet';
2+
3+
// This is a string so Closure's advanced compilation mode doesn't mangle it.
4+
// eslint-disable-next-line dot-notation
5+
window['$RM'] = new Map();
6+
// eslint-disable-next-line dot-notation
7+
window['$RR'] = completeBoundaryWithStyles;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {completeSegment} from './ReactDOMFizzInstructionSet';
2+
3+
// This is a string so Closure's advanced compilation mode doesn't mangle it.
4+
// eslint-disable-next-line dot-notation
5+
window['$RS'] = completeSegment;
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/* eslint-disable dot-notation */
2+
3+
const COMMENT_NODE = 8;
4+
const SUSPENSE_START_DATA = '$';
5+
const SUSPENSE_END_DATA = '/$';
6+
const SUSPENSE_PENDING_START_DATA = '$?';
7+
const SUSPENSE_FALLBACK_START_DATA = '$!';
8+
const LOADED = 'l';
9+
const ERRORED = 'e';
10+
11+
// TODO: Symbols that are referenced outside this module use dynamic accessor
12+
// notation instead of dot notation to prevent Closure's advanced compilation
13+
// mode from renaming. We could use extern files instead, but I couldn't get it
14+
// working. Closure converts it to a dot access anyway, though, so it's not an
15+
// urgent issue.
16+
17+
export function clientRenderBoundary(
18+
suspenseBoundaryID,
19+
errorDigest,
20+
errorMsg,
21+
errorComponentStack,
22+
) {
23+
// Find the fallback's first element.
24+
const suspenseIdNode = document.getElementById(suspenseBoundaryID);
25+
if (!suspenseIdNode) {
26+
// The user must have already navigated away from this tree.
27+
// E.g. because the parent was hydrated.
28+
return;
29+
}
30+
// Find the boundary around the fallback. This is always the previous node.
31+
const suspenseNode = suspenseIdNode.previousSibling;
32+
// Tag it to be client rendered.
33+
suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
34+
// assign error metadata to first sibling
35+
const dataset = suspenseIdNode.dataset;
36+
if (errorDigest) dataset['dgst'] = errorDigest;
37+
if (errorMsg) dataset['msg'] = errorMsg;
38+
if (errorComponentStack) dataset['stck'] = errorComponentStack;
39+
// Tell React to retry it if the parent already hydrated.
40+
if (suspenseNode['_reactRetry']) {
41+
suspenseNode['_reactRetry']();
42+
}
43+
}
44+
45+
export function completeBoundaryWithStyles(
46+
suspenseBoundaryID,
47+
contentID,
48+
styles,
49+
) {
50+
// This is a string so Closure's advanced compilation mode doesn't mangle it.
51+
// TODO: In the non-inline version of the runtime, these don't need to be read
52+
// from the global scope.
53+
// eslint-disable-next-line dot-notation
54+
const completeBoundaryImpl = window['$RX'];
55+
// eslint-disable-next-line dot-notation
56+
const resourceMap = window['$RM'];
57+
58+
const precedences = new Map();
59+
const thisDocument = document;
60+
let lastResource, node;
61+
62+
// Seed the precedence list with existing resources
63+
const nodes = thisDocument.querySelectorAll('link[data-rprec]');
64+
for (let i = 0; (node = nodes[i++]); ) {
65+
precedences.set(node.dataset['rprec'], (lastResource = node));
66+
}
67+
68+
let i = 0;
69+
const dependencies = [];
70+
let style, href, precedence, attr, loadingState, resourceEl;
71+
72+
function setStatus(s) {
73+
this.s = s;
74+
}
75+
76+
while ((style = styles[i++])) {
77+
let j = 0;
78+
href = style[j++];
79+
// We check if this resource is already in our resourceMap and reuse it if so.
80+
// If it is already loaded we don't return it as a depenendency since there is nothing
81+
// to wait for
82+
loadingState = resourceMap.get(href);
83+
if (loadingState) {
84+
if (loadingState.s !== 'l') {
85+
dependencies.push(loadingState);
86+
}
87+
continue;
88+
}
89+
90+
// We construct our new resource element, looping over remaining attributes if any
91+
// setting them to the Element.
92+
resourceEl = thisDocument.createElement('link');
93+
resourceEl.href = href;
94+
resourceEl.rel = 'stylesheet';
95+
resourceEl.dataset['rprec'] = precedence = style[j++];
96+
while ((attr = style[j++])) {
97+
resourceEl.setAttribute(attr, style[j++]);
98+
}
99+
100+
// We stash a pending promise in our map by href which will resolve or reject
101+
// when the underlying resource loads or errors. We add it to the dependencies
102+
// array to be returned.
103+
loadingState = resourceEl._p = new Promise((re, rj) => {
104+
resourceEl.onload = re;
105+
resourceEl.onerror = rj;
106+
});
107+
loadingState.then(
108+
setStatus.bind(loadingState, LOADED),
109+
setStatus.bind(loadingState, ERRORED),
110+
);
111+
resourceMap.set(href, loadingState);
112+
dependencies.push(loadingState);
113+
114+
// The prior style resource is the last one placed at a given
115+
// precedence or the last resource itself which may be null.
116+
// We grab this value and then update the last resource for this
117+
// precedence to be the inserted element, updating the lastResource
118+
// pointer if needed.
119+
const prior = precedences.get(precedence) || lastResource;
120+
if (prior === lastResource) {
121+
lastResource = resourceEl;
122+
}
123+
precedences.set(precedence, resourceEl);
124+
125+
// Finally, we insert the newly constructed instance at an appropriate location
126+
// in the Document.
127+
if (prior) {
128+
prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
129+
} else {
130+
const head = thisDocument.head;
131+
head.insertBefore(resourceEl, head.firstChild);
132+
}
133+
}
134+
135+
Promise.all(dependencies).then(
136+
completeBoundaryImpl.bind(null, suspenseBoundaryID, contentID, ''),
137+
completeBoundaryImpl.bind(
138+
null,
139+
suspenseBoundaryID,
140+
contentID,
141+
'Resource failed to load',
142+
),
143+
);
144+
}
145+
146+
export function completeBoundary(suspenseBoundaryID, contentID, errorDigest) {
147+
const contentNode = document.getElementById(contentID);
148+
// We'll detach the content node so that regardless of what happens next we don't leave in the tree.
149+
// This might also help by not causing recalcing each time we move a child from here to the target.
150+
contentNode.parentNode.removeChild(contentNode);
151+
152+
// Find the fallback's first element.
153+
const suspenseIdNode = document.getElementById(suspenseBoundaryID);
154+
if (!suspenseIdNode) {
155+
// The user must have already navigated away from this tree.
156+
// E.g. because the parent was hydrated. That's fine there's nothing to do
157+
// but we have to make sure that we already deleted the container node.
158+
return;
159+
}
160+
// Find the boundary around the fallback. This is always the previous node.
161+
const suspenseNode = suspenseIdNode.previousSibling;
162+
163+
if (!errorDigest) {
164+
// Clear all the existing children. This is complicated because
165+
// there can be embedded Suspense boundaries in the fallback.
166+
// This is similar to clearSuspenseBoundary in ReactDOMHostConfig.
167+
// TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.
168+
// They never hydrate anyway. However, currently we support incrementally loading the fallback.
169+
const parentInstance = suspenseNode.parentNode;
170+
let node = suspenseNode.nextSibling;
171+
let depth = 0;
172+
do {
173+
if (node && node.nodeType === COMMENT_NODE) {
174+
const data = node.data;
175+
if (data === SUSPENSE_END_DATA) {
176+
if (depth === 0) {
177+
break;
178+
} else {
179+
depth--;
180+
}
181+
} else if (
182+
data === SUSPENSE_START_DATA ||
183+
data === SUSPENSE_PENDING_START_DATA ||
184+
data === SUSPENSE_FALLBACK_START_DATA
185+
) {
186+
depth++;
187+
}
188+
}
189+
190+
const nextNode = node.nextSibling;
191+
parentInstance.removeChild(node);
192+
node = nextNode;
193+
} while (node);
194+
195+
const endOfBoundary = node;
196+
197+
// Insert all the children from the contentNode between the start and end of suspense boundary.
198+
while (contentNode.firstChild) {
199+
parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
200+
}
201+
202+
suspenseNode.data = SUSPENSE_START_DATA;
203+
} else {
204+
suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
205+
suspenseIdNode.setAttribute('data-dgst', errorDigest);
206+
}
207+
208+
if (suspenseNode['_reactRetry']) {
209+
suspenseNode['_reactRetry']();
210+
}
211+
}
212+
213+
export function completeSegment(containerID, placeholderID) {
214+
const segmentContainer = document.getElementById(containerID);
215+
const placeholderNode = document.getElementById(placeholderID);
216+
// We always expect both nodes to exist here because, while we might
217+
// have navigated away from the main tree, we still expect the detached
218+
// tree to exist.
219+
segmentContainer.parentNode.removeChild(segmentContainer);
220+
while (segmentContainer.firstChild) {
221+
placeholderNode.parentNode.insertBefore(
222+
segmentContainer.firstChild,
223+
placeholderNode,
224+
);
225+
}
226+
placeholderNode.parentNode.removeChild(placeholderNode);
227+
}

packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-dom-bindings/src/server/fizz-instruction-set/clientRenderFunctionString.js

Lines changed: 0 additions & 38 deletions
This file was deleted.

packages/react-dom-bindings/src/server/fizz-instruction-set/completeBoundaryFunctionString.js

Lines changed: 0 additions & 82 deletions
This file was deleted.

0 commit comments

Comments
 (0)