Skip to content

Commit

Permalink
Add comments, renames, and helper functions
Browse files Browse the repository at this point in the history
  • Loading branch information
mofeiZ committed Oct 26, 2022
1 parent e51984f commit 8e3dc30
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ export type BootstrapScriptDescriptor = {
integrity?: string,
};
// Allows us to keep track of what we've already written so we can refer back to it.
// if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag
// is set, the server will send instructions via data attributes (instead of inline scripts)
export function createResponseState(
identifierPrefix: string | void,
nonce: string | void,
Expand Down Expand Up @@ -2291,7 +2293,7 @@ const completeSegmentScript1Full = stringToPrecomputedChunk(
);
const completeSegmentScript1Partial = stringToPrecomputedChunk('$RS("');
const completeSegmentScript2 = stringToPrecomputedChunk('","');
const completeSegmentScript3 = stringToPrecomputedChunk('")</script>');
const completeSegmentScriptEnd = stringToPrecomputedChunk('")</script>');

export function writeCompletedSegmentInstruction(
destination: Destination,
Expand All @@ -2307,13 +2309,16 @@ export function writeCompletedSegmentInstruction(
// Future calls can just reuse the same function.
writeChunk(destination, completeSegmentScript1Partial);
}

// Write function arguments, which are string literals
writeChunk(destination, responseState.segmentPrefix);
const formattedID = stringToChunk(contentSegmentID.toString(16));
writeChunk(destination, formattedID);
writeChunk(destination, completeSegmentScript2);
writeChunk(destination, responseState.placeholderPrefix);
writeChunk(destination, formattedID);
return writeChunkAndReturn(destination, completeSegmentScript3);

return writeChunkAndReturn(destination, completeSegmentScriptEnd);
}

const completeBoundaryScript1Full = stringToPrecomputedChunk(
Expand All @@ -2331,9 +2336,9 @@ const completeBoundaryWithStylesScript1Partial = stringToPrecomputedChunk(
'$RR("',
);
const completeBoundaryScript2 = stringToPrecomputedChunk('","');
const completeBoundaryScript2a = stringToPrecomputedChunk('",');
const completeBoundaryScript3 = stringToPrecomputedChunk('"');
const completeBoundaryScript4 = stringToPrecomputedChunk(')</script>');
const completeBoundaryScript3a = stringToPrecomputedChunk('",');
const completeBoundaryScript3b = stringToPrecomputedChunk('"');
const completeBoundaryScriptEnd = stringToPrecomputedChunk(')</script>');

export function writeCompletedBoundaryInstruction(
destination: Destination,
Expand Down Expand Up @@ -2373,26 +2378,28 @@ export function writeCompletedBoundaryInstruction(
);
}

// Write function arguments, which are string literals and array
const formattedContentID = stringToChunk(contentSegmentID.toString(16));
writeChunk(destination, boundaryID);
writeChunk(destination, completeBoundaryScript2);
writeChunk(destination, responseState.segmentPrefix);
writeChunk(destination, formattedContentID);
if (enableFloat && hasStyleDependencies) {
writeChunk(destination, completeBoundaryScript2a);
writeChunk(destination, completeBoundaryScript3a);
// boundaryResources encodes an array literal
writeStyleResourceDependencies(destination, boundaryResources);
} else {
writeChunk(destination, completeBoundaryScript3);
writeChunk(destination, completeBoundaryScript3b);
}
return writeChunkAndReturn(destination, completeBoundaryScript4);
return writeChunkAndReturn(destination, completeBoundaryScriptEnd);
}

const clientRenderScript1Full = stringToPrecomputedChunk(
clientRenderFunction + ';$RX("',
);
const clientRenderScript1Partial = stringToPrecomputedChunk('$RX("');
const clientRenderScript1A = stringToPrecomputedChunk('"');
const clientRenderScript2 = stringToPrecomputedChunk(')</script>');
const clientRenderScriptEnd = stringToPrecomputedChunk(')</script>');
const clientRenderErrorScriptArgInterstitial = stringToPrecomputedChunk(',');

export function writeClientRenderBoundaryInstruction(
Expand All @@ -2418,31 +2425,34 @@ export function writeClientRenderBoundaryInstruction(
'An ID must have been assigned before we can complete the boundary.',
);
}

// Write function arguments, which are string literals
writeChunk(destination, boundaryID);
writeChunk(destination, clientRenderScript1A);
if (errorDigest || errorMessage || errorComponentStack) {
// ,"JSONString"
writeChunk(destination, clientRenderErrorScriptArgInterstitial);
writeChunk(
destination,
stringToChunk(escapeJSStringsForInstructionScripts(errorDigest || '')),
);
}
if (errorMessage || errorComponentStack) {
// ,"JSONString"
writeChunk(destination, clientRenderErrorScriptArgInterstitial);
writeChunk(
destination,
stringToChunk(escapeJSStringsForInstructionScripts(errorMessage || '')),
);
}
if (errorComponentStack) {
// ,"JSONString"
writeChunk(destination, clientRenderErrorScriptArgInterstitial);
writeChunk(
destination,
stringToChunk(escapeJSStringsForInstructionScripts(errorComponentStack)),
);
}
return writeChunkAndReturn(destination, clientRenderScript2);
return writeChunkAndReturn(destination, clientRenderScriptEnd);
}

