Skip to content

Commit 8e3dc30

Browse files
committed
Add comments, renames, and helper functions
1 parent e51984f commit 8e3dc30

File tree

4 files changed

+164
-49
lines changed

4 files changed

+164
-49
lines changed

packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ export type BootstrapScriptDescriptor = {
153153
integrity?: string,
154154
};
155155
// Allows us to keep track of what we've already written so we can refer back to it.
156+
// if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag
157+
// is set, the server will send instructions via data attributes (instead of inline scripts)
156158
export function createResponseState(
157159
identifierPrefix: string | void,
158160
nonce: string | void,
@@ -2291,7 +2293,7 @@ const completeSegmentScript1Full = stringToPrecomputedChunk(
22912293
);
22922294
const completeSegmentScript1Partial = stringToPrecomputedChunk('$RS("');
22932295
const completeSegmentScript2 = stringToPrecomputedChunk('","');
2294-
const completeSegmentScript3 = stringToPrecomputedChunk('")</script>');
2296+
const completeSegmentScriptEnd = stringToPrecomputedChunk('")</script>');
22952297

22962298
export function writeCompletedSegmentInstruction(
22972299
destination: Destination,
@@ -2307,13 +2309,16 @@ export function writeCompletedSegmentInstruction(
23072309
// Future calls can just reuse the same function.
23082310
writeChunk(destination, completeSegmentScript1Partial);
23092311
}
2312+
2313+
// Write function arguments, which are string literals
23102314
writeChunk(destination, responseState.segmentPrefix);
23112315
const formattedID = stringToChunk(contentSegmentID.toString(16));
23122316
writeChunk(destination, formattedID);
23132317
writeChunk(destination, completeSegmentScript2);
23142318
writeChunk(destination, responseState.placeholderPrefix);
23152319
writeChunk(destination, formattedID);
2316-
return writeChunkAndReturn(destination, completeSegmentScript3);
2320+
2321+
return writeChunkAndReturn(destination, completeSegmentScriptEnd);
23172322
}
23182323

23192324
const completeBoundaryScript1Full = stringToPrecomputedChunk(
@@ -2331,9 +2336,9 @@ const completeBoundaryWithStylesScript1Partial = stringToPrecomputedChunk(
23312336
'$RR("',
23322337
);
23332338
const completeBoundaryScript2 = stringToPrecomputedChunk('","');
2334-
const completeBoundaryScript2a = stringToPrecomputedChunk('",');
2335-
const completeBoundaryScript3 = stringToPrecomputedChunk('"');
2336-
const completeBoundaryScript4 = stringToPrecomputedChunk(')</script>');
2339+
const completeBoundaryScript3a = stringToPrecomputedChunk('",');
2340+
const completeBoundaryScript3b = stringToPrecomputedChunk('"');
2341+
const completeBoundaryScriptEnd = stringToPrecomputedChunk(')</script>');
23372342

23382343
export function writeCompletedBoundaryInstruction(
23392344
destination: Destination,
@@ -2373,26 +2378,28 @@ export function writeCompletedBoundaryInstruction(
23732378
);
23742379
}
23752380

2381+
// Write function arguments, which are string literals and array
23762382
const formattedContentID = stringToChunk(contentSegmentID.toString(16));
23772383
writeChunk(destination, boundaryID);
23782384
writeChunk(destination, completeBoundaryScript2);
23792385
writeChunk(destination, responseState.segmentPrefix);
23802386
writeChunk(destination, formattedContentID);
23812387
if (enableFloat && hasStyleDependencies) {
2382-
writeChunk(destination, completeBoundaryScript2a);
2388+
writeChunk(destination, completeBoundaryScript3a);
2389+
// boundaryResources encodes an array literal
23832390
writeStyleResourceDependencies(destination, boundaryResources);
23842391
} else {
2385-
writeChunk(destination, completeBoundaryScript3);
2392+
writeChunk(destination, completeBoundaryScript3b);
23862393
}
2387-
return writeChunkAndReturn(destination, completeBoundaryScript4);
2394+
return writeChunkAndReturn(destination, completeBoundaryScriptEnd);
23882395
}
23892396

23902397
const clientRenderScript1Full = stringToPrecomputedChunk(
23912398
clientRenderFunction + ';$RX("',
23922399
);
23932400
const clientRenderScript1Partial = stringToPrecomputedChunk('$RX("');
23942401
const clientRenderScript1A = stringToPrecomputedChunk('"');
2395-
const clientRenderScript2 = stringToPrecomputedChunk(')</script>');
2402+
const clientRenderScriptEnd = stringToPrecomputedChunk(')</script>');
23962403
const clientRenderErrorScriptArgInterstitial = stringToPrecomputedChunk(',');
23972404

23982405
export function writeClientRenderBoundaryInstruction(
@@ -2418,31 +2425,34 @@ export function writeClientRenderBoundaryInstruction(
24182425
'An ID must have been assigned before we can complete the boundary.',
24192426
);
24202427
}
2421-
2428+
// Write function arguments, which are string literals
24222429
writeChunk(destination, boundaryID);
24232430
writeChunk(destination, clientRenderScript1A);
24242431
if (errorDigest || errorMessage || errorComponentStack) {
2432+
// ,"JSONString"
24252433
writeChunk(destination, clientRenderErrorScriptArgInterstitial);
24262434
writeChunk(
24272435
destination,
24282436
stringToChunk(escapeJSStringsForInstructionScripts(errorDigest || '')),
24292437
);
24302438
}
24312439
if (errorMessage || errorComponentStack) {
2440+
// ,"JSONString"
24322441
writeChunk(destination, clientRenderErrorScriptArgInterstitial);
24332442
writeChunk(
24342443
destination,
24352444
stringToChunk(escapeJSStringsForInstructionScripts(errorMessage || '')),
24362445
);
24372446
}
24382447
if (errorComponentStack) {
2448+
// ,"JSONString"
24392449
writeChunk(destination, clientRenderErrorScriptArgInterstitial);
24402450
writeChunk(
24412451
destination,
24422452
stringToChunk(escapeJSStringsForInstructionScripts(errorComponentStack)),
24432453
);
24442454
}
2445-
return writeChunkAndReturn(destination, clientRenderScript2);
2455+
return writeChunkAndReturn(destination, clientRenderScriptEnd);
24462456
}
24472457

