From d49123f73f12564223c890bfa36be537de2c571d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Sat, 19 Oct 2024 22:33:28 -0400 Subject: [PATCH] Expose prerender() for SSG in stable (#31298) When we added `renderToReadableStream` we added the `allReady` helper to make it easier to do SSG rendering but it's kind of awkward to wire up that way. Since we're also discouraging `renderToString` in React 19 the cliff is kind of awkward. ([As noted by Docusaurus.](https://github.com/facebook/react/pull/24752#issuecomment-2178309299)) The idea of the `react-dom/static` `prerender` API was that this would be the replacement for SSG rendering. Awkwardly this entry point actually already exists in stable but it has only `undefined` exports. Since then we've also added other useful heuristics into the `prerender` branch that makes this really the favored and easiest to use API for the prerender (SSG/ISR) use case. `prerender` is also used for Partial Prerendering but that part is still experimental. However, we can expose only the `prerender` API on `react-dom/static` without it returning the `postponeState`. Instead the stream is on `prelude`. The naming is a bit awkward if you don't consider resuming but it's the same thing. It's really just `renderToReadable` stream with automatic `allReady` and better heuristics for prerendering. --- .../src/__tests__/ReactDOMFizzStatic-test.js | 16 ++++++------- .../ReactDOMFizzStaticBrowser-test.js | 24 +++++-------------- .../src/server/ReactDOMFizzStaticBrowser.js | 15 ++++++++---- .../src/server/ReactDOMFizzStaticEdge.js | 15 ++++++++---- .../src/server/ReactDOMFizzStaticNode.js | 15 ++++++++---- .../server/react-dom-server.browser.stable.js | 1 + .../server/react-dom-server.edge.stable.js | 1 + .../server/react-dom-server.node.stable.js | 1 + 8 files changed, 49 insertions(+), 39 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js index 03db4e3f5ed8f..96e6538cd2196 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js @@ -32,9 +32,7 @@ describe('ReactDOMFizzStatic', () => { React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); - if (__EXPERIMENTAL__) { - ReactDOMFizzStatic = require('react-dom/static'); - } + ReactDOMFizzStatic = require('react-dom/static'); Stream = require('stream'); Suspense = React.Suspense; @@ -212,7 +210,6 @@ describe('ReactDOMFizzStatic', () => { return readText(text); } - // @gate experimental it('should render a fully static document, send it and then hydrate it', async () => { function App() { return ( @@ -230,7 +227,11 @@ describe('ReactDOMFizzStatic', () => { const result = await promise; - expect(result.postponed).toBe(null); + expect(result.postponed).toBe( + gate(flags => flags.enableHalt || flags.enablePostpone) + ? null + : undefined, + ); await act(async () => { result.prelude.pipe(writable); @@ -244,7 +245,6 @@ describe('ReactDOMFizzStatic', () => { expect(getVisibleChildren(container)).toEqual(
Hello
); }); - // @gate experimental it('should support importMap option', async () => { const importMap = { foo: 'path/to/foo.js', @@ -265,7 +265,6 @@ describe('ReactDOMFizzStatic', () => { ]); }); - // @gate experimental it('supports onHeaders', async () => { let headers; function onHeaders(x) { @@ -300,7 +299,7 @@ describe('ReactDOMFizzStatic', () => { expect(getVisibleChildren(container)).toEqual('hello'); }); - // @gate experimental && enablePostpone + // @gate enablePostpone it('includes stylesheet preloads in onHeaders when postponing in the Shell', async () => { let headers; function onHeaders(x) { @@ -336,7 +335,6 @@ describe('ReactDOMFizzStatic', () => { expect(getVisibleChildren(container)).toEqual(undefined); }); - // @gate experimental it('will prerender Suspense fallbacks before children', async () => { const values = []; function Indirection({children}) { diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js index 1512e1d4c7e99..535bd5d7ba815 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js @@ -42,9 +42,7 @@ describe('ReactDOMFizzStaticBrowser', () => { React = require('react'); ReactDOM = require('react-dom'); ReactDOMFizzServer = require('react-dom/server.browser'); - if (__EXPERIMENTAL__) { - ReactDOMFizzStatic = require('react-dom/static.browser'); - } + ReactDOMFizzStatic = require('react-dom/static.browser'); Suspense = React.Suspense; container = document.createElement('div'); document.body.appendChild(container); @@ -131,7 +129,6 @@ describe('ReactDOMFizzStaticBrowser', () => { await insertNodesAndExecuteScripts(temp, container, null); } - // @gate experimental it('should call prerender', async () => { const result = await serverAct(() => ReactDOMFizzStatic.prerender(
hello world
), @@ -140,7 +137,6 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(prelude).toMatchInlineSnapshot(`"
hello world
"`); }); - // @gate experimental it('should emit DOCTYPE at the root of the document', async () => { const result = await serverAct(() => ReactDOMFizzStatic.prerender( @@ -155,7 +151,6 @@ describe('ReactDOMFizzStaticBrowser', () => { ); }); - // @gate experimental it('should emit bootstrap script src at the end', async () => { const result = await serverAct(() => ReactDOMFizzStatic.prerender(
hello world
, { @@ -170,7 +165,6 @@ describe('ReactDOMFizzStaticBrowser', () => { ); }); - // @gate experimental it('emits all HTML as one unit', async () => { let hasLoaded = false; let resolve; @@ -202,7 +196,6 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(prelude).toMatchInlineSnapshot(`"
Done
"`); }); - // @gate experimental it('should reject the promise when an error is thrown at the root', async () => { const reportedErrors = []; let caughtError = null; @@ -226,7 +219,6 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(reportedErrors).toEqual([theError]); }); - // @gate experimental it('should reject the promise when an error is thrown inside a fallback', async () => { const reportedErrors = []; let caughtError = null; @@ -252,7 +244,6 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(reportedErrors).toEqual([theError]); }); - // @gate experimental it('should not error the stream when an error is thrown inside suspense boundary', async () => { const reportedErrors = []; const result = await serverAct(() => @@ -275,7 +266,6 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(reportedErrors).toEqual([theError]); }); - // @gate experimental it('should be able to complete by aborting even if the promise never resolves', async () => { const errors = []; const controller = new AbortController(); @@ -306,7 +296,6 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(errors).toEqual(['The operation was aborted.']); }); - // @gate experimental // @gate !enableHalt it('should reject if aborting before the shell is complete and enableHalt is disabled', async () => { const errors = []; @@ -376,7 +365,6 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(content).toBe(''); }); - // @gate experimental it('should be able to abort before something suspends', async () => { const errors = []; const controller = new AbortController(); @@ -419,7 +407,6 @@ describe('ReactDOMFizzStaticBrowser', () => { } }); - // @gate experimental // @gate !enableHalt it('should reject if passing an already aborted signal and enableHalt is disabled', async () => { const errors = []; @@ -493,7 +480,6 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(content).toBe(''); }); - // @gate experimental it('supports custom abort reasons with a string', async () => { const promise = new Promise(r => {}); function Wait() { @@ -536,7 +522,6 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(errors).toEqual(['foobar', 'foobar']); }); - // @gate experimental it('supports custom abort reasons with an Error', async () => { const promise = new Promise(r => {}); function Wait() { @@ -1610,7 +1595,6 @@ describe('ReactDOMFizzStaticBrowser', () => { ); }); - // @gate experimental it('logs an error if onHeaders throws but continues the prerender', async () => { const errors = []; function onError(error) { @@ -1627,7 +1611,11 @@ describe('ReactDOMFizzStaticBrowser', () => { onError, }), ); - expect(prerendered.postponed).toBe(null); + expect(prerendered.postponed).toBe( + gate(flags => flags.enableHalt || flags.enablePostpone) + ? null + : undefined, + ); expect(errors).toEqual(['bad onHeaders']); await readIntoContainer(prerendered.prelude); diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js index 6785515bbebe7..82d7b12f54b6b 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js @@ -38,6 +38,8 @@ import { createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +import {enablePostpone, enableHalt} from 'shared/ReactFeatureFlags'; + import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion'; ensureCorrectIsomorphicReactVersion(); @@ -85,10 +87,15 @@ function prerender( {highWaterMark: 0}, ); - const result = { - postponed: getPostponedState(request), - prelude: stream, - }; + const result: StaticResult = + enablePostpone || enableHalt + ? { + postponed: getPostponedState(request), + prelude: stream, + } + : ({ + prelude: stream, + }: any); resolve(result); } diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js b/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js index 5a7467002cb5c..1243b18adaab1 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js @@ -38,6 +38,8 @@ import { createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +import {enablePostpone, enableHalt} from 'shared/ReactFeatureFlags'; + import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion'; ensureCorrectIsomorphicReactVersion(); @@ -85,10 +87,15 @@ function prerender( {highWaterMark: 0}, ); - const result = { - postponed: getPostponedState(request), - prelude: stream, - }; + const result: StaticResult = + enablePostpone || enableHalt + ? { + postponed: getPostponedState(request), + prelude: stream, + } + : ({ + prelude: stream, + }: any); resolve(result); } diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js index 9b9cd680c16b9..e90e17cc207ee 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js @@ -39,6 +39,8 @@ import { createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +import {enablePostpone, enableHalt} from 'shared/ReactFeatureFlags'; + import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion'; ensureCorrectIsomorphicReactVersion(); @@ -94,10 +96,15 @@ function prerenderToNodeStream( }); const writable = createFakeWritable(readable); - const result = { - postponed: getPostponedState(request), - prelude: readable, - }; + const result: StaticResult = + enablePostpone || enableHalt + ? { + postponed: getPostponedState(request), + prelude: readable, + } + : ({ + prelude: readable, + }: any); resolve(result); } const resumableState = createResumableState( diff --git a/packages/react-dom/src/server/react-dom-server.browser.stable.js b/packages/react-dom/src/server/react-dom-server.browser.stable.js index 3471bba15ac94..2c8f8b5dd59a3 100644 --- a/packages/react-dom/src/server/react-dom-server.browser.stable.js +++ b/packages/react-dom/src/server/react-dom-server.browser.stable.js @@ -8,3 +8,4 @@ */ export {renderToReadableStream, version} from './ReactDOMFizzServerBrowser.js'; +export {prerender} from './ReactDOMFizzStaticBrowser.js'; diff --git a/packages/react-dom/src/server/react-dom-server.edge.stable.js b/packages/react-dom/src/server/react-dom-server.edge.stable.js index b2f0278099ce3..5f47ecafd371a 100644 --- a/packages/react-dom/src/server/react-dom-server.edge.stable.js +++ b/packages/react-dom/src/server/react-dom-server.edge.stable.js @@ -8,3 +8,4 @@ */ export {renderToReadableStream, version} from './ReactDOMFizzServerEdge.js'; +export {prerender} from './ReactDOMFizzStaticEdge.js'; diff --git a/packages/react-dom/src/server/react-dom-server.node.stable.js b/packages/react-dom/src/server/react-dom-server.node.stable.js index bd17b91a9602a..4003622625110 100644 --- a/packages/react-dom/src/server/react-dom-server.node.stable.js +++ b/packages/react-dom/src/server/react-dom-server.node.stable.js @@ -8,3 +8,4 @@ */ export {renderToPipeableStream, version} from './ReactDOMFizzServerNode.js'; +export {prerenderToNodeStream} from './ReactDOMFizzStaticNode.js';