[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