@samitouri / QOS-React / commits / bb57fa7351

[Fizz] Share code between inline and external runtime (#33066)

Stacked on #33065. The runtime is about to be a lot more complicated so we need to start sharing some more code. The problem with sharing code is that we want the inline runtime to as much as possible be isolated in its scope using only a few global variables to refer across runtimes. A problem with Closure Compiler is that it refuses to inline functions if they have closures inside of them. Which makes sense because of how VMs work it can cause memory leaks. However, in our cases this doesn't matter and code size matters more. So we can't use many clever tricks. So this just favors writing the source in the inline form. Then we add an extra compiler pass to turn those global variables into local variables in the external runtime.

Sebastian Markbåge committed May 1, 2025 at 14:25 UTC bb57fa7351776a86448c1a094bea8108fd6b34ff
12 files changed +190 -291
packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js
+5 -14
@@ -7,12 +7,7 @@
7
8 // Imports are resolved statically by the closure compiler in release bundles
9 // and by rollup in jest unit tests
10 -import {
11 - clientRenderBoundary,
12 - completeBoundaryWithStyles,
13 - completeBoundary,
14 - completeSegment,
15 -} from './fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime';
10 +import './fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime';
11
12 if (document.body != null) {
13 if (document.readyState === 'loading') {
@@ -82,7 +77,7 @@ function handleNode(node_: Node) {
77 const node = (node_: HTMLElement);
78 const dataset = node.dataset;
79 if (dataset['rxi'] != null) {
85 - clientRenderBoundary(
80 + window['$RX'](
81 dataset['bid'],
82 dataset['dgst'],
83 dataset['msg'],
@@ -92,17 +87,13 @@ function handleNode(node_: Node) {
87 node.remove();
88 } else if (dataset['rri'] != null) {
89 // Convert styles here, since its type is Array<Array<string>>
95 - completeBoundaryWithStyles(
96 - dataset['bid'],
97 - dataset['sid'],
98 - JSON.parse(dataset['sty']),
99 - );
90 + window['$RR'](dataset['bid'], dataset['sid'], JSON.parse(dataset['sty']));
91 node.remove();
92 } else if (dataset['rci'] != null) {
102 - completeBoundary(dataset['bid'], dataset['sid']);
93 + window['$RC'](dataset['bid'], dataset['sid']);
94 node.remove();
95 } else if (dataset['rsi'] != null) {
105 - completeSegment(dataset['sid'], dataset['pid']);
96 + window['$RS'](dataset['sid'], dataset['pid']);
97 node.remove();
98 }
99 }
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineClientRenderBoundary.js
+1 -1
@@ -1,4 +1,4 @@
1 -import {clientRenderBoundary} from './ReactDOMFizzInstructionSetInlineSource';
1 +import {clientRenderBoundary} from './ReactDOMFizzInstructionSetShared';
2
3 // This is a string so Closure's advanced compilation mode doesn't mangle it.
4 // eslint-disable-next-line dot-notation
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundary.js
+1 -1
@@ -1,4 +1,4 @@
1 -import {completeBoundary} from './ReactDOMFizzInstructionSetInlineSource';
1 +import {completeBoundary} from './ReactDOMFizzInstructionSetShared';
2
3 // This is a string so Closure's advanced compilation mode doesn't mangle it.
4 // eslint-disable-next-line dot-notation
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundaryWithStyles.js
+1 -1
@@ -1,4 +1,4 @@
1 -import {completeBoundaryWithStyles} from './ReactDOMFizzInstructionSetInlineSource';
1 +import {completeBoundaryWithStyles} from './ReactDOMFizzInstructionSetShared';
2
3 // This is a string so Closure's advanced compilation mode doesn't mangle it.
4 // eslint-disable-next-line dot-notation
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteSegment.js
+1 -1
@@ -1,4 +1,4 @@
1 -import {completeSegment} from './ReactDOMFizzInstructionSetInlineSource';
1 +import {completeSegment} from './ReactDOMFizzInstructionSetShared';
2
3 // This is a string so Closure's advanced compilation mode doesn't mangle it.
4 // eslint-disable-next-line dot-notation
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js
+8 -129
@@ -5,138 +5,17 @@
5 import {
6 clientRenderBoundary,
7 completeBoundary,
8 + completeBoundaryWithStyles,
9 completeSegment,
10 listenToFormSubmissionsForReplaying,
11 } from './ReactDOMFizzInstructionSetShared';
12
12 -export {clientRenderBoundary, completeBoundary, completeSegment};
13 -
14 -const resourceMap = new Map();
15 -
16 -// This function is almost identical to the version used by inline scripts
17 -// (ReactDOMFizzInstructionSetInlineSource), with the exception of how we read
18 -// completeBoundary and resourceMap
19 -export function completeBoundaryWithStyles(
20 - suspenseBoundaryID,
21 - contentID,
22 - stylesheetDescriptors,
23 -) {
24 - const precedences = new Map();
25 - const thisDocument = document;
26 - let lastResource, node;
27 -
28 - // Seed the precedence list with existing resources and collect hoistable style tags
29 - const nodes = thisDocument.querySelectorAll(
30 - 'link[data-precedence],style[data-precedence]',
31 - );
32 - const styleTagsToHoist = [];
33 - for (let i = 0; (node = nodes[i++]); ) {
34 - if (node.getAttribute('media') === 'not all') {
35 - styleTagsToHoist.push(node);
36 - } else {
37 - if (node.tagName === 'LINK') {
38 - resourceMap.set(node.getAttribute('href'), node);
39 - }
40 - precedences.set(node.dataset['precedence'], (lastResource = node));
41 - }
42 - }
43 -
44 - let i = 0;
45 - const dependencies = [];
46 - let href, precedence, attr, loadingState, resourceEl, media;
47 -
48 - function cleanupWith(cb) {
49 - this['_p'] = null;
50 - cb();
51 - }
52 -
53 - // Sheets Mode
54 - let sheetMode = true;
55 - while (true) {
56 - if (sheetMode) {
57 - // Sheet Mode iterates over the stylesheet arguments and constructs them if new or checks them for
58 - // dependency if they already existed
59 - const stylesheetDescriptor = stylesheetDescriptors[i++];
60 - if (!stylesheetDescriptor) {
61 - // enter <style> Mode
62 - sheetMode = false;
63 - i = 0;
64 - continue;
65 - }
66 -
67 - let avoidInsert = false;
68 - let j = 0;
69 - href = stylesheetDescriptor[j++];
70 -
71 - if ((resourceEl = resourceMap.get(href))) {
72 - // We have an already inserted stylesheet.
73 - loadingState = resourceEl['_p'];
74 - avoidInsert = true;
75 - } else {
76 - // We haven't already processed this href so we need to construct a stylesheet and hoist it
77 - // We construct it here and attach a loadingState. We also check whether it matches
78 - // media before we include it in the dependency array.
79 - resourceEl = thisDocument.createElement('link');
80 - resourceEl.href = href;
81 - resourceEl.rel = 'stylesheet';
82 - resourceEl.dataset['precedence'] = precedence =
83 - stylesheetDescriptor[j++];
84 - while ((attr = stylesheetDescriptor[j++])) {
85 - resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
86 - }
87 - loadingState = resourceEl['_p'] = new Promise((resolve, reject) => {
88 - resourceEl.onload = cleanupWith.bind(resourceEl, resolve);
89 - resourceEl.onerror = cleanupWith.bind(resourceEl, reject);
90 - });
91 - // Save this resource element so we can bailout if it is used again
92 - resourceMap.set(href, resourceEl);
93 - }
94 - media = resourceEl.getAttribute('media');
95 - if (loadingState && (!media || window['matchMedia'](media).matches)) {
96 - dependencies.push(loadingState);
97 - }
98 - if (avoidInsert) {
99 - // We have a link that is already in the document. We don't want to fall through to the insert path
100 - continue;
101 - }
102 - } else {
103 - // <style> mode iterates over not-yet-hoisted <style> tags with data-precedence and hoists them.
104 - resourceEl = styleTagsToHoist[i++];
105 - if (!resourceEl) {
106 - // we are done with all style tags
107 - break;
108 - }
109 -
110 - precedence = resourceEl.getAttribute('data-precedence');
111 - resourceEl.removeAttribute('media');
112 - }
113 -
114 - // resourceEl is either a newly constructed <link rel="stylesheet" ...> or a <style> tag requiring hoisting
115 - const prior = precedences.get(precedence) || lastResource;
116 - if (prior === lastResource) {
117 - lastResource = resourceEl;
118 - }
119 - precedences.set(precedence, resourceEl);
120 -
121 - // Finally, we insert the newly constructed instance at an appropriate location
122 - // in the Document.
123 - if (prior) {
124 - prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
125 - } else {
126 - const head = thisDocument.head;
127 - head.insertBefore(resourceEl, head.firstChild);
128 - }
129 - }
130 -
131 - Promise.all(dependencies).then(
132 - completeBoundary.bind(null, suspenseBoundaryID, contentID, ''),
133 - completeBoundary.bind(
134 - null,
135 - suspenseBoundaryID,
136 - contentID,
137 - 'Resource failed to load',
138 - ),
139 - );
140 -}
13 +// This is a string so Closure's advanced compilation mode doesn't mangle it.
14 +// These will be renamed to local references by the external-runtime-plugin.
15 +window['$RM'] = new Map();
16 +window['$RX'] = clientRenderBoundary;
17 +window['$RC'] = completeBoundary;
18 +window['$RR'] = completeBoundaryWithStyles;
19 +window['$RS'] = completeSegment;
20
21 listenToFormSubmissionsForReplaying();
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
+1 -1
@@ -6,7 +6,7 @@ export const clientRenderBoundary =
6 export const completeBoundary =
7 '$RC=function(b,d,e){if(d=document.getElementById(d)){d.parentNode.removeChild(d);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 c=a.data;if("/$"===c||"/&"===c)if(0===f)break;else f--;else"$"!==c&&"$?"!==c&&"$!"!==c&&"&"!==c||f++}c=a.nextSibling;e.removeChild(a);a=c}while(a);for(;d.firstChild;)e.insertBefore(d.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}}};';
8 export const completeBoundaryWithStyles =
9 - '$RM=new Map;\n$RR=function(t,u,y){function v(n){this._p=null;n()}for(var w=$RC,p=$RM,q=new Map,r=document,g,b,h=r.querySelectorAll("link[data-precedence],style[data-precedence]"),x=[],k=0;b=h[k++];)"not all"===b.getAttribute("media")?x.push(b):("LINK"===b.tagName&&p.set(b.getAttribute("href"),b),q.set(b.dataset.precedence,g=b));b=0;h=[];var l,a;for(k=!0;;){if(k){var e=y[b++];if(!e){k=!1;b=0;continue}var c=!1,m=0;var d=e[m++];if(a=p.get(d)){var f=a._p;c=!0}else{a=r.createElement("link");a.href=\nd;a.rel="stylesheet";for(a.dataset.precedence=l=e[m++];f=e[m++];)a.setAttribute(f,e[m++]);f=a._p=new Promise(function(n,z){a.onload=v.bind(a,n);a.onerror=v.bind(a,z)});p.set(d,a)}d=a.getAttribute("media");!f||d&&!matchMedia(d).matches||h.push(f);if(c)continue}else{a=x[b++];if(!a)break;l=a.getAttribute("data-precedence");a.removeAttribute("media")}c=q.get(l)||g;c===g&&(g=a);q.set(l,a);c?c.parentNode.insertBefore(a,c.nextSibling):(c=r.head,c.insertBefore(a,c.firstChild))}Promise.all(h).then(w.bind(null,\nt,u,""),w.bind(null,t,u,"Resource failed to load"))};';
9 + '$RM=new Map;\n$RR=function(r,t,w){function u(n){this._p=null;n()}for(var p=new Map,q=document,g,b,h=q.querySelectorAll("link[data-precedence],style[data-precedence]"),v=[],k=0;b=h[k++];)"not all"===b.getAttribute("media")?v.push(b):("LINK"===b.tagName&&$RM.set(b.getAttribute("href"),b),p.set(b.dataset.precedence,g=b));b=0;h=[];var l,a;for(k=!0;;){if(k){var e=w[b++];if(!e){k=!1;b=0;continue}var c=!1,m=0;var d=e[m++];if(a=$RM.get(d)){var f=a._p;c=!0}else{a=q.createElement("link");a.href=d;a.rel=\n"stylesheet";for(a.dataset.precedence=l=e[m++];f=e[m++];)a.setAttribute(f,e[m++]);f=a._p=new Promise(function(n,x){a.onload=u.bind(a,n);a.onerror=u.bind(a,x)});$RM.set(d,a)}d=a.getAttribute("media");!f||d&&!matchMedia(d).matches||h.push(f);if(c)continue}else{a=v[b++];if(!a)break;l=a.getAttribute("data-precedence");a.removeAttribute("media")}c=p.get(l)||g;c===g&&(g=a);p.set(l,a);c?c.parentNode.insertBefore(a,c.nextSibling):(c=q.head,c.insertBefore(a,c.firstChild))}Promise.all(h).then($RC.bind(null,\nr,t,""),$RC.bind(null,r,t,"Resource failed to load"))};';
10 export const completeSegment =
11 '$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)};';
12 export const formReplaying =
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineSource.js deleted
-142
@@ -1,142 +0,0 @@
1 -/* eslint-disable dot-notation */
2 -
3 -// Instruction set for Fizz inline scripts.
4 -// DO NOT DIRECTLY IMPORT THIS FILE. This is the source for the compiled and
5 -// minified code in ReactDOMFizzInstructionSetInlineCodeStrings.
6 -
7 -import {
8 - clientRenderBoundary,
9 - completeBoundary,
10 - completeSegment,
11 -} from './ReactDOMFizzInstructionSetShared';
12 -
13 -export {clientRenderBoundary, completeBoundary, completeSegment};
14 -
15 -// This function is almost identical to the version used by the external
16 -// runtime (ReactDOMFizzInstructionSetExternalRuntime), with the exception of
17 -// how we read completeBoundaryImpl and resourceMap
18 -export function completeBoundaryWithStyles(
19 - suspenseBoundaryID,
20 - contentID,
21 - stylesheetDescriptors,
22 -) {
23 - const completeBoundaryImpl = window['$RC'];
24 - const resourceMap = window['$RM'];
25 -
26 - const precedences = new Map();
27 - const thisDocument = document;
28 - let lastResource, node;
29 -
30 - // Seed the precedence list with existing resources and collect hoistable style tags
31 - const nodes = thisDocument.querySelectorAll(
32 - 'link[data-precedence],style[data-precedence]',
33 - );
34 - const styleTagsToHoist = [];
35 - for (let i = 0; (node = nodes[i++]); ) {
36 - if (node.getAttribute('media') === 'not all') {
37 - styleTagsToHoist.push(node);
38 - } else {
39 - if (node.tagName === 'LINK') {
40 - resourceMap.set(node.getAttribute('href'), node);
41 - }
42 - precedences.set(node.dataset['precedence'], (lastResource = node));
43 - }
44 - }
45 -
46 - let i = 0;
47 - const dependencies = [];
48 - let href, precedence, attr, loadingState, resourceEl, media;
49 -
50 - function cleanupWith(cb) {
51 - this['_p'] = null;
52 - cb();
53 - }
54 -
55 - // Sheets Mode
56 - let sheetMode = true;
57 - while (true) {
58 - if (sheetMode) {
59 - // Sheet Mode iterates over the stylesheet arguments and constructs them if new or checks them for
60 - // dependency if they already existed
61 - const stylesheetDescriptor = stylesheetDescriptors[i++];
62 - if (!stylesheetDescriptor) {
63 - // enter <style> Mode
64 - sheetMode = false;
65 - i = 0;
66 - continue;
67 - }
68 -
69 - let avoidInsert = false;
70 - let j = 0;
71 - href = stylesheetDescriptor[j++];
72 -
73 - if ((resourceEl = resourceMap.get(href))) {
74 - // We have an already inserted stylesheet.
75 - loadingState = resourceEl['_p'];
76 - avoidInsert = true;
77 - } else {
78 - // We haven't already processed this href so we need to construct a stylesheet and hoist it
79 - // We construct it here and attach a loadingState. We also check whether it matches
80 - // media before we include it in the dependency array.
81 - resourceEl = thisDocument.createElement('link');
82 - resourceEl.href = href;
83 - resourceEl.rel = 'stylesheet';
84 - resourceEl.dataset['precedence'] = precedence =
85 - stylesheetDescriptor[j++];
86 - while ((attr = stylesheetDescriptor[j++])) {
87 - resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
88 - }
89 - loadingState = resourceEl['_p'] = new Promise((resolve, reject) => {
90 - resourceEl.onload = cleanupWith.bind(resourceEl, resolve);
91 - resourceEl.onerror = cleanupWith.bind(resourceEl, reject);
92 - });
93 - // Save this resource element so we can bailout if it is used again
94 - resourceMap.set(href, resourceEl);
95 - }
96 - media = resourceEl.getAttribute('media');
97 - if (loadingState && (!media || window['matchMedia'](media).matches)) {
98 - dependencies.push(loadingState);
99 - }
100 - if (avoidInsert) {
101 - // We have a link that is already in the document. We don't want to fall through to the insert path
102 - continue;
103 - }
104 - } else {
105 - // <style> mode iterates over not-yet-hoisted <style> tags with data-precedence and hoists them.
106 - resourceEl = styleTagsToHoist[i++];
107 - if (!resourceEl) {
108 - // we are done with all style tags
109 - break;
110 - }
111 -
112 - precedence = resourceEl.getAttribute('data-precedence');
113 - resourceEl.removeAttribute('media');
114 - }
115 -
116 - // resourceEl is either a newly constructed <link rel="stylesheet" ...> or a <style> tag requiring hoisting
117 - const prior = precedences.get(precedence) || lastResource;
118 - if (prior === lastResource) {
119 - lastResource = resourceEl;
120 - }
121 - precedences.set(precedence, resourceEl);
122 -
123 - // Finally, we insert the newly constructed instance at an appropriate location
124 - // in the Document.
125 - if (prior) {
126 - prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
127 - } else {
128 - const head = thisDocument.head;
129 - head.insertBefore(resourceEl, head.firstChild);
130 - }
131 - }
132 -
133 - Promise.all(dependencies).then(
134 - completeBoundaryImpl.bind(null, suspenseBoundaryID, contentID, ''),
135 - completeBoundaryImpl.bind(
136 - null,
137 - suspenseBoundaryID,
138 - contentID,
139 - 'Resource failed to load',
140 - ),
141 - );
142 -}
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js
+123
@@ -121,6 +121,129 @@ export function completeBoundary(suspenseBoundaryID, contentID, errorDigest) {
121 }
122 }
123
124 +export function completeBoundaryWithStyles(
125 + suspenseBoundaryID,
126 + contentID,
127 + stylesheetDescriptors,
128 +) {
129 + const precedences = new Map();
130 + const thisDocument = document;
131 + let lastResource, node;
132 +
133 + // Seed the precedence list with existing resources and collect hoistable style tags
134 + const nodes = thisDocument.querySelectorAll(
135 + 'link[data-precedence],style[data-precedence]',
136 + );
137 + const styleTagsToHoist = [];
138 + for (let i = 0; (node = nodes[i++]); ) {
139 + if (node.getAttribute('media') === 'not all') {
140 + styleTagsToHoist.push(node);
141 + } else {
142 + if (node.tagName === 'LINK') {
143 + window['$RM'].set(node.getAttribute('href'), node);
144 + }
145 + precedences.set(node.dataset['precedence'], (lastResource = node));
146 + }
147 + }
148 +
149 + let i = 0;
150 + const dependencies = [];
151 + let href, precedence, attr, loadingState, resourceEl, media;
152 +
153 + function cleanupWith(cb) {
154 + this['_p'] = null;
155 + cb();
156 + }
157 +
158 + // Sheets Mode
159 + let sheetMode = true;
160 + while (true) {
161 + if (sheetMode) {
162 + // Sheet Mode iterates over the stylesheet arguments and constructs them if new or checks them for
163 + // dependency if they already existed
164 + const stylesheetDescriptor = stylesheetDescriptors[i++];
165 + if (!stylesheetDescriptor) {
166 + // enter <style> Mode
167 + sheetMode = false;
168 + i = 0;
169 + continue;
170 + }
171 +
172 + let avoidInsert = false;
173 + let j = 0;
174 + href = stylesheetDescriptor[j++];
175 +
176 + if ((resourceEl = window['$RM'].get(href))) {
177 + // We have an already inserted stylesheet.
178 + loadingState = resourceEl['_p'];
179 + avoidInsert = true;
180 + } else {
181 + // We haven't already processed this href so we need to construct a stylesheet and hoist it
182 + // We construct it here and attach a loadingState. We also check whether it matches
183 + // media before we include it in the dependency array.
184 + resourceEl = thisDocument.createElement('link');
185 + resourceEl.href = href;
186 + resourceEl.rel = 'stylesheet';
187 + resourceEl.dataset['precedence'] = precedence =
188 + stylesheetDescriptor[j++];
189 + while ((attr = stylesheetDescriptor[j++])) {
190 + resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
191 + }
192 + loadingState = resourceEl['_p'] = new Promise((resolve, reject) => {
193 + resourceEl.onload = cleanupWith.bind(resourceEl, resolve);
194 + resourceEl.onerror = cleanupWith.bind(resourceEl, reject);
195 + });
196 + // Save this resource element so we can bailout if it is used again
197 + window['$RM'].set(href, resourceEl);
198 + }
199 + media = resourceEl.getAttribute('media');
200 + if (loadingState && (!media || window['matchMedia'](media).matches)) {
201 + dependencies.push(loadingState);
202 + }
203 + if (avoidInsert) {
204 + // We have a link that is already in the document. We don't want to fall through to the insert path
205 + continue;
206 + }
207 + } else {
208 + // <style> mode iterates over not-yet-hoisted <style> tags with data-precedence and hoists them.
209 + resourceEl = styleTagsToHoist[i++];
210 + if (!resourceEl) {
211 + // we are done with all style tags
212 + break;
213 + }
214 +
215 + precedence = resourceEl.getAttribute('data-precedence');
216 + resourceEl.removeAttribute('media');
217 + }
218 +
219 + // resourceEl is either a newly constructed <link rel="stylesheet" ...> or a <style> tag requiring hoisting
220 + const prior = precedences.get(precedence) || lastResource;
221 + if (prior === lastResource) {
222 + lastResource = resourceEl;
223 + }
224 + precedences.set(precedence, resourceEl);
225 +
226 + // Finally, we insert the newly constructed instance at an appropriate location
227 + // in the Document.
228 + if (prior) {
229 + prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
230 + } else {
231 + const head = thisDocument.head;
232 + head.insertBefore(resourceEl, head.firstChild);
233 + }
234 + }
235 +
236 + Promise.all(dependencies).then(
237 + window['$RC'].bind(null, suspenseBoundaryID, contentID, ''),
238 + window['$RC'].bind(
239 + null,
240 + suspenseBoundaryID,
241 + contentID,
242 + 'Resource failed to load',
243 + ),
244 + );
245 +}
246 +
247 export function completeSegment(containerID, placeholderID) {
248 const segmentContainer = document.getElementById(containerID);
249 const placeholderNode = document.getElementById(placeholderID);
scripts/rollup/build.js
+3
@@ -21,6 +21,7 @@ const Sync = require('./sync');
21 const sizes = require('./plugins/sizes-plugin');
22 const useForks = require('./plugins/use-forks-plugin');
23 const dynamicImports = require('./plugins/dynamic-imports');
24 +const externalRuntime = require('./plugins/external-runtime-plugin');
25 const Packaging = require('./packaging');
26 const {asyncRimRaf} = require('./utils');
27 const codeFrame = require('@babel/code-frame').default;
@@ -441,6 +442,8 @@ function getPlugins(
442 __EXPERIMENTAL__,
443 },
444 }),
445 + // For the external runtime we turn global identifiers into local.
446 + entry.includes('server-external-runtime') && externalRuntime(),
447 {
448 name: 'top-level-definitions',
449 renderChunk(source) {
scripts/rollup/generate-inline-fizz-runtime.js
-1
@@ -46,7 +46,6 @@ async function main() {
46 js: [
47 require.resolve('./externs/closure-externs.js'),
48 fullEntryPath,
49 - instructionDir + '/ReactDOMFizzInstructionSetInlineSource.js',
49 instructionDir + '/ReactDOMFizzInstructionSetShared.js',
50 ],
51 compilation_level: 'ADVANCED',
scripts/rollup/plugins/external-runtime-plugin.js new
+46
@@ -0,0 +1,46 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +'use strict';
8 +
9 +module.exports = function externalRuntime() {
10 + // When generating the source code for the Fizz runtime chunks we use global identifiers to refer
11 + // to different parts of the implementation. When generating the external runtime we need to
12 + // replace those with local identifiers instead.
13 + return {
14 + name: 'scripts/rollup/plugins/dynamic-imports',
15 + renderChunk(source) {
16 + // This replaces "window['$globalVar']" with "$globalVar".
17 + const variables = new Set();
18 + source = source.replace(
19 + /window\[['"](\$[A-z0-9_]*)['"]\]/g,
20 + (_, variableName) => {
21 + variables.add(variableName);
22 + return variableName;
23 + }
24 + );
25 + const startOfFn = 'use strict';
26 + let index = source.indexOf(startOfFn);
27 + if (index === -1) {
28 + return source;
29 + }
30 + index += startOfFn.length + 2;
31 +
32 + // Insert the declarations in the beginning of the function closure
33 + // to scope them to inside the runtime.
34 + let declarations = 'let ';
35 + variables.forEach(variable => {
36 + if (declarations !== 'let ') {
37 + declarations += ', ';
38 + }
39 + declarations += variable;
40 + });
41 + declarations += ';';
42 + source = source.slice(0, index) + declarations + source.slice(index);
43 + return source;
44 + },
45 + };
46 +};