From fe5cabacbd2e1faed53866b396bde481c99f60ca Mon Sep 17 00:00:00 2001 From: Mofei Zhang Date: Mon, 31 Oct 2022 17:01:23 -0400 Subject: [PATCH 01/13] Add renames --- .../src/server/ReactDOMServerFormatConfig.js | 68 ++++++++++--------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js index 91d241e4e7929..5c4892a48eb89 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js @@ -154,6 +154,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, @@ -2384,7 +2386,7 @@ const completeSegmentScript1Full = stringToPrecomputedChunk( ); const completeSegmentScript1Partial = stringToPrecomputedChunk('$RS("'); const completeSegmentScript2 = stringToPrecomputedChunk('","'); -const completeSegmentScript3 = stringToPrecomputedChunk('")'); +const completeSegmentScriptEnd = stringToPrecomputedChunk('")'); export function writeCompletedSegmentInstruction( destination: Destination, @@ -2400,13 +2402,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( @@ -2424,9 +2429,9 @@ const completeBoundaryWithStylesScript1Partial = stringToPrecomputedChunk( '$RR("', ); const completeBoundaryScript2 = stringToPrecomputedChunk('","'); -const completeBoundaryScript2a = stringToPrecomputedChunk('",'); -const completeBoundaryScript3 = stringToPrecomputedChunk('"'); -const completeBoundaryScript4 = stringToPrecomputedChunk(')'); +const completeBoundaryScript3a = stringToPrecomputedChunk('",'); +const completeBoundaryScript3b = stringToPrecomputedChunk('"'); +const completeBoundaryScriptEnd = stringToPrecomputedChunk(')'); export function writeCompletedBoundaryInstruction( destination: Destination, @@ -2469,18 +2474,20 @@ 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( @@ -2488,7 +2495,7 @@ const clientRenderScript1Full = stringToPrecomputedChunk( ); const clientRenderScript1Partial = stringToPrecomputedChunk('$RX("'); const clientRenderScript1A = stringToPrecomputedChunk('"'); -const clientRenderScript2 = stringToPrecomputedChunk(')'); +const clientRenderScriptEnd = stringToPrecomputedChunk(')'); const clientRenderErrorScriptArgInterstitial = stringToPrecomputedChunk(','); export function writeClientRenderBoundaryInstruction( @@ -2514,10 +2521,11 @@ 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, @@ -2525,6 +2533,7 @@ export function writeClientRenderBoundaryInstruction( ); } if (errorMessage || errorComponentStack) { + // ,"JSONString" writeChunk(destination, clientRenderErrorScriptArgInterstitial); writeChunk( destination, @@ -2532,13 +2541,14 @@ export function writeClientRenderBoundaryInstruction( ); } if (errorComponentStack) { + // ,"JSONString" writeChunk(destination, clientRenderErrorScriptArgInterstitial); writeChunk( destination, stringToChunk(escapeJSStringsForInstructionScripts(errorComponentStack)), ); } - return writeChunkAndReturn(destination, clientRenderScript2); + return writeChunkAndReturn(destination, clientRenderScriptEnd); } const regexForJSStringsInInstructionScripts = /[<\u2028\u2029]/g; @@ -2839,6 +2849,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, @@ -2883,10 +2903,7 @@ function writeStyleResourceDependencyHrefOnly( checkAttributeStringCoercion(href, 'href'); } const coercedHref = '' + (href: any); - writeChunk( - destination, - stringToChunk(escapeJSObjectForInstructionScripts(coercedHref)), - ); + writeStyleResourceObject(destination, coercedHref); } function writeStyleResourceDependency( @@ -2900,20 +2917,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)) { @@ -3012,13 +3024,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); } From 5e81c7d1f9cb85d06003edea2749a8e9aabd3022 Mon Sep 17 00:00:00 2001 From: Mofei Zhang Date: Tue, 1 Nov 2022 17:37:34 -0400 Subject: [PATCH 02/13] Add non-execution format, runtime, and unit tests --- .../server/ReactDOMServerExternalRuntime.js | 67 +++- .../src/server/ReactDOMServerFormatConfig.js | 315 +++++++++++++----- .../ReactDOMServerLegacyFormatConfig.js | 22 +- .../src/__tests__/ReactDOMFizzServer-test.js | 78 ++++- .../src/__tests__/ReactDOMFloat-test.js | 20 +- .../src/server/ReactDOMLegacyServerImpl.js | 3 + .../react-dom/src/test-utils/FizzTestUtils.js | 47 ++- .../src/ReactDOMServerFB.js | 3 + 8 files changed, 448 insertions(+), 107 deletions(-) diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js b/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js index 01f53697e2e5f..ab3363716eebc 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js @@ -13,13 +13,60 @@ import { completeSegment, } from './fizz-instruction-set/ReactDOMFizzInstructionSet'; -// Intentionally does nothing. Implementation will be added in future PR. -// eslint-disable-next-line no-unused-vars -const observer = new MutationObserver(mutations => { - // These are only called so I can check what the module output looks like. The - // code is unreachable. - clientRenderBoundary(); - completeBoundaryWithStyles(); - completeBoundary(); - completeSegment(); -}); +// This runtime may be sent to the client multiple times (if FizzServer.render +// is called more than once). Here, we check whether the mutation observer +// was already created / installed +if (!window.$REACT_FIZZ_OBSERVER) { + // TODO: Eventually remove, we currently need to set these globals for + // compatibility with ReactDOMFizzInstructionSet + window.$RC = completeBoundary; + window.$RM = new Map(); + window.$REACT_FIZZ_OBSERVER = new MutationObserver(mutations => { + mutations.forEach(mutation => { + mutation.addedNodes.forEach(handleNode); + }); + }); + // $FlowFixMe[incompatible-call] document.body should exist at this point + window.$REACT_FIZZ_OBSERVER.observe(document.body, { + childList: true, + subtree: true, + }); + + const existingNodes = document.getElementsByTagName('div'); + for (let i = 0; i < existingNodes.length; i++) { + handleNode(existingNodes[i]); + } +} + +function handleNode(node /*: Node */) { + if (node.nodeType !== 1) { + return; + } + // $FlowFixMe[incompatible-cast] + const dataset = (node /*: HTMLElement*/).dataset; + const instr = dataset ? dataset[':fi'] : null; + switch (instr) { + case '$RX': + clientRenderBoundary( + dataset[':a0'], + dataset[':a1'], + dataset[':a2'], + dataset[':a3'], + ); + break; + case '$RR': + // Convert arg2 here, since its type is Array> + completeBoundaryWithStyles( + dataset[':a0'], + dataset[':a1'], + JSON.parse(dataset[':a2']), + ); + break; + case '$RC': + completeBoundary(dataset[':a0'], dataset[':a1'], dataset[':a2']); + break; + case '$RS': + completeSegment(dataset[':a0'], dataset[':a1']); + break; + } +} diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js index 5c4892a48eb89..f9d51be2e3dd6 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js @@ -108,19 +108,25 @@ export const isPrimaryRenderer = true; // Per response, global state that is not contextual to the rendering subtree. export type ResponseState = { bootstrapChunks: Array, - startInlineScript: PrecomputedChunk, placeholderPrefix: PrecomputedChunk, segmentPrefix: PrecomputedChunk, boundaryPrefix: string, idPrefix: string, nextSuspenseID: number, + streamingFormat: 'SCRIPT' | 'DATA', + // state for script streaming format, unused if using external runtime / data + startInlineScript: PrecomputedChunk, sentCompleteSegmentFunction: boolean, sentCompleteBoundaryFunction: boolean, sentClientRenderFunction: boolean, - sentStyleInsertionFunction: boolean, // We allow the legacy renderer to extend this object. + sentStyleInsertionFunction: boolean, + // We allow the legacy renderer to extend this object. ... }; +const dataElementQuotedEnd = stringToPrecomputedChunk('">'); +const dataElementUnquotedEnd = stringToPrecomputedChunk('>'); + const startInlineScript = stringToPrecomputedChunk(''); @@ -172,6 +178,7 @@ export function createResponseState( ''); +const completeSegmentData1 = stringToPrecomputedChunk( + ''); +const dataElementQuotedEnd = stringToPrecomputedChunk('">'); +const dataElementUnquotedEnd = stringToPrecomputedChunk('>'); const startInlineScript = stringToPrecomputedChunk(''); @@ -178,7 +182,7 @@ export function createResponseState( ''); const completeSegmentData1 = stringToPrecomputedChunk( - '