@samitouri / QOS-React / commits / fa6eab5854

[Flight] Implement prerender (#30686)

Prerendering in flight is similar to prerendering in Fizz. Instead of receiving a result (the stream) immediately a promise is returned which resolves to the stream when the prerender is complete. The promise will reject if the flight render fatally errors otherwise it will resolve when the render is completed or is aborted.

Josh Story committed Aug 15, 2024 at 14:28 UTC fa6eab58541330349480690ef4e211520cc08d94
59 files changed +1174 -9
fixtures/flight/__tests__/__e2e__/smoke.test.js
+7
@@ -16,6 +16,13 @@ test('smoke test', async ({page}) => {
16 await expect(page.getByTestId('promise-as-a-child-test')).toHaveText(
17 'Promise as a child hydrates without errors: deferred text'
18 );
19 + await expect(page.getByTestId('prerendered')).not.toBeAttached();
20 +
21 + await expect(consoleErrors).toEqual([]);
22 + await expect(pageErrors).toEqual([]);
23 +
24 + await page.goto('/prerender');
25 + await expect(page.getByTestId('prerendered')).toBeAttached();
26
27 await expect(consoleErrors).toEqual([]);
28 await expect(pageErrors).toEqual([]);
fixtures/flight/server/global.js
+8 -3
@@ -86,7 +86,7 @@ function request(options, body) {
86 });
87 }
88
89 -app.all('/', async function (req, res, next) {
89 +async function renderApp(req, res, next) {
90 // Proxy the request to the regional server.
91 const proxiedHeaders = {
92 'X-Forwarded-Host': req.hostname,
@@ -102,12 +102,14 @@ app.all('/', async function (req, res, next) {
102 proxiedHeaders['Content-type'] = req.get('Content-type');
103 }
104
105 + const requestsPrerender = req.path === '/prerender';
106 +
107 const promiseForData = request(
108 {
109 host: '127.0.0.1',
110 port: 3001,
111 method: req.method,
110 - path: '/',
112 + path: requestsPrerender ? '/?prerender=1' : '/',
113 headers: proxiedHeaders,
114 },
115 req
@@ -210,7 +212,10 @@ app.all('/', async function (req, res, next) {
212 res.end();
213 }
214 }
213 -});
215 +}
216 +
217 +app.all('/', renderApp);
218 +app.all('/prerender', renderApp);
219
220 if (process.env.NODE_ENV === 'development') {
221 app.use(express.static('public'));
fixtures/flight/server/region.js
+60 -1
@@ -105,8 +105,67 @@ async function renderApp(res, returnValue, formState) {
105 pipe(res);
106 }
107
108 +async function prerenderApp(res, returnValue, formState) {
109 + const {prerenderToNodeStream} = await import(
110 + 'react-server-dom-webpack/static'
111 + );
112 + // const m = require('../src/App.js');
113 + const m = await import('../src/App.js');
114 +
115 + let moduleMap;
116 + let mainCSSChunks;
117 + if (process.env.NODE_ENV === 'development') {
118 + // Read the module map from the HMR server in development.
119 + moduleMap = await (
120 + await fetch('http://localhost:3000/react-client-manifest.json')
121 + ).json();
122 + mainCSSChunks = (
123 + await (
124 + await fetch('http://localhost:3000/entrypoint-manifest.json')
125 + ).json()
126 + ).main.css;
127 + } else {
128 + // Read the module map from the static build in production.
129 + moduleMap = JSON.parse(
130 + await readFile(
131 + path.resolve(__dirname, `../build/react-client-manifest.json`),
132 + 'utf8'
133 + )
134 + );
135 + mainCSSChunks = JSON.parse(
136 + await readFile(
137 + path.resolve(__dirname, `../build/entrypoint-manifest.json`),
138 + 'utf8'
139 + )
140 + ).main.css;
141 + }
142 + const App = m.default.default || m.default;
143 + const root = React.createElement(
144 + React.Fragment,
145 + null,
146 + // Prepend the App's tree with stylesheets required for this entrypoint.
147 + mainCSSChunks.map(filename =>
148 + React.createElement('link', {
149 + rel: 'stylesheet',
150 + href: filename,
151 + precedence: 'default',
152 + key: filename,
153 + })
154 + ),
155 + React.createElement(App, {prerender: true})
156 + );
157 + // For client-invoked server actions we refresh the tree and return a return value.
158 + const payload = {root, returnValue, formState};
159 + const {prelude} = await prerenderToNodeStream(payload, moduleMap);
160 + prelude.pipe(res);
161 +}
162 +
163 app.get('/', async function (req, res) {
109 - await renderApp(res, null, null);
164 + if ('prerender' in req.query) {
165 + await prerenderApp(res, null, null);
166 + } else {
167 + await renderApp(res, null, null);
168 + }
169 });
170
171 app.post('/', bodyParser.text(), async function (req, res) {
fixtures/flight/src/App.js
+6 -1
@@ -23,7 +23,7 @@ const promisedText = new Promise(resolve =>
23 setTimeout(() => resolve('deferred text'), 100)
24 );
25
26 -export default async function App() {
26 +export default async function App({prerender}) {
27 const res = await fetch('http://localhost:3001/todos');
28 const todos = await res.json();
29 return (
@@ -35,6 +35,11 @@ export default async function App() {
35 </head>
36 <body>
37 <Container>
38 + {prerender ? (
39 + <meta data-testid="prerendered" name="prerendered" content="true" />
40 + ) : (
41 + <meta content="when not prerendering we render this meta tag. When prerendering you will expect to see this tag and the one with data-testid=prerendered because we SSR one and hydrate the other" />
42 + )}
43 <h1>{getServerState()}</h1>
44 <React.Suspense fallback={null}>
45 <div data-testid="promise-as-a-child-test">
packages/react-server-dom-esm/npm/static.js new
+6
@@ -0,0 +1,6 @@
1 +'use strict';
2 +
3 +throw new Error(
4 + 'The React Server Writer cannot be used outside a react-server environment. ' +
5 + 'You must configure Node.js using the `--conditions react-server` flag.'
6 +);
packages/react-server-dom-esm/npm/static.node.js new
+12
@@ -0,0 +1,12 @@
1 +'use strict';
2 +
3 +var s;
4 +if (process.env.NODE_ENV === 'production') {
5 + s = require('./cjs/react-server-dom-esm-server.node.production.js');
6 +} else {
7 + s = require('./cjs/react-server-dom-esm-server.node.development.js');
8 +}
9 +
10 +if (s.prerenderToNodeStream) {
11 + exports.prerenderToNodeStream = s.prerenderToNodeStream;
12 +}
packages/react-server-dom-esm/package.json
+7
@@ -17,6 +17,8 @@
17 "client.node.js",
18 "server.js",
19 "server.node.js",
20 + "static.js",
21 + "static.node.js",
22 "cjs/",
23 "esm/"
24 ],
@@ -33,6 +35,11 @@
35 "default": "./server.js"
36 },
37 "./server.node": "./server.node.js",
38 + "./static": {
39 + "react-server": "./static.node.js",
40 + "default": "./static.js"
41 + },
42 + "./static.node": "./static.node.js",
43 "./node-loader": "./esm/react-server-dom-esm-node-loader.production.js",
44 "./src/*": "./src/*.js",
45 "./package.json": "./package.json"
packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js
+77
@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
18 import type {Writable} from 'stream';
19 import type {Thenable} from 'shared/ReactTypes';
20
21 +import {Readable} from 'stream';
22 +
23 import {
24 createRequest,
25 startWork,
@@ -123,6 +125,80 @@ function renderToPipeableStream(
125 },
126 };
127 }
128 +function createFakeWritable(readable: any): Writable {
129 + // The current host config expects a Writable so we create
130 + // a fake writable for now to push into the Readable.
131 + return ({
132 + write(chunk) {
133 + return readable.push(chunk);
134 + },
135 + end() {
136 + readable.push(null);
137 + },
138 + destroy(error) {
139 + readable.destroy(error);
140 + },
141 + }: any);
142 +}
143 +
144 +type PrerenderOptions = {
145 + environmentName?: string | (() => string),
146 + filterStackFrame?: (url: string, functionName: string) => boolean,
147 + onError?: (error: mixed) => void,
148 + onPostpone?: (reason: string) => void,
149 + identifierPrefix?: string,
150 + temporaryReferences?: TemporaryReferenceSet,
151 + signal?: AbortSignal,
152 +};
153 +
154 +type StaticResult = {
155 + prelude: Readable,
156 +};
157 +
158 +function prerenderToNodeStream(
159 + model: ReactClientValue,
160 + moduleBasePath: ClientManifest,
161 + options?: PrerenderOptions,
162 +): Promise<StaticResult> {
163 + return new Promise((resolve, reject) => {
164 + const onFatalError = reject;
165 + function onAllReady() {
166 + const readable: Readable = new Readable({
167 + read() {
168 + startFlowing(request, writable);
169 + },
170 + });
171 + const writable = createFakeWritable(readable);
172 + resolve({prelude: readable});
173 + }
174 +
175 + const request = createRequest(
176 + model,
177 + moduleBasePath,
178 + options ? options.onError : undefined,
179 + options ? options.identifierPrefix : undefined,
180 + options ? options.onPostpone : undefined,
181 + options ? options.temporaryReferences : undefined,
182 + __DEV__ && options ? options.environmentName : undefined,
183 + __DEV__ && options ? options.filterStackFrame : undefined,
184 + onAllReady,
185 + onFatalError,
186 + );
187 + if (options && options.signal) {
188 + const signal = options.signal;
189 + if (signal.aborted) {
190 + abort(request, (signal: any).reason);
191 + } else {
192 + const listener = () => {
193 + abort(request, (signal: any).reason);
194 + signal.removeEventListener('abort', listener);
195 + };
196 + signal.addEventListener('abort', listener);
197 + }
198 + }
199 + startWork(request);
200 + });
201 +}
202
203 function decodeReplyFromBusboy<T>(
204 busboyStream: Busboy,
@@ -207,6 +283,7 @@ function decodeReply<T>(
283
284 export {
285 renderToPipeableStream,
286 + prerenderToNodeStream,
287 decodeReplyFromBusboy,
288 decodeReply,
289 decodeAction,
packages/react-server-dom-esm/src/server/react-flight-dom-server.node.js
+1
@@ -9,6 +9,7 @@
9
10 export {
11 renderToPipeableStream,
12 + prerenderToNodeStream,
13 decodeReplyFromBusboy,
14 decodeReply,
15 decodeAction,
packages/react-server-dom-esm/src/server/react-flight-dom-server.node.stable.js new
+19
@@ -0,0 +1,19 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {
11 + renderToPipeableStream,
12 + decodeReplyFromBusboy,
13 + decodeReply,
14 + decodeAction,
15 + decodeFormState,
16 + registerServerReference,
17 + registerClientReference,
18 + createTemporaryReferenceSet,
19 +} from './ReactFlightDOMServerNode';
packages/react-server-dom-esm/static.js new
+13
@@ -0,0 +1,13 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +throw new Error(
11 + 'The React Server cannot be used outside a react-server environment. ' +
12 + 'You must configure Node.js using the `--conditions react-server` flag.',
13 +);
packages/react-server-dom-esm/static.node.js new
+10
@@ -0,0 +1,10 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {prerenderToNodeStream} from './src/server/react-flight-dom-server.node';
packages/react-server-dom-turbopack/npm/static.browser.js new
+12
@@ -0,0 +1,12 @@
1 +'use strict';
2 +
3 +var s;
4 +if (process.env.NODE_ENV === 'production') {
5 + s = require('./cjs/react-server-dom-turbopack-server.browser.production.js');
6 +} else {
7 + s = require('./cjs/react-server-dom-turbopack-server.browser.development.js');
8 +}
9 +
10 +if (s.prerender) {
11 + exports.prerender = s.prerender;
12 +}
packages/react-server-dom-turbopack/npm/static.edge.js new
+12
@@ -0,0 +1,12 @@
1 +'use strict';
2 +
3 +var s;
4 +if (process.env.NODE_ENV === 'production') {
5 + s = require('./cjs/react-server-dom-turbopack-server.edge.production.js');
6 +} else {
7 + s = require('./cjs/react-server-dom-turbopack-server.edge.development.js');
8 +}
9 +
10 +if (s.prerender) {
11 + exports.prerender = s.prerender;
12 +}
packages/react-server-dom-turbopack/npm/static.js new
+6
@@ -0,0 +1,6 @@
1 +'use strict';
2 +
3 +throw new Error(
4 + 'The React Server Writer cannot be used outside a react-server environment. ' +
5 + 'You must configure Node.js using the `--conditions react-server` flag.'
6 +);
packages/react-server-dom-turbopack/npm/static.node.js new
+12
@@ -0,0 +1,12 @@
1 +'use strict';
2 +
3 +var s;
4 +if (process.env.NODE_ENV === 'production') {
5 + s = require('./cjs/react-server-dom-turbopack-server.node.production.js');
6 +} else {
7 + s = require('./cjs/react-server-dom-turbopack-server.node.development.js');
8 +}
9 +
10 +if (s.prerenderToNodeStream) {
11 + exports.prerenderToNodeStream = s.prerenderToNodeStream;
12 +}
packages/react-server-dom-turbopack/npm/static.node.unbundled.js new
+12
@@ -0,0 +1,12 @@
1 +'use strict';
2 +
3 +var s;
4 +if (process.env.NODE_ENV === 'production') {
5 + s = require('./cjs/react-server-dom-turbopack-server.node.unbundled.production.js');
6 +} else {
7 + s = require('./cjs/react-server-dom-turbopack-server.node.unbundled.development.js');
8 +}
9 +
10 +if (s.prerenderToNodeStream) {
11 + exports.prerenderToNodeStream = s.prerenderToNodeStream;
12 +}
packages/react-server-dom-turbopack/package.json
+23
@@ -22,6 +22,11 @@
22 "server.edge.js",
23 "server.node.js",
24 "server.node.unbundled.js",
25 + "static.js",
26 + "static.browser.js",
27 + "static.edge.js",
28 + "static.node.js",
29 + "static.node.unbundled.js",
30 "node-register.js",
31 "cjs/",
32 "esm/"
@@ -63,6 +68,24 @@
68 "./server.edge": "./server.edge.js",
69 "./server.node": "./server.node.js",
70 "./server.node.unbundled": "./server.node.unbundled.js",
71 + "./static": {
72 + "react-server": {
73 + "workerd": "./static.edge.js",
74 + "deno": "./static.browser.js",
75 + "node": {
76 + "turbopack": "./static.node.js",
77 + "webpack": "./static.node.js",
78 + "default": "./static.node.unbundled.js"
79 + },
80 + "edge-light": "./static.edge.js",
81 + "browser": "./static.browser.js"
82 + },
83 + "default": "./static.js"
84 + },
85 + "./static.browser": "./static.browser.js",
86 + "./static.edge": "./static.edge.js",
87 + "./static.node": "./static.node.js",
88 + "./static.node.unbundled": "./static.node.unbundled.js",
89 "./node-loader": "./esm/react-server-dom-turbopack-node-loader.production.js",
90 "./node-register": "./node-register.js",
91 "./src/*": "./src/*.js",
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js
+66 -1
@@ -100,6 +100,65 @@ function renderToReadableStream(
100 return stream;
101 }
102
103 +type StaticResult = {
104 + prelude: ReadableStream,
105 +};
106 +
107 +function prerender(
108 + model: ReactClientValue,
109 + turbopackMap: ClientManifest,
110 + options?: Options,
111 +): Promise<StaticResult> {
112 + return new Promise((resolve, reject) => {
113 + const onFatalError = reject;
114 + function onAllReady() {
115 + const stream = new ReadableStream(
116 + {
117 + type: 'bytes',
118 + start: (controller): ?Promise<void> => {
119 + startWork(request);
120 + },
121 + pull: (controller): ?Promise<void> => {
122 + startFlowing(request, controller);
123 + },
124 + cancel: (reason): ?Promise<void> => {
125 + stopFlowing(request);
126 + abort(request, reason);
127 + },
128 + },
129 + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
130 + {highWaterMark: 0},
131 + );
132 + resolve({prelude: stream});
133 + }
134 + const request = createRequest(
135 + model,
136 + turbopackMap,
137 + options ? options.onError : undefined,
138 + options ? options.identifierPrefix : undefined,
139 + options ? options.onPostpone : undefined,
140 + options ? options.temporaryReferences : undefined,
141 + __DEV__ && options ? options.environmentName : undefined,
142 + __DEV__ && options ? options.filterStackFrame : undefined,
143 + onAllReady,
144 + onFatalError,
145 + );
146 + if (options && options.signal) {
147 + const signal = options.signal;
148 + if (signal.aborted) {
149 + abort(request, (signal: any).reason);
150 + } else {
151 + const listener = () => {
152 + abort(request, (signal: any).reason);
153 + signal.removeEventListener('abort', listener);
154 + };
155 + signal.addEventListener('abort', listener);
156 + }
157 + }
158 + startWork(request);
159 + });
160 +}
161 +
162 function decodeReply<T>(
163 body: string | FormData,
164 turbopackMap: ServerManifest,
@@ -121,4 +180,10 @@ function decodeReply<T>(
180 return root;
181 }
182
124 -export {renderToReadableStream, decodeReply, decodeAction, decodeFormState};
183 +export {
184 + renderToReadableStream,
185 + prerender,
186 + decodeReply,
187 + decodeAction,
188 + decodeFormState,
189 +};
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js
+66 -1
@@ -100,6 +100,65 @@ function renderToReadableStream(
100 return stream;
101 }
102
103 +type StaticResult = {
104 + prelude: ReadableStream,
105 +};
106 +
107 +function prerender(
108 + model: ReactClientValue,
109 + turbopackMap: ClientManifest,
110 + options?: Options,
111 +): Promise<StaticResult> {
112 + return new Promise((resolve, reject) => {
113 + const onFatalError = reject;
114 + function onAllReady() {
115 + const stream = new ReadableStream(
116 + {
117 + type: 'bytes',
118 + start: (controller): ?Promise<void> => {
119 + startWork(request);
120 + },
121 + pull: (controller): ?Promise<void> => {
122 + startFlowing(request, controller);
123 + },
124 + cancel: (reason): ?Promise<void> => {
125 + stopFlowing(request);
126 + abort(request, reason);
127 + },
128 + },
129 + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
130 + {highWaterMark: 0},
131 + );
132 + resolve({prelude: stream});
133 + }
134 + const request = createRequest(
135 + model,
136 + turbopackMap,
137 + options ? options.onError : undefined,
138 + options ? options.identifierPrefix : undefined,
139 + options ? options.onPostpone : undefined,
140 + options ? options.temporaryReferences : undefined,
141 + __DEV__ && options ? options.environmentName : undefined,
142 + __DEV__ && options ? options.filterStackFrame : undefined,
143 + onAllReady,
144 + onFatalError,
145 + );
146 + if (options && options.signal) {
147 + const signal = options.signal;
148 + if (signal.aborted) {
149 + abort(request, (signal: any).reason);
150 + } else {
151 + const listener = () => {
152 + abort(request, (signal: any).reason);
153 + signal.removeEventListener('abort', listener);
154 + };
155 + signal.addEventListener('abort', listener);
156 + }
157 + }
158 + startWork(request);
159 + });
160 +}
161 +
162 function decodeReply<T>(
163 body: string | FormData,
164 turbopackMap: ServerManifest,
@@ -121,4 +180,10 @@ function decodeReply<T>(
180 return root;
181 }
182
124 -export {renderToReadableStream, decodeReply, decodeAction, decodeFormState};
183 +export {
184 + renderToReadableStream,
185 + prerender,
186 + decodeReply,
187 + decodeAction,
188 + decodeFormState,
189 +};
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js
+78
@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
18 import type {Writable} from 'stream';
19 import type {Thenable} from 'shared/ReactTypes';
20
21 +import {Readable} from 'stream';
22 +
23 import {
24 createRequest,
25 startWork,
@@ -125,6 +127,81 @@ function renderToPipeableStream(
127 };
128 }
129
130 +function createFakeWritable(readable: any): Writable {
131 + // The current host config expects a Writable so we create
132 + // a fake writable for now to push into the Readable.
133 + return ({
134 + write(chunk) {
135 + return readable.push(chunk);
136 + },
137 + end() {
138 + readable.push(null);
139 + },
140 + destroy(error) {
141 + readable.destroy(error);
142 + },
143 + }: any);
144 +}
145 +
146 +type PrerenderOptions = {
147 + environmentName?: string | (() => string),
148 + filterStackFrame?: (url: string, functionName: string) => boolean,
149 + onError?: (error: mixed) => void,
150 + onPostpone?: (reason: string) => void,
151 + identifierPrefix?: string,
152 + temporaryReferences?: TemporaryReferenceSet,
153 + signal?: AbortSignal,
154 +};
155 +
156 +type StaticResult = {
157 + prelude: Readable,
158 +};
159 +
160 +function prerenderToNodeStream(
161 + model: ReactClientValue,
162 + turbopackMap: ClientManifest,
163 + options?: PrerenderOptions,
164 +): Promise<StaticResult> {
165 + return new Promise((resolve, reject) => {
166 + const onFatalError = reject;
167 + function onAllReady() {
168 + const readable: Readable = new Readable({
169 + read() {
170 + startFlowing(request, writable);
171 + },
172 + });
173 + const writable = createFakeWritable(readable);
174 + resolve({prelude: readable});
175 + }
176 +
177 + const request = createRequest(
178 + model,
179 + turbopackMap,
180 + options ? options.onError : undefined,
181 + options ? options.identifierPrefix : undefined,
182 + options ? options.onPostpone : undefined,
183 + options ? options.temporaryReferences : undefined,
184 + __DEV__ && options ? options.environmentName : undefined,
185 + __DEV__ && options ? options.filterStackFrame : undefined,
186 + onAllReady,
187 + onFatalError,
188 + );
189 + if (options && options.signal) {
190 + const signal = options.signal;
191 + if (signal.aborted) {
192 + abort(request, (signal: any).reason);
193 + } else {
194 + const listener = () => {
195 + abort(request, (signal: any).reason);
196 + signal.removeEventListener('abort', listener);
197 + };
198 + signal.addEventListener('abort', listener);
199 + }
200 + }
201 + startWork(request);
202 + });
203 +}
204 +
205 function decodeReplyFromBusboy<T>(
206 busboyStream: Busboy,
207 turbopackMap: ServerManifest,
@@ -208,6 +285,7 @@ function decodeReply<T>(
285
286 export {
287 renderToPipeableStream,
288 + prerenderToNodeStream,
289 decodeReplyFromBusboy,
290 decodeReply,
291 decodeAction,
packages/react-server-dom-turbopack/src/server/react-flight-dom-server.browser.js
+1
@@ -9,6 +9,7 @@
9
10 export {
11 renderToReadableStream,
12 + prerender,
13 decodeReply,
14 decodeAction,
15 decodeFormState,
packages/react-server-dom-turbopack/src/server/react-flight-dom-server.browser.stable.js new
+19
@@ -0,0 +1,19 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {
11 + renderToReadableStream,
12 + decodeReply,
13 + decodeAction,
14 + decodeFormState,
15 + registerServerReference,
16 + registerClientReference,
17 + createClientModuleProxy,
18 + createTemporaryReferenceSet,
19 +} from './ReactFlightDOMServerBrowser';
packages/react-server-dom-turbopack/src/server/react-flight-dom-server.edge.js
+1
@@ -9,6 +9,7 @@
9
10 export {
11 renderToReadableStream,
12 + prerender,
13 decodeReply,
14 decodeAction,
15 decodeFormState,
packages/react-server-dom-turbopack/src/server/react-flight-dom-server.edge.stable.js new
+19
@@ -0,0 +1,19 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {
11 + renderToReadableStream,
12 + decodeReply,
13 + decodeAction,
14 + decodeFormState,
15 + registerServerReference,
16 + registerClientReference,
17 + createClientModuleProxy,
18 + createTemporaryReferenceSet,
19 +} from './ReactFlightDOMServerEdge';
packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.js
+1
@@ -9,6 +9,7 @@
9
10 export {
11 renderToPipeableStream,
12 + prerenderToNodeStream,
13 decodeReplyFromBusboy,
14 decodeReply,
15 decodeAction,
packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.stable.js new
+20
@@ -0,0 +1,20 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {
11 + renderToPipeableStream,
12 + decodeReplyFromBusboy,
13 + decodeReply,
14 + decodeAction,
15 + decodeFormState,
16 + registerServerReference,
17 + registerClientReference,
18 + createClientModuleProxy,
19 + createTemporaryReferenceSet,
20 +} from './ReactFlightDOMServerNode';
packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled.js
+1
@@ -9,6 +9,7 @@
9
10 export {
11 renderToPipeableStream,
12 + prerenderToNodeStream,
13 decodeReplyFromBusboy,
14 decodeReply,
15 decodeAction,
packages/react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled.stable.js new
+20
@@ -0,0 +1,20 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {
11 + renderToPipeableStream,
12 + decodeReplyFromBusboy,
13 + decodeReply,
14 + decodeAction,
15 + decodeFormState,
16 + registerServerReference,
17 + registerClientReference,
18 + createClientModuleProxy,
19 + createTemporaryReferenceSet,
20 +} from './ReactFlightDOMServerNode';
packages/react-server-dom-turbopack/static.browser.js new
+10
@@ -0,0 +1,10 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {prerender} from './src/server/react-flight-dom-server.browser';
packages/react-server-dom-turbopack/static.edge.js new
+10
@@ -0,0 +1,10 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {prerender} from './src/server/react-flight-dom-server.edge';
packages/react-server-dom-turbopack/static.js new
+13
@@ -0,0 +1,13 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +throw new Error(
11 + 'The React Server cannot be used outside a react-server environment. ' +
12 + 'You must configure Node.js using the `--conditions react-server` flag.',
13 +);
packages/react-server-dom-turbopack/static.node.js new
+10
@@ -0,0 +1,10 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {prerenderToNodeStream} from './src/server/react-flight-dom-server.node';
packages/react-server-dom-turbopack/static.node.unbundled.js new
+10
@@ -0,0 +1,10 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {prerenderToNodeStream} from './src/server/react-flight-dom-server.node.unbundled';
packages/react-server-dom-webpack/npm/static.browser.js new
+12
@@ -0,0 +1,12 @@
1 +'use strict';
2 +
3 +var s;
4 +if (process.env.NODE_ENV === 'production') {
5 + s = require('./cjs/react-server-dom-webpack-server.browser.production.js');
6 +} else {
7 + s = require('./cjs/react-server-dom-webpack-server.browser.development.js');
8 +}
9 +
10 +if (s.prerender) {
11 + exports.prerender = s.prerender;
12 +}
packages/react-server-dom-webpack/npm/static.edge.js new
+12
@@ -0,0 +1,12 @@
1 +'use strict';
2 +
3 +var s;
4 +if (process.env.NODE_ENV === 'production') {
5 + s = require('./cjs/react-server-dom-webpack-server.edge.production.js');
6 +} else {
7 + s = require('./cjs/react-server-dom-webpack-server.edge.development.js');
8 +}
9 +
10 +if (s.prerender) {
11 + exports.prerender = s.prerender;
12 +}
packages/react-server-dom-webpack/npm/static.js new
+6
@@ -0,0 +1,6 @@
1 +'use strict';
2 +
3 +throw new Error(
4 + 'The React Server Writer cannot be used outside a react-server environment. ' +
5 + 'You must configure Node.js using the `--conditions react-server` flag.'
6 +);
packages/react-server-dom-webpack/npm/static.node.js new
+12
@@ -0,0 +1,12 @@
1 +'use strict';
2 +
3 +var s;
4 +if (process.env.NODE_ENV === 'production') {
5 + s = require('./cjs/react-server-dom-webpack-server.node.production.js');
6 +} else {
7 + s = require('./cjs/react-server-dom-webpack-server.node.development.js');
8 +}
9 +
10 +if (s.prerenderToNodeStream) {
11 + exports.prerenderToNodeStream = s.prerenderToNodeStream;
12 +}
packages/react-server-dom-webpack/npm/static.node.unbundled.js new
+12
@@ -0,0 +1,12 @@
1 +'use strict';
2 +
3 +var s;
4 +if (process.env.NODE_ENV === 'production') {
5 + s = require('./cjs/react-server-dom-webpack-server.node.unbundled.production.js');
6 +} else {
7 + s = require('./cjs/react-server-dom-webpack-server.node.unbundled.development.js');
8 +}
9 +
10 +if (s.prerenderToNodeStream) {
11 + exports.prerenderToNodeStream = s.prerenderToNodeStream;
12 +}
packages/react-server-dom-webpack/package.json
+22
@@ -23,6 +23,11 @@
23 "server.edge.js",
24 "server.node.js",
25 "server.node.unbundled.js",
26 + "static.js",
27 + "static.browser.js",
28 + "static.edge.js",
29 + "static.node.js",
30 + "static.node.unbundled.js",
31 "node-register.js",
32 "cjs/",
33 "esm/"
@@ -63,6 +68,23 @@
68 "./server.edge": "./server.edge.js",
69 "./server.node": "./server.node.js",
70 "./server.node.unbundled": "./server.node.unbundled.js",
71 + "./static": {
72 + "react-server": {
73 + "workerd": "./static.edge.js",
74 + "deno": "./static.browser.js",
75 + "node": {
76 + "webpack": "./static.node.js",
77 + "default": "./static.node.unbundled.js"
78 + },
79 + "edge-light": "./static.edge.js",
80 + "browser": "./static.browser.js"
81 + },
82 + "default": "./static.js"
83 + },
84 + "./static.browser": "./static.browser.js",
85 + "./static.edge": "./static.edge.js",
86 + "./static.node": "./static.node.js",
87 + "./static.node.unbundled": "./static.node.unbundled.js",
88 "./node-loader": "./esm/react-server-dom-webpack-node-loader.production.js",
89 "./node-register": "./node-register.js",
90 "./src/*": "./src/*.js",
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+72
@@ -10,6 +10,7 @@
10 'use strict';
11
12 import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
13 +import {Readable} from 'stream';
14
15 // Polyfills for test environment
16 global.ReadableStream =
@@ -28,6 +29,7 @@ let React;
29 let FlightReactDOM;
30 let ReactDOMClient;
31 let ReactServerDOMServer;
32 +let ReactServerDOMStaticServer;
33 let ReactServerDOMClient;
34 let ReactDOMFizzServer;
35 let ReactDOMStaticServer;
@@ -59,12 +61,20 @@ describe('ReactFlightDOM', () => {
61 jest.mock('react-server-dom-webpack/server', () =>
62 require('react-server-dom-webpack/server.node.unbundled'),
63 );
64 + if (__EXPERIMENTAL__) {
65 + jest.mock('react-server-dom-webpack/static', () =>
66 + require('react-server-dom-webpack/static.node.unbundled'),
67 + );
68 + }
69 const WebpackMock = require('./utils/WebpackMock');
70 clientExports = WebpackMock.clientExports;
71 clientModuleError = WebpackMock.clientModuleError;
72 webpackMap = WebpackMock.webpackMap;
73
74 ReactServerDOMServer = require('react-server-dom-webpack/server');
75 + if (__EXPERIMENTAL__) {
76 + ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
77 + }
78
79 // This reset is to load modules for the SSR/Browser scope.
80 jest.unmock('react-server-dom-webpack/server');
@@ -2650,4 +2660,66 @@ describe('ReactFlightDOM', () => {
2660 </div>,
2661 );
2662 });
2663 +
2664 + // @gate experimental
2665 + it('can prerender', async () => {
2666 + let resolveGreeting;
2667 + const greetingPromise = new Promise(resolve => {
2668 + resolveGreeting = resolve;
2669 + });
2670 +
2671 + function App() {
2672 + return (
2673 + <div>
2674 + <Greeting />
2675 + </div>
2676 + );
2677 + }
2678 +
2679 + async function Greeting() {
2680 + await greetingPromise;
2681 + return 'hello world';
2682 + }
2683 +
2684 + const {pendingResult} = await serverAct(async () => {
2685 + // destructure trick to avoid the act scope from awaiting the returned value
2686 + return {
2687 + pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
2688 + <App />,
2689 + webpackMap,
2690 + ),
2691 + };
2692 + });
2693 +
2694 + resolveGreeting();
2695 + const {prelude} = await pendingResult;
2696 +
2697 + const response = ReactServerDOMClient.createFromReadableStream(
2698 + Readable.toWeb(prelude),
2699 + );
2700 +
2701 + const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2702 +
2703 + function ClientApp() {
2704 + return use(response);
2705 + }
2706 +
2707 + const shellErrors = [];
2708 + await serverAct(async () => {
2709 + ReactDOMFizzServer.renderToPipeableStream(
2710 + React.createElement(ClientApp),
2711 + {
2712 + onShellError(error) {
2713 + shellErrors.push(error.message);
2714 + },
2715 + },
2716 + ).pipe(fizzWritable);
2717 + });
2718 +
2719 + expect(shellErrors).toEqual([]);
2720 +
2721 + const container = document.createElement('div');
2722 + await readInto(container, fizzReadable);
2723 + expect(getMeaningfulChildren(container)).toEqual(<div>hello world</div>);
2724 + });
2725 });
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js
+66 -1
@@ -100,6 +100,65 @@ function renderToReadableStream(
100 return stream;
101 }
102
103 +type StaticResult = {
104 + prelude: ReadableStream,
105 +};
106 +
107 +function prerender(
108 + model: ReactClientValue,
109 + webpackMap: ClientManifest,
110 + options?: Options,
111 +): Promise<StaticResult> {
112 + return new Promise((resolve, reject) => {
113 + const onFatalError = reject;
114 + function onAllReady() {
115 + const stream = new ReadableStream(
116 + {
117 + type: 'bytes',
118 + start: (controller): ?Promise<void> => {
119 + startWork(request);
120 + },
121 + pull: (controller): ?Promise<void> => {
122 + startFlowing(request, controller);
123 + },
124 + cancel: (reason): ?Promise<void> => {
125 + stopFlowing(request);
126 + abort(request, reason);
127 + },
128 + },
129 + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
130 + {highWaterMark: 0},
131 + );
132 + resolve({prelude: stream});
133 + }
134 + const request = createRequest(
135 + model,
136 + webpackMap,
137 + options ? options.onError : undefined,
138 + options ? options.identifierPrefix : undefined,
139 + options ? options.onPostpone : undefined,
140 + options ? options.temporaryReferences : undefined,
141 + __DEV__ && options ? options.environmentName : undefined,
142 + __DEV__ && options ? options.filterStackFrame : undefined,
143 + onAllReady,
144 + onFatalError,
145 + );
146 + if (options && options.signal) {
147 + const signal = options.signal;
148 + if (signal.aborted) {
149 + abort(request, (signal: any).reason);
150 + } else {
151 + const listener = () => {
152 + abort(request, (signal: any).reason);
153 + signal.removeEventListener('abort', listener);
154 + };
155 + signal.addEventListener('abort', listener);
156 + }
157 + }
158 + startWork(request);
159 + });
160 +}
161 +
162 function decodeReply<T>(
163 body: string | FormData,
164 webpackMap: ServerManifest,
@@ -121,4 +180,10 @@ function decodeReply<T>(
180 return root;
181 }
182
124 -export {renderToReadableStream, decodeReply, decodeAction, decodeFormState};
183 +export {
184 + renderToReadableStream,
185 + prerender,
186 + decodeReply,
187 + decodeAction,
188 + decodeFormState,
189 +};
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js
+66 -1
@@ -100,6 +100,65 @@ function renderToReadableStream(
100 return stream;
101 }
102
103 +type StaticResult = {
104 + prelude: ReadableStream,
105 +};
106 +
107 +function prerender(
108 + model: ReactClientValue,
109 + webpackMap: ClientManifest,
110 + options?: Options,
111 +): Promise<StaticResult> {
112 + return new Promise((resolve, reject) => {
113 + const onFatalError = reject;
114 + function onAllReady() {
115 + const stream = new ReadableStream(
116 + {
117 + type: 'bytes',
118 + start: (controller): ?Promise<void> => {
119 + startWork(request);
120 + },
121 + pull: (controller): ?Promise<void> => {
122 + startFlowing(request, controller);
123 + },
124 + cancel: (reason): ?Promise<void> => {
125 + stopFlowing(request);
126 + abort(request, reason);
127 + },
128 + },
129 + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
130 + {highWaterMark: 0},
131 + );
132 + resolve({prelude: stream});
133 + }
134 + const request = createRequest(
135 + model,
136 + webpackMap,
137 + options ? options.onError : undefined,
138 + options ? options.identifierPrefix : undefined,
139 + options ? options.onPostpone : undefined,
140 + options ? options.temporaryReferences : undefined,
141 + __DEV__ && options ? options.environmentName : undefined,
142 + __DEV__ && options ? options.filterStackFrame : undefined,
143 + onAllReady,
144 + onFatalError,
145 + );
146 + if (options && options.signal) {
147 + const signal = options.signal;
148 + if (signal.aborted) {
149 + abort(request, (signal: any).reason);
150 + } else {
151 + const listener = () => {
152 + abort(request, (signal: any).reason);
153 + signal.removeEventListener('abort', listener);
154 + };
155 + signal.addEventListener('abort', listener);
156 + }
157 + }
158 + startWork(request);
159 + });
160 +}
161 +
162 function decodeReply<T>(
163 body: string | FormData,
164 webpackMap: ServerManifest,
@@ -121,4 +180,10 @@ function decodeReply<T>(
180 return root;
181 }
182
124 -export {renderToReadableStream, decodeReply, decodeAction, decodeFormState};
183 +export {
184 + renderToReadableStream,
185 + prerender,
186 + decodeReply,
187 + decodeAction,
188 + decodeFormState,
189 +};
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
+78
@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
18 import type {Writable} from 'stream';
19 import type {Thenable} from 'shared/ReactTypes';
20
21 +import {Readable} from 'stream';
22 +
23 import {
24 createRequest,
25 startWork,
@@ -125,6 +127,81 @@ function renderToPipeableStream(
127 };
128 }
129
130 +function createFakeWritable(readable: any): Writable {
131 + // The current host config expects a Writable so we create
132 + // a fake writable for now to push into the Readable.
133 + return ({
134 + write(chunk) {
135 + return readable.push(chunk);
136 + },
137 + end() {
138 + readable.push(null);
139 + },
140 + destroy(error) {
141 + readable.destroy(error);
142 + },
143 + }: any);
144 +}
145 +
146 +type PrerenderOptions = {
147 + environmentName?: string | (() => string),
148 + filterStackFrame?: (url: string, functionName: string) => boolean,
149 + onError?: (error: mixed) => void,
150 + onPostpone?: (reason: string) => void,
151 + identifierPrefix?: string,
152 + temporaryReferences?: TemporaryReferenceSet,
153 + signal?: AbortSignal,
154 +};
155 +
156 +type StaticResult = {
157 + prelude: Readable,
158 +};
159 +
160 +function prerenderToNodeStream(
161 + model: ReactClientValue,
162 + webpackMap: ClientManifest,
163 + options?: PrerenderOptions,
164 +): Promise<StaticResult> {
165 + return new Promise((resolve, reject) => {
166 + const onFatalError = reject;
167 + function onAllReady() {
168 + const readable: Readable = new Readable({
169 + read() {
170 + startFlowing(request, writable);
171 + },
172 + });
173 + const writable = createFakeWritable(readable);
174 + resolve({prelude: readable});
175 + }
176 +
177 + const request = createRequest(
178 + model,
179 + webpackMap,
180 + options ? options.onError : undefined,
181 + options ? options.identifierPrefix : undefined,
182 + options ? options.onPostpone : undefined,
183 + options ? options.temporaryReferences : undefined,
184 + __DEV__ && options ? options.environmentName : undefined,
185 + __DEV__ && options ? options.filterStackFrame : undefined,
186 + onAllReady,
187 + onFatalError,
188 + );
189 + if (options && options.signal) {
190 + const signal = options.signal;
191 + if (signal.aborted) {
192 + abort(request, (signal: any).reason);
193 + } else {
194 + const listener = () => {
195 + abort(request, (signal: any).reason);
196 + signal.removeEventListener('abort', listener);
197 + };
198 + signal.addEventListener('abort', listener);
199 + }
200 + }
201 + startWork(request);
202 + });
203 +}
204 +
205 function decodeReplyFromBusboy<T>(
206 busboyStream: Busboy,
207 webpackMap: ServerManifest,
@@ -208,6 +285,7 @@ function decodeReply<T>(
285
286 export {
287 renderToPipeableStream,
288 + prerenderToNodeStream,
289 decodeReplyFromBusboy,
290 decodeReply,
291 decodeAction,
packages/react-server-dom-webpack/src/server/react-flight-dom-server.browser.js
+1
@@ -9,6 +9,7 @@
9
10 export {
11 renderToReadableStream,
12 + prerender,
13 decodeReply,
14 decodeAction,
15 decodeFormState,
packages/react-server-dom-webpack/src/server/react-flight-dom-server.browser.stable.js new
+19
@@ -0,0 +1,19 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {
11 + renderToReadableStream,
12 + decodeReply,
13 + decodeAction,
14 + decodeFormState,
15 + registerServerReference,
16 + registerClientReference,
17 + createClientModuleProxy,
18 + createTemporaryReferenceSet,
19 +} from './ReactFlightDOMServerBrowser';
packages/react-server-dom-webpack/src/server/react-flight-dom-server.edge.js
+1
@@ -9,6 +9,7 @@
9
10 export {
11 renderToReadableStream,
12 + prerender,
13 decodeReply,
14 decodeAction,
15 decodeFormState,
packages/react-server-dom-webpack/src/server/react-flight-dom-server.edge.stable.js new
+19
@@ -0,0 +1,19 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {
11 + renderToReadableStream,
12 + decodeReply,
13 + decodeAction,
14 + decodeFormState,
15 + registerServerReference,
16 + registerClientReference,
17 + createClientModuleProxy,
18 + createTemporaryReferenceSet,
19 +} from './ReactFlightDOMServerEdge';
packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.js
+1
@@ -9,6 +9,7 @@
9
10 export {
11 renderToPipeableStream,
12 + prerenderToNodeStream,
13 decodeReplyFromBusboy,
14 decodeReply,
15 decodeAction,
packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.stable.js new
+20
@@ -0,0 +1,20 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {
11 + renderToPipeableStream,
12 + decodeReplyFromBusboy,
13 + decodeReply,
14 + decodeAction,
15 + decodeFormState,
16 + registerServerReference,
17 + registerClientReference,
18 + createClientModuleProxy,
19 + createTemporaryReferenceSet,
20 +} from './ReactFlightDOMServerNode';
packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled.js
+1
@@ -9,6 +9,7 @@
9
10 export {
11 renderToPipeableStream,
12 + prerenderToNodeStream,
13 decodeReplyFromBusboy,
14 decodeReply,
15 decodeAction,
packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled.stable.js new
+20
@@ -0,0 +1,20 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {
11 + renderToPipeableStream,
12 + decodeReplyFromBusboy,
13 + decodeReply,
14 + decodeAction,
15 + decodeFormState,
16 + registerServerReference,
17 + registerClientReference,
18 + createClientModuleProxy,
19 + createTemporaryReferenceSet,
20 +} from './ReactFlightDOMServerNode';
packages/react-server-dom-webpack/static.browser.js new
+10
@@ -0,0 +1,10 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {prerender} from './src/server/react-flight-dom-server.browser';
packages/react-server-dom-webpack/static.edge.js new
+10
@@ -0,0 +1,10 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {prerender} from './src/server/react-flight-dom-server.edge';
packages/react-server-dom-webpack/static.js new
+13
@@ -0,0 +1,13 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +throw new Error(
11 + 'The React Server cannot be used outside a react-server environment. ' +
12 + 'You must configure Node.js using the `--conditions react-server` flag.',
13 +);
packages/react-server-dom-webpack/static.node.js new
+10
@@ -0,0 +1,10 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {prerenderToNodeStream} from './src/server/react-flight-dom-server.node';
packages/react-server-dom-webpack/static.node.unbundled.js new
+10
@@ -0,0 +1,10 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export {prerenderToNodeStream} from './src/server/react-flight-dom-server.node.unbundled';
packages/react-server/src/ReactFlightServer.js
+19
@@ -376,6 +376,8 @@ export type Request = {
376 taintCleanupQueue: Array<string | bigint>,
377 onError: (error: mixed) => ?string,
378 onPostpone: (reason: string) => void,
379 + onAllReady: () => void,
380 + onFatalError: mixed => void,
381 // DEV-only
382 environmentName: () => string,
383 filterStackFrame: (url: string, functionName: string) => boolean,
@@ -435,6 +437,8 @@ function RequestInstance(
437 temporaryReferences: void | TemporaryReferenceSet,
438 environmentName: void | string | (() => string), // DEV-only
439 filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
440 + onAllReady: void | (() => void),
441 + onFatalError: void | ((error: mixed) => void),
442 ) {
443 if (
444 ReactSharedInternals.A !== null &&
@@ -486,6 +490,8 @@ function RequestInstance(
490 this.onError = onError === undefined ? defaultErrorHandler : onError;
491 this.onPostpone =
492 onPostpone === undefined ? defaultPostponeHandler : onPostpone;
493 + this.onAllReady = onAllReady === undefined ? noop : onAllReady;
494 + this.onFatalError = onFatalError === undefined ? noop : onFatalError;
495
496 if (__DEV__) {
497 this.environmentName =
@@ -513,6 +519,8 @@ function RequestInstance(
519 pingedTasks.push(rootTask);
520 }
521
522 +function noop(): void {}
523 +
524 export function createRequest(
525 model: ReactClientValue,
526 bundlerConfig: ClientManifest,
@@ -522,6 +530,8 @@ export function createRequest(
530 temporaryReferences: void | TemporaryReferenceSet,
531 environmentName: void | string | (() => string), // DEV-only
532 filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
533 + onAllReady: void | (() => void),
534 + onFatalError: void | (() => void),
535 ): Request {
536 // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
537 return new RequestInstance(
@@ -533,6 +543,8 @@ export function createRequest(
543 temporaryReferences,
544 environmentName,
545 filterStackFrame,
546 + onAllReady,
547 + onFatalError,
548 );
549 }
550
@@ -2890,6 +2902,8 @@ function logRecoverableError(
2902 }
2903
2904 function fatalError(request: Request, error: mixed): void {
2905 + const onFatalError = request.onFatalError;
2906 + onFatalError(error);
2907 if (enableTaint) {
2908 cleanupTaintQueue(request);
2909 }
@@ -3753,6 +3767,11 @@ function performWork(request: Request): void {
3767 if (request.destination !== null) {
3768 flushCompletedChunks(request, request.destination);
3769 }
3770 + if (request.abortableTasks.size === 0) {
3771 + // we're done rendering
3772 + const onAllReady = request.onAllReady;
3773 + onAllReady();
3774 + }
3775 } catch (error) {
3776 logRecoverableError(request, error, null);
3777 fatalError(request, error);
scripts/shared/inlinedHostConfigs.js
+14
@@ -43,6 +43,8 @@ module.exports = [
43 'react-server-dom-webpack/client.node.unbundled',
44 'react-server-dom-webpack/server',
45 'react-server-dom-webpack/server.node.unbundled',
46 + 'react-server-dom-webpack/static',
47 + 'react-server-dom-webpack/static.node.unbundled',
48 'react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node
49 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerNode.js',
50 'react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled',
@@ -82,6 +84,8 @@ module.exports = [
84 'react-server-dom-webpack/client.node',
85 'react-server-dom-webpack/server',
86 'react-server-dom-webpack/server.node',
87 + 'react-server-dom-webpack/static',
88 + 'react-server-dom-webpack/static.node',
89 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack.js',
90 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackServer.js',
91 'react-server-dom-webpack/src/server/react-flight-dom-server.node',
@@ -123,6 +127,8 @@ module.exports = [
127 'react-server-dom-turbopack/client.node.unbundled',
128 'react-server-dom-turbopack/server',
129 'react-server-dom-turbopack/server.node.unbundled',
130 + 'react-server-dom-turbopack/static',
131 + 'react-server-dom-turbopack/static.node.unbundled',
132 'react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js', // react-server-dom-turbopack/client.node.unbundled
133 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerNode.js',
134 'react-server-dom-turbopack/src/server/react-flight-dom-server.node.unbundled',
@@ -164,6 +170,8 @@ module.exports = [
170 'react-server-dom-turbopack/client.node',
171 'react-server-dom-turbopack/server',
172 'react-server-dom-turbopack/server.node',
173 + 'react-server-dom-turbopack/static',
174 + 'react-server-dom-turbopack/static.node',
175 'react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js', // react-server-dom-turbopack/client.node
176 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopack.js',
177 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopackServer.js',
@@ -238,6 +246,7 @@ module.exports = [
246 'react-server-dom-webpack/client',
247 'react-server-dom-webpack/client.browser',
248 'react-server-dom-webpack/server.browser',
249 + 'react-server-dom-webpack/static.browser',
250 'react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js', // react-server-dom-webpack/client.browser
251 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack.js',
252 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackBrowser.js',
@@ -299,6 +308,7 @@ module.exports = [
308 'react-server-dom-turbopack/client',
309 'react-server-dom-turbopack/client.browser',
310 'react-server-dom-turbopack/server.browser',
311 + 'react-server-dom-turbopack/static.browser',
312 'react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js', // react-server-dom-turbopack/client.browser
313 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopack.js',
314 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopackBrowser.js',
@@ -339,6 +349,7 @@ module.exports = [
349 'react-server-dom-webpack',
350 'react-server-dom-webpack/client.edge',
351 'react-server-dom-webpack/server.edge',
352 + 'react-server-dom-webpack/static.edge',
353 'react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js', // react-server-dom-webpack/client.edge
354 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack.js',
355 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackServer.js',
@@ -378,6 +389,7 @@ module.exports = [
389 'react-server-dom-turbopack',
390 'react-server-dom-turbopack/client.edge',
391 'react-server-dom-turbopack/server.edge',
392 + 'react-server-dom-turbopack/static.edge',
393 'react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js', // react-server-dom-turbopack/client.edge
394 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopack.js',
395 'react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopackServer.js',
@@ -419,6 +431,8 @@ module.exports = [
431 'react-server-dom-esm/client.node',
432 'react-server-dom-esm/server',
433 'react-server-dom-esm/server.node',
434 + 'react-server-dom-esm/static',
435 + 'react-server-dom-esm/static.node',
436 'react-server-dom-esm/src/client/ReactFlightDOMClientNode.js', // react-server-dom-esm/client.node
437 'react-server-dom-esm/src/server/react-flight-dom-server.node',
438 'react-server-dom-esm/src/server/ReactFlightDOMServerNode.js', // react-server-dom-esm/src/server/react-flight-dom-server.node