const regexForJSStringsInInstructionScripts = /[<\u2028\u2029]/g;
Expand Down Expand Up @@ -2743,6 +2753,16 @@ const arraySubsequentOpenBracket = stringToPrecomputedChunk(',[');
const arrayInterstitial = stringToPrecomputedChunk(',');
const arrayCloseBracket = stringToPrecomputedChunk(']');

function writeStyleResourceObject(destination: Destination, str: string) {
// write "script_escaped_string", since this is writing to a script tag
// and will be evaluated as a string literal inside an array literal
writeChunk(
destination,
stringToChunk(escapeJSObjectForInstructionScripts(str)),
);
// TODO(mofeiZ): will add a data writer format here in a later PR
}

function writeStyleResourceDependencies(
destination: Destination,
boundaryResources: BoundaryResources,
Expand Down Expand Up @@ -2787,10 +2807,7 @@ function writeStyleResourceDependencyHrefOnly(
checkAttributeStringCoercion(href, 'href');
}
const coercedHref = '' + (href: any);
writeChunk(
destination,
stringToChunk(escapeJSObjectForInstructionScripts(coercedHref)),
);
writeStyleResourceObject(destination, coercedHref);
}

function writeStyleResourceDependency(
Expand All @@ -2804,20 +2821,15 @@ function writeStyleResourceDependency(
}
const coercedHref = '' + (href: any);
sanitizeURL(coercedHref);
writeChunk(
destination,
stringToChunk(escapeJSObjectForInstructionScripts(coercedHref)),
);
writeStyleResourceObject(destination, coercedHref);

if (__DEV__) {
checkAttributeStringCoercion(precedence, 'precedence');
}
const coercedPrecedence = '' + (precedence: any);
writeChunk(destination, arrayInterstitial);
writeChunk(
destination,
stringToChunk(escapeJSObjectForInstructionScripts(coercedPrecedence)),
);

writeStyleResourceObject(destination, coercedPrecedence);

for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
Expand Down Expand Up @@ -2916,13 +2928,7 @@ function writeStyleResourceAttribute(
}
attributeValue = '' + (value: any);
writeChunk(destination, arrayInterstitial);
writeChunk(
destination,
stringToChunk(escapeJSObjectForInstructionScripts(attributeName)),
);
writeStyleResourceObject(destination, attributeName);
writeChunk(destination, arrayInterstitial);
writeChunk(
destination,
stringToChunk(escapeJSObjectForInstructionScripts(attributeValue)),
);
writeStyleResourceObject(destination, attributeValue);
}
132 changes: 116 additions & 16 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
*/

'use strict';
import * as fs from 'fs';
import * as tmp from 'tmp';
import {rollup} from 'rollup';
const replace = require('rollup-plugin-replace');

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

