diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js index e5f39dfe0b168..1d3a9913517d3 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js @@ -2442,27 +2442,29 @@ export function writeEarlyPreamble( // If we emitted a preamble early it will have flushed and . // We check that we haven't flushed anything yet which is equivalent // to checking whether we have not flushed an or - if (responseState.flushed === NONE && responseState.rendered !== NONE) { - let i = 0; - const {htmlChunks, headChunks} = responseState; - if (htmlChunks.length) { - for (i = 0; i < htmlChunks.length; i++) { - writeChunk(destination, htmlChunks[i]); + if (responseState.rendered !== NONE) { + if (responseState.flushed === NONE) { + let i = 0; + const {htmlChunks, headChunks} = responseState; + if (htmlChunks.length) { + for (i = 0; i < htmlChunks.length; i++) { + writeChunk(destination, htmlChunks[i]); + } + } else { + writeChunk(destination, DOCTYPE); + writeChunk(destination, startChunkForTag('html')); + writeChunk(destination, endOfStartTag); } - } else { - writeChunk(destination, DOCTYPE); - writeChunk(destination, startChunkForTag('html')); - writeChunk(destination, endOfStartTag); - } - if (headChunks.length) { - for (i = 0; i < headChunks.length; i++) { - writeChunk(destination, headChunks[i]); + if (headChunks.length) { + for (i = 0; i < headChunks.length; i++) { + writeChunk(destination, headChunks[i]); + } + } else { + writeChunk(destination, startChunkForTag('head')); + writeChunk(destination, endOfStartTag); } - } else { - writeChunk(destination, startChunkForTag('head')); - writeChunk(destination, endOfStartTag); + responseState.flushed |= HTML | HEAD; } - responseState.flushed |= HTML | HEAD; return writeEarlyResources( destination, @@ -2587,14 +2589,16 @@ export function prepareForFallback(responseState: ResponseState): void { if (__DEV__) { (responseState: any).inFallbackDEV = true; } - // This function would ideally check somethign to see whether embedding - // was required however at the moment the only time we use a Request Fallback - // is when we use renderIntoDocument which is the only variant where which - // utilizes fallback children. We assume we're in that mode if this function - // is called and update the requirement accordingly + // Reset rendered states responseState.htmlChunks = []; responseState.headChunks = []; responseState.rendered = NONE; + + // Move fallback bootstrap to bootstrap if configured + const fallbackBootstrapChunks = responseState.fallbackBootstrapChunks; + if (fallbackBootstrapChunks && fallbackBootstrapChunks.length) { + responseState.bootstrapChunks = fallbackBootstrapChunks; + } } export function writeCompletedRoot( diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerLegacyFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerLegacyFormatConfig.js index 84dd47e706f94..cfc0d3140c435 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerLegacyFormatConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerLegacyFormatConfig.js @@ -44,6 +44,8 @@ export type ResponseState = { requiresEmbedding: boolean, rendered: DocumentStructureTag, flushed: DocumentStructureTag, + charsetChunks: Array, + hoistableChunks: Array, placeholderPrefix: PrecomputedChunk, segmentPrefix: PrecomputedChunk, boundaryPrefix: string, @@ -87,6 +89,8 @@ export function createResponseState( requiresEmbedding: false, rendered: NONE, flushed: NONE, + charsetChunks: [], + hoistableChunks: [], placeholderPrefix: responseState.placeholderPrefix, segmentPrefix: responseState.segmentPrefix, boundaryPrefix: responseState.boundaryPrefix, diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 6510c8fea2a35..335e4b8842af2 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -212,6 +212,12 @@ describe('ReactDOMFizzServer', () => { while (true) { const bufferedContent = buffer; + document.__headOpen = + document.__headOpen || + ((bufferedContent.includes('') || + bufferedContent.includes('')); + let parent; let temporaryHostElement; if (bufferedContent.startsWith('')) { @@ -255,6 +261,12 @@ describe('ReactDOMFizzServer', () => { temporaryHostElement = document.createElement('head'); temporaryHostElement.innerHTML = headContent; buffer = bodyContent; + document.__headOpen = false; + } else if (document.__headOpen) { + parent = document.head; + temporaryHostElement = document.createElement('head'); + temporaryHostElement.innerHTML = bufferedContent; + buffer = ''; } else { parent = document.body; temporaryHostElement = document.createElement('body'); @@ -6154,6 +6166,7 @@ describe('ReactDOMFizzServer', () => { }); describe('renderIntoDocument', () => { + // @gate enableFloat && enableFizzIntoDocument it('can render arbitrary HTML into a Document', async () => { let content = ''; writable.on('data', chunk => (content += chunk)); @@ -6178,6 +6191,7 @@ describe('ReactDOMFizzServer', () => { ); }); + // @gate enableFloat && enableFizzIntoDocument it('can render into a Document', async () => { let content = ''; writable.on('data', chunk => (content += chunk)); @@ -6200,6 +6214,7 @@ describe('ReactDOMFizzServer', () => { ); }); + // @gate enableFloat && enableFizzIntoDocument it('can render into a Document', async () => { let content = ''; writable.on('data', chunk => (content += chunk)); @@ -6233,6 +6248,7 @@ describe('ReactDOMFizzServer', () => { ); }); + // @gate enableFloat && enableFizzIntoDocument it('inserts an empty head when rendering if no is provided', async () => { let content = ''; writable.on('data', chunk => (content += chunk)); @@ -6257,6 +6273,7 @@ describe('ReactDOMFizzServer', () => { ); }); + // @gate enableFloat && enableFizzIntoDocument it('can render a fallback if the shell errors', async () => { function Throw() { throw new Error('uh oh'); @@ -6299,6 +6316,7 @@ describe('ReactDOMFizzServer', () => { ); }); + // @gate enableFloat && enableFizzIntoDocument it('can render a fallback if the shell errors even if the preamble has already been flushed', async () => { function Throw() { throw new Error('uh oh'); @@ -6370,5 +6388,389 @@ describe('ReactDOMFizzServer', () => { , ); }); + + // @gate enableFloat && enableFizzIntoDocument + it('can render an empty fallback', async () => { + function Throw() { + throw new Error('uh oh'); + } + + let content = ''; + writable.on('data', chunk => (content += chunk)); + + let didBootstrap = false; + function bootstrap() { + didBootstrap = true; + } + window.__INIT__ = bootstrap; + + const errors = []; + await act(() => { + const {pipe} = renderIntoDocumentAsPipeableStream( + + + + + + + , + undefined, + { + onError(err) { + errors.push(err.message); + }, + fallbackBootstrapScriptContent: '__INIT__()', + }, + ); + pipe(writable); + }); + + expect(errors).toEqual(['uh oh']); + expect(didBootstrap).toBe(true); + + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + , + ); + }); + + // @gate enableFloat && enableFizzIntoDocument + it('emits fallback bootstrap scripts if configured when rendering the fallback shell', async () => { + function Throw() { + throw new Error('uh oh'); + } + + let didBootstrap = false; + function bootstrap() { + didBootstrap = true; + } + window.__INIT__ = bootstrap; + + let didFallback = false; + function fallback() { + didFallback = true; + } + window.__FALLBACK_INIT__ = fallback; + + const errors = []; + await act(() => { + const {pipe} = renderIntoDocumentAsPipeableStream( + + + hello world + + + , +
fallback
, + { + onError(err) { + errors.push(err.message); + }, + bootstrapScriptContent: '__INIT__();', + fallbackBootstrapScriptContent: '__FALLBACK_INIT__();', + }, + ); + pipe(writable); + }); + + expect(getMeaningfulChildren(document)).toEqual( + + + +
fallback
+ + , + ); + + expect(didBootstrap).toBe(false); + expect(didFallback).toBe(true); + }); + + // @gate enableFloat && enableFizzIntoDocument + it('emits bootstrap scripts if no fallback bootstrap scripts are configured when rendering the fallback shell', async () => { + function Throw() { + throw new Error('uh oh'); + } + + let didBootstrap = false; + function bootstrap() { + didBootstrap = true; + } + window.__INIT__ = bootstrap; + + const errors = []; + await act(() => { + const {pipe} = renderIntoDocumentAsPipeableStream( + + + + hello world + + , +
fallback
, + { + onError(err) { + errors.push(err.message); + }, + bootstrapScriptContent: '__INIT__();', + }, + ); + pipe(writable); + }); + + expect(getMeaningfulChildren(document)).toEqual( + + + +
fallback
+ + , + ); + + expect(didBootstrap).toBe(true); + }); + + // @gate enableFloat && enableFizzIntoDocument + it('does not work on the fallback unless the primary children error in the shell', async () => { + function Throw() { + throw new Error('uh oh'); + } + + const logs = []; + function BlockOn({value, children}) { + readText(value); + logs.push(value); + return children; + } + + const errors = []; + await act(() => { + const {pipe} = renderIntoDocumentAsPipeableStream( + + + + + + + + + hello world + + , +
+ fallback +
, + { + onError(err) { + errors.push(err.message); + }, + }, + ); + pipe(writable); + }); + + expect(logs).toEqual([]); + logs.length = 0; + expect(getMeaningfulChildren(document)).toEqual( + + + + , + ); + + // Even though we unblock fallback since the task is not scheduled no log is observed + await act(() => { + resolveText('fallback'); + }); + expect(logs).toEqual([]); + logs.length = 0; + expect(getMeaningfulChildren(document)).toEqual( + + + + , + ); + + // When we resolve the resource it is emitted in the open preamble. + await act(() => { + resolveText('resource'); + }); + expect(logs).toEqual(['resource']); + logs.length = 0; + expect(getMeaningfulChildren(document)).toEqual( + + + + + + , + ); + + // When we resolve the resource it is emitted in the open preamble. + await act(() => { + resolveText('error'); + }); + expect(logs).toEqual(['error', 'fallback']); + logs.length = 0; + expect(getMeaningfulChildren(document)).toEqual( + + + + + +
fallback
+ + , + ); + }); + + // @gate enableFloat && enableFizzIntoDocument + it('only emits stylesheets up to the first precedence during the early preamble', async () => { + function BlockOn({value, children}) { + readText(value); + return children; + } + + await act(() => { + const {pipe} = renderIntoDocumentAsPipeableStream( + + + + + + +