diff --git a/fixtures/ssr/server/render.js b/fixtures/ssr/server/render.js index a4fe698858ab1..e20b9a35dc502 100644 --- a/fixtures/ssr/server/render.js +++ b/fixtures/ssr/server/render.js @@ -1,5 +1,6 @@ import React from 'react'; import {renderToPipeableStream} from 'react-dom/server'; +import {Writable} from 'stream'; import App from '../src/components/App'; @@ -14,11 +15,41 @@ if (process.env.NODE_ENV === 'development') { assets = require('../build/asset-manifest.json'); } +class ThrottledWritable extends Writable { + constructor(destination) { + super(); + this.destination = destination; + this.delay = 150; + } + + _write(chunk, encoding, callback) { + let o = 0; + const write = () => { + this.destination.write(chunk.slice(o, o + 100), encoding, x => { + o += 100; + if (o < chunk.length) { + setTimeout(write, this.delay); + } else { + callback(x); + } + }); + }; + setTimeout(write, this.delay); + } + + _final(callback) { + setTimeout(() => { + this.destination.end(callback); + }, this.delay); + } +} + export default function render(url, res) { res.socket.on('error', error => { // Log fatal errors console.error('Fatal', error); }); + console.log('hello'); let didError = false; const {pipe, abort} = renderToPipeableStream(, { bootstrapScripts: [assets['main.js']], @@ -26,7 +57,10 @@ export default function render(url, res) { // If something errored before we started streaming, we set the error code appropriately. res.statusCode = didError ? 500 : 200; res.setHeader('Content-type', 'text/html'); - pipe(res); + // To test the actual chunks taking time to load over the network, we throttle + // the stream a bit. + const throttledResponse = new ThrottledWritable(res); + pipe(throttledResponse); }, onShellError(x) { // Something errored before we could complete the shell so we emit an alternative shell. diff --git a/fixtures/ssr/src/components/Chrome.js b/fixtures/ssr/src/components/Chrome.js index 5cf81a877f7e3..984c726a02652 100644 --- a/fixtures/ssr/src/components/Chrome.js +++ b/fixtures/ssr/src/components/Chrome.js @@ -37,6 +37,7 @@ export default class Chrome extends Component { +

This should appear in the first paint.

'); const startScriptSrc = stringToPrecomputedChunk(''); +const scriptNonce = stringToPrecomputedChunk(' nonce="'); +const scriptIntegirty = stringToPrecomputedChunk(' integrity="'); +const scriptCrossOrigin = stringToPrecomputedChunk(' crossorigin="'); +const endAsyncScript = stringToPrecomputedChunk(' async="">'); /** * This escaping function is designed to work with with inline scripts where the entire @@ -367,7 +368,7 @@ export function createRenderState( nonce === undefined ? startInlineScript : stringToPrecomputedChunk( - '', + '' + + '', ); }); @@ -4189,7 +4190,7 @@ describe('ReactDOMFizzServer', () => { renderOptions.unstable_externalRuntimeSrc, ).map(n => n.outerHTML), ).toEqual([ - '', + '', '', '', '', @@ -4276,7 +4277,7 @@ describe('ReactDOMFizzServer', () => { renderOptions.unstable_externalRuntimeSrc, ).map(n => n.outerHTML), ).toEqual([ - '', + '', '', '', '', @@ -4512,7 +4513,7 @@ describe('ReactDOMFizzServer', () => { // the html should be as-is expect(document.documentElement.innerHTML).toEqual( - '

hello world!

', + '

hello world!

', ); }); @@ -6492,7 +6493,7 @@ describe('ReactDOMFizzServer', () => { }); expect(document.documentElement.outerHTML).toEqual( - '', + '', ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js index 4022f227a8abe..f5b01d2462403 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js @@ -85,7 +85,7 @@ describe('ReactDOMFizzServerBrowser', () => { ); const result = await readResult(stream); expect(result).toMatchInlineSnapshot( - `"hello world"`, + `"hello world"`, ); }); @@ -99,7 +99,7 @@ describe('ReactDOMFizzServerBrowser', () => { ); const result = await readResult(stream); expect(result).toMatchInlineSnapshot( - `"
hello world
"`, + `"
hello world
"`, ); }); @@ -529,7 +529,7 @@ describe('ReactDOMFizzServerBrowser', () => { const result = await readResult(stream); expect(result).toEqual( - 'foobar', + 'foobar', ); }); @@ -547,7 +547,7 @@ describe('ReactDOMFizzServerBrowser', () => { expect(result).toMatchInlineSnapshot( // TODO: remove interpolation because it prevents snapshot updates. // eslint-disable-next-line jest/no-interpolation-in-snapshots - `"
hello world
"`, + `"
hello world
"`, ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerEdge-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerEdge-test.js index c442f1813836c..1eefe1a4082e6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerEdge-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerEdge-test.js @@ -72,7 +72,7 @@ describe('ReactDOMFizzServerEdge', () => { }); expect(result).toMatchInlineSnapshot( - `"
hello
"`, + `"
hello
"`, ); }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js index e97b4a29a7497..2704c243eba48 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js @@ -79,7 +79,7 @@ describe('ReactDOMFizzServerNode', () => { }); // with Float, we emit empty heads if they are elided when rendering expect(output.result).toMatchInlineSnapshot( - `"hello world"`, + `"hello world"`, ); }); @@ -97,7 +97,7 @@ describe('ReactDOMFizzServerNode', () => { pipe(writable); }); expect(output.result).toMatchInlineSnapshot( - `"
hello world
"`, + `"
hello world
"`, ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js index 96e6538cd2196..de6e21b557a1d 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js @@ -106,7 +106,10 @@ describe('ReactDOMFizzStatic', () => { node.tagName !== 'TEMPLATE' && node.tagName !== 'template' && !node.hasAttribute('hidden') && - !node.hasAttribute('aria-hidden') + !node.hasAttribute('aria-hidden') && + // Ignore the render blocking expect + (node.getAttribute('rel') !== 'expect' || + node.getAttribute('blocking') !== 'render') ) { const props = {}; const attributes = node.attributes; diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js index f973a5ed4d6e0..7eecb16cf82f6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js @@ -187,7 +187,7 @@ describe('ReactDOMFizzStaticBrowser', () => { ); const prelude = await readContent(result.prelude); expect(prelude).toMatchInlineSnapshot( - `"hello world"`, + `"hello world"`, ); }); @@ -201,7 +201,7 @@ describe('ReactDOMFizzStaticBrowser', () => { ); const prelude = await readContent(result.prelude); expect(prelude).toMatchInlineSnapshot( - `"
hello world
"`, + `"
hello world
"`, ); }); @@ -1428,7 +1428,8 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(await readContent(content)).toBe( '' + '' + - 'Hello', + '' + + 'Hello', ); }); @@ -1474,7 +1475,8 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(await readContent(content)).toBe( '' + '' + - 'Hello', + '' + + 'Hello', ); }); @@ -1525,7 +1527,8 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(await readContent(content)).toBe( '' + '' + - '
Hello
', + '' + + '
Hello
', ); }); @@ -1607,7 +1610,8 @@ describe('ReactDOMFizzStaticBrowser', () => { let result = decoder.decode(value, {stream: true}); expect(result).toBe( - 'hello', + '' + + 'hello', ); await 1; @@ -1631,7 +1635,9 @@ describe('ReactDOMFizzStaticBrowser', () => { const slice = result.slice(0, instructionIndex + '$RC'.length); expect(slice).toBe( - 'hello"`, + `"
hello world
"`, ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 7404cec64a00c..5328a4ac9e055 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -250,7 +250,10 @@ describe('ReactDOMFloat', () => { node.tagName !== 'TEMPLATE' && node.tagName !== 'template' && !node.hasAttribute('hidden') && - !node.hasAttribute('aria-hidden')) + !node.hasAttribute('aria-hidden') && + // Ignore the render blocking expect + (node.getAttribute('rel') !== 'expect' || + node.getAttribute('blocking') !== 'render')) ) { const props = {}; const attributes = node.attributes; @@ -690,7 +693,9 @@ describe('ReactDOMFloat', () => { pipe(writable); }); expect(chunks).toEqual([ - 'foobar', + '' + + 'foo' + + 'bar', '', ]); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMLegacyFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMLegacyFloat-test.js index 52c9746abdb4f..f2cabafc9f575 100644 --- a/packages/react-dom/src/__tests__/ReactDOMLegacyFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMLegacyFloat-test.js @@ -34,7 +34,8 @@ describe('ReactDOMFloat', () => { ); expect(result).toEqual( - 'title', + '' + + 'title', ); }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js b/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js index 84db05bc779db..d887972e92ca1 100644 --- a/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js @@ -104,7 +104,10 @@ describe('ReactDOM HostSingleton', () => { el.tagName !== 'TEMPLATE' && el.tagName !== 'template' && !el.hasAttribute('hidden') && - !el.hasAttribute('aria-hidden')) || + !el.hasAttribute('aria-hidden') && + // Ignore the render blocking expect + (node.getAttribute('rel') !== 'expect' || + node.getAttribute('blocking') !== 'render')) || el.hasAttribute('data-meaningful') ) { const props = {}; diff --git a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js index 9522a920bc291..2b54bc90090e4 100644 --- a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js +++ b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js @@ -77,12 +77,16 @@ describe('rendering React components at document', () => { await act(() => { root = ReactDOMClient.hydrateRoot(testDocument, ); }); - expect(testDocument.body.innerHTML).toBe('Hello world'); + expect(testDocument.body.innerHTML).toBe( + 'Hello world' + '', + ); await act(() => { root.render(); }); - expect(testDocument.body.innerHTML).toBe('Hello moon'); + expect(testDocument.body.innerHTML).toBe( + 'Hello moon' + '', + ); expect(body === testDocument.body).toBe(true); }); @@ -107,7 +111,9 @@ describe('rendering React components at document', () => { await act(() => { root = ReactDOMClient.hydrateRoot(testDocument, ); }); - expect(testDocument.body.innerHTML).toBe('Hello world'); + expect(testDocument.body.innerHTML).toBe( + 'Hello world' + '', + ); const originalDocEl = testDocument.documentElement; const originalHead = testDocument.head; @@ -118,8 +124,10 @@ describe('rendering React components at document', () => { expect(testDocument.firstChild).toBe(originalDocEl); expect(testDocument.head).toBe(originalHead); expect(testDocument.body).toBe(originalBody); - expect(originalBody.firstChild).toEqual(null); - expect(originalHead.firstChild).toEqual(null); + expect(originalBody.innerHTML).toBe(''); + expect(originalHead.innerHTML).toBe( + '', + ); }); it('should not be able to switch root constructors', async () => { @@ -157,13 +165,17 @@ describe('rendering React components at document', () => { root = ReactDOMClient.hydrateRoot(testDocument, ); }); - expect(testDocument.body.innerHTML).toBe('Hello world'); + expect(testDocument.body.innerHTML).toBe( + 'Hello world' + '', + ); await act(() => { root.render(); }); - expect(testDocument.body.innerHTML).toBe('Goodbye world'); + expect(testDocument.body.innerHTML).toBe( + '' + 'Goodbye world', + ); }); it('should be able to mount into document', async () => { @@ -192,7 +204,9 @@ describe('rendering React components at document', () => { ); }); - expect(testDocument.body.innerHTML).toBe('Hello world'); + expect(testDocument.body.innerHTML).toBe( + 'Hello world' + '', + ); }); it('cannot render over an existing text child at the root', async () => { @@ -325,7 +339,9 @@ describe('rendering React components at document', () => { : [], ); expect(testDocument.body.innerHTML).toBe( - favorSafetyOverHydrationPerf ? 'Hello world' : 'Goodbye world', + favorSafetyOverHydrationPerf + ? 'Hello world' + : 'Goodbye world', ); }); diff --git a/packages/react-dom/src/test-utils/FizzTestUtils.js b/packages/react-dom/src/test-utils/FizzTestUtils.js index 537c64a889a7d..12c768e1a0008 100644 --- a/packages/react-dom/src/test-utils/FizzTestUtils.js +++ b/packages/react-dom/src/test-utils/FizzTestUtils.js @@ -150,7 +150,10 @@ function getVisibleChildren(element: Element): React$Node { node.tagName !== 'TEMPLATE' && node.tagName !== 'template' && !node.hasAttribute('hidden') && - !node.hasAttribute('aria-hidden') + !node.hasAttribute('aria-hidden') && + // Ignore the render blocking expect + (node.getAttribute('rel') !== 'expect' || + node.getAttribute('blocking') !== 'render') ) { const props: any = {}; const attributes = node.attributes; diff --git a/packages/react-markup/src/ReactFizzConfigMarkup.js b/packages/react-markup/src/ReactFizzConfigMarkup.js index 3d08ed1ee64a2..444952dc58502 100644 --- a/packages/react-markup/src/ReactFizzConfigMarkup.js +++ b/packages/react-markup/src/ReactFizzConfigMarkup.js @@ -17,7 +17,10 @@ import type { FormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; -import {pushStartInstance as pushStartInstanceImpl} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +import { + pushStartInstance as pushStartInstanceImpl, + writePreambleStart as writePreambleStartImpl, +} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; import type { Destination, @@ -62,13 +65,11 @@ export { writeEndPendingSuspenseBoundary, writeHoistablesForBoundary, writePlaceholder, - writeCompletedRoot, createRootFormatContext, createRenderState, createResumableState, createPreambleState, createHoistableState, - writePreambleStart, writePreambleEnd, writeHoistables, writePostamble, @@ -203,5 +204,30 @@ export function writeEndClientRenderedSuspenseBoundary( return true; } +export function writePreambleStart( + destination: Destination, + resumableState: ResumableState, + renderState: RenderState, + willFlushAllSegments: boolean, + skipExpect?: boolean, // Used as an override by ReactFizzConfigMarkup +): void { + return writePreambleStartImpl( + destination, + resumableState, + renderState, + willFlushAllSegments, + true, // skipExpect + ); +} + +export function writeCompletedRoot( + destination: Destination, + resumableState: ResumableState, + renderState: RenderState, +): boolean { + // Markup doesn't have any bootstrap scripts nor shell completions. + return true; +} + export type TransitionStatus = FormStatus; export const NotPendingTransition: TransitionStatus = NotPending; diff --git a/packages/react-server-dom-fb/src/__tests__/ReactDOMServerFB-test.internal.js b/packages/react-server-dom-fb/src/__tests__/ReactDOMServerFB-test.internal.js index 35b41cbd230d0..6d022ceb26c17 100644 --- a/packages/react-server-dom-fb/src/__tests__/ReactDOMServerFB-test.internal.js +++ b/packages/react-server-dom-fb/src/__tests__/ReactDOMServerFB-test.internal.js @@ -59,7 +59,7 @@ describe('ReactDOMServerFB', () => { }); const result = readResult(stream); expect(result).toMatchInlineSnapshot( - `"
hello world
"`, + `"
hello world
"`, ); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index 0b16b3b32114d..80562624eb173 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -193,7 +193,10 @@ describe('ReactFlightDOM', () => { node.tagName !== 'TEMPLATE' && node.tagName !== 'template' && !node.hasAttribute('hidden') && - !node.hasAttribute('aria-hidden')) + !node.hasAttribute('aria-hidden') && + // Ignore the render blocking expect + (node.getAttribute('rel') !== 'expect' || + node.getAttribute('blocking') !== 'render')) ) { const props = {}; const attributes = node.attributes; @@ -1917,11 +1920,15 @@ describe('ReactFlightDOM', () => { expect(content1).toEqual( '' + - '

hello world

', + '' + + '' + + '

hello world

', ); expect(content2).toEqual( '' + - '

hello world

', + '' + + '' + + '

hello world

', ); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index f3fa444fc1528..4313c379b70bd 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -1899,8 +1899,8 @@ describe('ReactFlightDOMBrowser', () => { } expect(content).toEqual( - '' + - '

hello world

', + '' + + '

hello world

', ); }); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 487751c6be385..52d677ad1be40 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -5157,7 +5157,11 @@ function flushCompletedQueues( ); flushSegment(request, destination, completedRootSegment, null); request.completedRootSegment = null; - writeCompletedRoot(destination, request.renderState); + writeCompletedRoot( + destination, + request.resumableState, + request.renderState, + ); } writeHoistables(destination, request.resumableState, request.renderState); diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index 5cf0518c41833..d745eaed4c04d 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -393,7 +393,8 @@ function getPlugins( }; }, }, - bundle.tsconfig != null ? commonjs() : false, + // See https://github.com/rollup/plugins/issues/1425 + bundle.tsconfig != null ? commonjs({strictRequires: true}) : false, // Shim any modules that need forking in this environment. useForks(forks), // Ensure we don't try to bundle any fbjs modules.