24482458
const regexForJSStringsInInstructionScripts = /[<\u2028\u2029]/g;
@@ -2743,6 +2753,16 @@ const arraySubsequentOpenBracket = stringToPrecomputedChunk(',[');
27432753
const arrayInterstitial = stringToPrecomputedChunk(',');
27442754
const arrayCloseBracket = stringToPrecomputedChunk(']');
27452755

2756+
function writeStyleResourceObject(destination: Destination, str: string) {
2757+
// write "script_escaped_string", since this is writing to a script tag
2758+
// and will be evaluated as a string literal inside an array literal
2759+
writeChunk(
2760+
destination,
2761+
stringToChunk(escapeJSObjectForInstructionScripts(str)),
2762+
);
2763+
// TODO(mofeiZ): will add a data writer format here in a later PR
2764+
}
2765+
27462766
function writeStyleResourceDependencies(
27472767
destination: Destination,
27482768
boundaryResources: BoundaryResources,
@@ -2787,10 +2807,7 @@ function writeStyleResourceDependencyHrefOnly(
27872807
checkAttributeStringCoercion(href, 'href');
27882808
}
27892809
const coercedHref = '' + (href: any);
2790-
writeChunk(
2791-
destination,
2792-
stringToChunk(escapeJSObjectForInstructionScripts(coercedHref)),
2793-
);
2810+
writeStyleResourceObject(destination, coercedHref);
27942811
}
27952812