describe('ReactDOMFizzServer', () => {
beforeEach(() => {
Expand Down Expand Up @@ -113,6 +120,90 @@ describe('ReactDOMFizzServer', () => {
.join('');
}

// Utility function to read and bundle a standalone browser script
async function getRollupResult(scriptSrc: string): string | null {
if (rollupCache.has(scriptSrc)) {
return rollupCache.get(scriptSrc);
}
let tmpFile;
try {
tmpFile = tmp.fileSync();
const rollupConfig = {
input: require.resolve(scriptSrc),
onwarn: console.warn,
plugins: [replace({__DEV__: 'true'})],
output: {
externalLiveBindings: false,
freeze: false,
interop: false,
esModule: false,
},
};
const outputConfig = {
file: tmpFile.name,
format: 'iife',
};
const bundle = await rollup(rollupConfig);
await bundle.write(outputConfig);
const bundleBuffer = new Buffer.alloc(4096);
let bundleStr = '';
while (true) {
const bytes = fs.readSync(tmpFile.fd, bundleBuffer);
if (bytes <= 0) {
break;
}
bundleStr += bundleBuffer.slice(0, bytes).toString();
}
rollupCache.set(scriptSrc, bundleStr);
return bundleStr;
} catch (e) {
rollupCache.set(scriptSrc, null);
return null;
} finally {
if (tmpFile) {
tmpFile.removeCallback();
}
}
}

// Utility function to process received HTML nodes.
// 1. Move node into an existing parent container (if passed)
// 2. Execute any scripts that are embedded and try to resolve
// scripts with sources
async function replaceScriptsAndMove(node, parent) {
if (node.nodeName === 'SCRIPT' || node.nodeName === 'script') {
const script = document.createElement('SCRIPT');
const scriptSrc = node.getAttribute('src');
if (scriptSrc) {
const rollupOutput = await getRollupResult(scriptSrc);
if (rollupOutput) {
script.textContent = rollupOutput;
} else {
for (let i = 0; i < node.attributes.length; i++) {
const attr = node.attributes.item(i);
script.setAttribute(attr.name, attr.value);
}
}
} else if (CSPnonce === null || node.getAttribute('nonce') === CSPnonce) {
script.textContent = node.textContent;
}
if (parent != null) {
node.parentNode.removeChild(node);
parent.appendChild(script);
} else {
node.parentNode.replaceChild(script, node);
}
} else {
for (let i = 0; i < node.childNodes.length; i++) {
const inner = node.childNodes[i];
await replaceScriptsAndMove(inner, null);
}
if (parent != null) {
parent.appendChild(node);
}
}
}

async function act(callback) {
await callback();
// Await one turn around the event loop.
Expand All @@ -134,17 +225,7 @@ describe('ReactDOMFizzServer', () => {
container.nodeName === '#document' ? container.body : container;
while (fakeBody.firstChild) {
const node = fakeBody.firstChild;
if (
node.nodeName === 'SCRIPT' &&
(CSPnonce === null || node.getAttribute('nonce') === CSPnonce)
) {
const script = document.createElement('script');
script.textContent = node.textContent;
fakeBody.removeChild(node);
parent.appendChild(script);
} else {
parent.appendChild(node);
}
await replaceScriptsAndMove(node, parent);
}
}

Expand All @@ -170,6 +251,7 @@ describe('ReactDOMFizzServer', () => {
document = jsdom.window.document;
container = document;
buffer = '';
await replaceScriptsAndMove(document.documentElement);
}

function getVisibleChildren(element) {
Expand All @@ -179,6 +261,7 @@ describe('ReactDOMFizzServer', () => {
if (node.nodeType === 1) {
if (
node.tagName !== 'SCRIPT' &&
node.tagName !== 'script' &&
node.tagName !== 'TEMPLATE' &&
node.tagName !== 'template' &&
!node.hasAttribute('hidden') &&
Expand Down Expand Up @@ -289,11 +372,26 @@ describe('ReactDOMFizzServer', () => {
return <As>{readText(text)}</As>;
}

function getNonScriptChildren(node) {
return Array.from(node.childNodes).filter(
n => n.tagName !== 'SCRIPT' && n.tagName !== 'script',
);
}

function getNonScriptInnerHtml(node) {
return node.innerHTML.replace(matchScriptTag, '');
}

function renderToPipeableStream(jsx, options) {
// Merge options with renderOptions, which may contain featureFlag specific behavior
if (options) {
return ReactDOMFizzServer.renderToPipeableStream(jsx, options);
const mergedOptions = {
...renderOptions,
...options,
};
return ReactDOMFizzServer.renderToPipeableStream(jsx, mergedOptions);
} else {
return ReactDOMFizzServer.renderToPipeableStream(jsx);
return ReactDOMFizzServer.renderToPipeableStream(jsx, renderOptions);
}
}

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

expect(container.innerHTML).toEqual(
// strip inserted external runtime
expect(getNonScriptInnerHtml(container)).toEqual(
'<div>hello<b>world, <!-- -->Foo</b>!</div>',
);
const errors = [];
Expand Down Expand Up @@ -4811,7 +4910,8 @@ describe('ReactDOMFizzServer', () => {
pipe(writable);
});

expect(container.innerHTML).toEqual(
// strip inserted external runtime
expect(getNonScriptInnerHtml(container)).toEqual(
'<div><!--$-->hello<!-- -->world<!-- --><!--/$--><!--$-->world<!-- --><!--/$--><!--$-->hello<!-- -->world<!-- --><br><!--/$--><!--$-->world<!-- --><br><!--/$--></div>',
);

Expand Down
Loading

0 comments on commit 8e3dc30

Please sign in to comment.