27962813
function writeStyleResourceDependency(
@@ -2804,20 +2821,15 @@ function writeStyleResourceDependency(
28042821
}
28052822
const coercedHref = '' + (href: any);
28062823
sanitizeURL(coercedHref);
2807-
writeChunk(
2808-
destination,
2809-
stringToChunk(escapeJSObjectForInstructionScripts(coercedHref)),
2810-
);
2824+
writeStyleResourceObject(destination, coercedHref);
28112825

28122826
if (__DEV__) {
28132827
checkAttributeStringCoercion(precedence, 'precedence');
28142828
}
28152829
const coercedPrecedence = '' + (precedence: any);
28162830
writeChunk(destination, arrayInterstitial);
2817-
writeChunk(
2818-
destination,
2819-
stringToChunk(escapeJSObjectForInstructionScripts(coercedPrecedence)),
2820-
);
2831+
2832+
writeStyleResourceObject(destination, coercedPrecedence);
28212833

28222834
for (const propKey in props) {
28232835
if (hasOwnProperty.call(props, propKey)) {
@@ -2916,13 +2928,7 @@ function writeStyleResourceAttribute(
29162928
}
29172929
attributeValue = '' + (value: any);
29182930
writeChunk(destination, arrayInterstitial);
2919-
writeChunk(
2920-
destination,
2921-
stringToChunk(escapeJSObjectForInstructionScripts(attributeName)),
2922-
);
2931+
writeStyleResourceObject(destination, attributeName);
29232932
writeChunk(destination, arrayInterstitial);
2924-
writeChunk(
2925-
destination,
2926-
stringToChunk(escapeJSObjectForInstructionScripts(attributeValue)),
2927-
);
2933+
writeStyleResourceObject(destination, attributeValue);
29282934
}

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 116 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
*/
99

1010
'use strict';
11+
import * as fs from 'fs';
12+
import * as tmp from 'tmp';
13+
import {rollup} from 'rollup';
14+
const replace = require('rollup-plugin-replace');
1115

1216
let JSDOM;
1317
let Stream;
@@ -30,6 +34,9 @@ let container;
3034
let buffer = '';
3135
let hasErrored = false;
3236
let fatalError = undefined;
37+
const renderOptions = {};
38+
const rollupCache: Map<string, string | null> = new Map();
39+
const matchScriptTag = /<script[\s\S]*\/script>/m;
3340

3441
describe('ReactDOMFizzServer', () => {
3542
beforeEach(() => {
@@ -113,6 +120,90 @@ describe('ReactDOMFizzServer', () => {
113120
.join('');
114121
}
115122

123+
// Utility function to read and bundle a standalone browser script
124+
async function getRollupResult(scriptSrc: string): string | null {
125+
if (rollupCache.has(scriptSrc)) {
126+
return rollupCache.get(scriptSrc);
127+
}
128+
let tmpFile;
129+
try {
130+
tmpFile = tmp.fileSync();
131+
const rollupConfig = {
132+
input: require.resolve(scriptSrc),
133+
onwarn: console.warn,
134+
plugins: [replace({__DEV__: 'true'})],
135+
output: {
136+
externalLiveBindings: false,
137+
freeze: false,
138+
interop: false,
139+
esModule: false,
140+
},
141+
};
142+
const outputConfig = {
143+
file: tmpFile.name,
144+
format: 'iife',
145+
};
146+
const bundle = await rollup(rollupConfig);
147+
await bundle.write(outputConfig);
148+
const bundleBuffer = new Buffer.alloc(4096);
149+
let bundleStr = '';
150+
while (true) {
151+
const bytes = fs.readSync(tmpFile.fd, bundleBuffer);
152+
if (bytes <= 0) {
153+
break;
154+
}
155+
bundleStr += bundleBuffer.slice(0, bytes).toString();
156+
}
157+
rollupCache.set(scriptSrc, bundleStr);
158+
return bundleStr;
159+
} catch (e) {
160+
rollupCache.set(scriptSrc, null);
161+
return null;
162+
} finally {
163+
if (tmpFile) {
164+
tmpFile.removeCallback();
165+
}
166+
}
167+
}
168+
169+
// Utility function to process received HTML nodes.
170+
// 1. Move node into an existing parent container (if passed)
171+
// 2. Execute any scripts that are embedded and try to resolve
172+
// scripts with sources
173+
async function replaceScriptsAndMove(node, parent) {
174+
if (node.nodeName === 'SCRIPT' || node.nodeName === 'script') {
175+
const script = document.createElement('SCRIPT');
176+
const scriptSrc = node.getAttribute('src');
177+
if (scriptSrc) {
178+
const rollupOutput = await getRollupResult(scriptSrc);
179+
if (rollupOutput) {
180+
script.textContent = rollupOutput;
181+
} else {
182+
for (let i = 0; i < node.attributes.length; i++) {
183+
const attr = node.attributes.item(i);
184+
script.setAttribute(attr.name, attr.value);
185+
}
186+
}
187+
} else if (CSPnonce === null || node.getAttribute('nonce') === CSPnonce) {
188+
script.textContent = node.textContent;
189+
}
190+
if (parent != null) {
191+
node.parentNode.removeChild(node);
192+
parent.appendChild(script);
193+
} else {
194+
node.parentNode.replaceChild(script, node);
195+
}
196+
} else {
197+
for (let i = 0; i < node.childNodes.length; i++) {
198+
const inner = node.childNodes[i];
199+
await replaceScriptsAndMove(inner, null);
200+
}
201+
if (parent != null) {
202+
parent.appendChild(node);
203+
}
204+
}
205+
}
206+
116207
async function act(callback) {
117208
await callback();
118209
// Await one turn around the event loop.
@@ -134,17 +225,7 @@ describe('ReactDOMFizzServer', () => {
134225
container.nodeName === '#document' ? container.body : container;
135226
while (fakeBody.firstChild) {
136227
const node = fakeBody.firstChild;
137-
if (
138-
node.nodeName === 'SCRIPT' &&
139-
(CSPnonce === null || node.getAttribute('nonce') === CSPnonce)
140-
) {
141-
const script = document.createElement('script');
142-
script.textContent = node.textContent;
143-
fakeBody.removeChild(node);
144-
parent.appendChild(script);
145-
} else {
146-
parent.appendChild(node);
147-
}
228+
await replaceScriptsAndMove(node, parent);
148229
}
149230
}
150231

@@ -170,6 +251,7 @@ describe('ReactDOMFizzServer', () => {
170251
document = jsdom.window.document;
171252
container = document;
172253
buffer = '';
254+
await replaceScriptsAndMove(document.documentElement);
173255
}
174256

175257
function getVisibleChildren(element) {
@@ -179,6 +261,7 @@ describe('ReactDOMFizzServer', () => {
179261
if (node.nodeType === 1) {
180262
if (
181263
node.tagName !== 'SCRIPT' &&
264+
node.tagName !== 'script' &&
182265
node.tagName !== 'TEMPLATE' &&
183266
node.tagName !== 'template' &&
184267
!node.hasAttribute('hidden') &&
@@ -289,11 +372,26 @@ describe('ReactDOMFizzServer', () => {
289372
return <As>{readText(text)}</As>;
290373
}
291374

375+
function getNonScriptChildren(node) {
376+
return Array.from(node.childNodes).filter(
377+
n => n.tagName !== 'SCRIPT' && n.tagName !== 'script',
378+
);
379+
}
380+
381+
function getNonScriptInnerHtml(node) {
382+
return node.innerHTML.replace(matchScriptTag, '');
383+
}
384+
292385
function renderToPipeableStream(jsx, options) {
386+
// Merge options with renderOptions, which may contain featureFlag specific behavior
293387
if (options) {
294-
return ReactDOMFizzServer.renderToPipeableStream(jsx, options);
388+
const mergedOptions = {
389+
...renderOptions,
390+
...options,
391+
};
392+
return ReactDOMFizzServer.renderToPipeableStream(jsx, mergedOptions);
295393
} else {
296-
return ReactDOMFizzServer.renderToPipeableStream(jsx);
394+
return ReactDOMFizzServer.renderToPipeableStream(jsx, renderOptions);
297395
}
298396
}
299397

@@ -572,7 +670,7 @@ describe('ReactDOMFizzServer', () => {
572670
// Because there is no content inside the Suspense boundary that could've
573671
// been written, we expect to not see any additional partial data flushed
574672
// yet.
575-
expect(container.firstChild.nextSibling).toBe(null);
673+
expect(getNonScriptChildren(container).length).toBe(1);
576674
await act(async () => {
577675
resolveElement({default: <Text text="Hello" />});
578676
});
@@ -4460,7 +4558,8 @@ describe('ReactDOMFizzServer', () => {
44604558
pipe(writable);
44614559
});
44624560

4463-
expect(container.innerHTML).toEqual(
4561+
// strip inserted external runtime
4562+
expect(getNonScriptInnerHtml(container)).toEqual(
44644563
'<div>hello<b>world, <!-- -->Foo</b>!</div>',
44654564
);
44664565
const errors = [];
@@ -4811,7 +4910,8 @@ describe('ReactDOMFizzServer', () => {
48114910
pipe(writable);
48124911
});
48134912

4814-
expect(container.innerHTML).toEqual(
4913+
// strip inserted external runtime
4914+
expect(getNonScriptInnerHtml(container)).toEqual(
48154915
'<div><!--$-->hello<!-- -->world<!-- --><!--/$--><!--$-->world<!-- --><!--/$--><!--$-->hello<!-- -->world<!-- --><br><!--/$--><!--$-->world<!-- --><br><!--/$--></div>',
48164916
);
48174917

0 commit comments

Comments
 (0)