Add Bun streaming server renderer (#25597)
Add support for Bun server renderer
Colin McDonnell committed
Nov 17, 2022 at 13:15 UTC
56ffca8b9e4e49ad46136fe705203afc2d20fd9f
16 files changed
+422
-1
packages/react-client/src/forks/ReactFlightClientHostConfig.bun.js
new
+12
@@ -0,0 +1,12 @@
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 * from 'react-client/src/ReactFlightClientHostConfigBrowser';
11
+export * from 'react-client/src/ReactFlightClientHostConfigStream';
12
+export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
packages/react-dom/npm/server.bun.js
new
+18
@@ -0,0 +1,18 @@
1
+'use strict';
2
+
3
+var b;
4
+var l;
5
+if (process.env.NODE_ENV === 'production') {
6
+ b = require('./cjs/react-dom-server.bun.production.min.js');
7
+ l = require('./cjs/react-dom-server-legacy.browser.production.min.js');
8
+} else {
9
+ b = require('./cjs/react-dom-server.bun.development.js');
10
+ l = require('./cjs/react-dom-server-legacy.browser.development.js');
11
+}
12
+
13
+exports.version = b.version;
14
+exports.renderToReadableStream = b.renderToReadableStream;
15
+exports.renderToNodeStream = b.renderToNodeStream;
16
+exports.renderToStaticNodeStream = b.renderToStaticNodeStream;
17
+exports.renderToString = l.renderToString;
18
+exports.renderToStaticMarkup = l.renderToStaticMarkup;
packages/react-dom/package.json
+2
@@ -32,6 +32,7 @@
32
"server.js",
33
"server.browser.js",
34
"server.node.js",
35
+ "server.bun.js",
36
"static.js",
37
"static.browser.js",
38
"static.node.js",
@@ -46,6 +47,7 @@
47
".": "./index.js",
48
"./client": "./client.js",
49
"./server": {
50
+ "bun": "./server.bun.js",
51
"deno": "./server.browser.js",
52
"worker": "./server.browser.js",
53
"browser": "./server.browser.js",
packages/react-dom/server.bun.js
new
+47
@@ -0,0 +1,47 @@
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
+
8
+// This file is only used for tests.
9
+// It lazily loads the implementation so that we get the correct set of host configs.
10
+
11
+import ReactVersion from 'shared/ReactVersion';
12
+export {ReactVersion as version};
13
+
14
+export function renderToReadableStream() {
15
+ return require('./src/server/ReactDOMFizzServerBun').renderToReadableStream.apply(
16
+ this,
17
+ arguments,
18
+ );
19
+}
20
+
21
+export function renderToNodeStream() {
22
+ return require('./src/server/ReactDOMFizzServerBun').renderToNodeStream.apply(
23
+ this,
24
+ arguments,
25
+ );
26
+}
27
+
28
+export function renderToStaticNodeStream() {
29
+ return require('./src/server/ReactDOMFizzServerBun').renderToStaticNodeStream.apply(
30
+ this,
31
+ arguments,
32
+ );
33
+}
34
+
35
+export function renderToString() {
36
+ return require('./src/server/ReactDOMLegacyServerBrowser').renderToString.apply(
37
+ this,
38
+ arguments,
39
+ );
40
+}
41
+
42
+export function renderToStaticMarkup() {
43
+ return require('./src/server/ReactDOMLegacyServerBrowser').renderToStaticMarkup.apply(
44
+ this,
45
+ arguments,
46
+ );
47
+}
packages/react-dom/src/server/ReactDOMFizzServerBun.js
new
+136
@@ -0,0 +1,136 @@
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
+import type {ReactNodeList} from 'shared/ReactTypes';
11
+import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactDOMServerFormatConfig';
12
+
13
+import ReactVersion from 'shared/ReactVersion';
14
+
15
+import {
16
+ createRequest,
17
+ startWork,
18
+ startFlowing,
19
+ abort,
20
+} from 'react-server/src/ReactFizzServer';
21
+
22
+import {
23
+ createResponseState,
24
+ createRootFormatContext,
25
+} from 'react-dom-bindings/src/server/ReactDOMServerFormatConfig';
26
+
27
+type Options = {
28
+ identifierPrefix?: string,
29
+ namespaceURI?: string,
30
+ nonce?: string,
31
+ bootstrapScriptContent?: string,
32
+ bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
33
+ bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
34
+ progressiveChunkSize?: number,
35
+ signal?: AbortSignal,
36
+ onError?: (error: mixed) => ?string,
37
+ unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
38
+};
39
+
40
+// TODO: Move to sub-classing ReadableStream.
41
+type ReactDOMServerReadableStream = ReadableStream & {
42
+ allReady: Promise<void>,
43
+};
44
+
45
+function renderToReadableStream(
46
+ children: ReactNodeList,
47
+ options?: Options,
48
+): Promise<ReactDOMServerReadableStream> {
49
+ return new Promise((resolve, reject) => {
50
+ let onFatalError;
51
+ let onAllReady;
52
+ const allReady = new Promise((res, rej) => {
53
+ onAllReady = res;
54
+ onFatalError = rej;
55
+ });
56
+
57
+ function onShellReady() {
58
+ const stream: ReactDOMServerReadableStream = (new ReadableStream(
59
+ {
60
+ type: 'direct',
61
+ pull: (controller): ?Promise<void> => {
62
+ // $FlowIgnore
63
+ startFlowing(request, controller);
64
+ },
65
+ cancel: (reason): ?Promise<void> => {
66
+ abort(request);
67
+ },
68
+ },
69
+ // $FlowFixMe size() methods are not allowed on byte streams.
70
+ {highWaterMark: 2048},
71
+ ): any);
72
+ // TODO: Move to sub-classing ReadableStream.
73
+ stream.allReady = allReady;
74
+ resolve(stream);
75
+ }
76
+ function onShellError(error: mixed) {
77
+ // If the shell errors the caller of `renderToReadableStream` won't have access to `allReady`.
78
+ // However, `allReady` will be rejected by `onFatalError` as well.
79
+ // So we need to catch the duplicate, uncatchable fatal error in `allReady` to prevent a `UnhandledPromiseRejection`.
80
+ allReady.catch(() => {});
81
+ reject(error);
82
+ }
83
+ const request = createRequest(
84
+ children,
85
+ createResponseState(
86
+ options ? options.identifierPrefix : undefined,
87
+ options ? options.nonce : undefined,
88
+ options ? options.bootstrapScriptContent : undefined,
89
+ options ? options.bootstrapScripts : undefined,
90
+ options ? options.bootstrapModules : undefined,
91
+ options ? options.unstable_externalRuntimeSrc : undefined,
92
+ ),
93
+ createRootFormatContext(options ? options.namespaceURI : undefined),
94
+ options ? options.progressiveChunkSize : undefined,
95
+ options ? options.onError : undefined,
96
+ onAllReady,
97
+ onShellReady,
98
+ onShellError,
99
+ onFatalError,
100
+ );
101
+ if (options && options.signal) {
102
+ const signal = options.signal;
103
+ if (signal.aborted) {
104
+ abort(request, (signal: any).reason);
105
+ } else {
106
+ const listener = () => {
107
+ abort(request, (signal: any).reason);
108
+ signal.removeEventListener('abort', listener);
109
+ };
110
+ signal.addEventListener('abort', listener);
111
+ }
112
+ }
113
+ startWork(request);
114
+ });
115
+}
116
+
117
+function renderToNodeStream() {
118
+ throw new Error(
119
+ 'ReactDOMServer.renderToNodeStream(): The Node Stream API is not available ' +
120
+ 'in Bun. Use ReactDOMServer.renderToReadableStream() instead.',
121
+ );
122
+}
123
+
124
+function renderToStaticNodeStream() {
125
+ throw new Error(
126
+ 'ReactDOMServer.renderToStaticNodeStream(): The Node Stream API is not available ' +
127
+ 'in Bun. Use ReactDOMServer.renderToReadableStream() instead.',
128
+ );
129
+}
130
+
131
+export {
132
+ renderToReadableStream,
133
+ renderToNodeStream,
134
+ renderToStaticNodeStream,
135
+ ReactVersion as version,
136
+};
packages/react-reconciler/src/forks/ReactFiberHostConfig.bun.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 * from 'react-dom-bindings/src/client/ReactDOMHostConfig';
packages/react-server/src/ReactServerStreamConfigBun.js
new
+81
@@ -0,0 +1,81 @@
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
+type BunReadableStreamController = ReadableStreamController & {
11
+ end(): mixed,
12
+ write(data: Chunk): void,
13
+ error(error: Error): void,
14
+};
15
+export type Destination = BunReadableStreamController;
16
+
17
+export type PrecomputedChunk = string;
18
+export opaque type Chunk = string;
19
+
20
+export function scheduleWork(callback: () => void) {
21
+ callback();
22
+}
23
+
24
+export function flushBuffered(destination: Destination) {
25
+ // WHATWG Streams do not yet have a way to flush the underlying
26
+ // transform streams. https://github.com/whatwg/streams/issues/960
27
+}
28
+
29
+// AsyncLocalStorage is not available in bun
30
+export const supportsRequestStorage = false;
31
+export const requestStorage = (null: any);
32
+
33
+export function beginWriting(destination: Destination) {}
34
+
35
+export function writeChunk(
36
+ destination: Destination,
37
+ chunk: PrecomputedChunk | Chunk,
38
+): void {
39
+ if (chunk.length === 0) {
40
+ return;
41
+ }
42
+
43
+ destination.write(chunk);
44
+}
45
+
46
+export function writeChunkAndReturn(
47
+ destination: Destination,
48
+ chunk: PrecomputedChunk | Chunk,
49
+): boolean {
50
+ return !!destination.write(chunk);
51
+}
52
+
53
+export function completeWriting(destination: Destination) {}
54
+
55
+export function close(destination: Destination) {
56
+ destination.end();
57
+}
58
+
59
+export function stringToChunk(content: string): Chunk {
60
+ return content;
61
+}
62
+
63
+export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
64
+ return content;
65
+}
66
+
67
+export function closeWithError(destination: Destination, error: mixed): void {
68
+ // $FlowFixMe[method-unbinding]
69
+ if (typeof destination.error === 'function') {
70
+ // $FlowFixMe: This is an Error object or the destination accepts other types.
71
+ destination.error(error);
72
+ } else {
73
+ // Earlier implementations doesn't support this method. In that environment you're
74
+ // supposed to throw from a promise returned but we don't return a promise in our
75
+ // approach. We could fork this implementation but this is environment is an edge
76
+ // case to begin with. It's even less common to run this in an older environment.
77
+ // Even then, this is not where errors are supposed to happen and they get reported
78
+ // to a global callback in addition to this anyway. So it's fine just to close this.
79
+ destination.close();
80
+ }
81
+}
packages/react-server/src/forks/ReactFlightServerConfig.bun.js
new
+11
@@ -0,0 +1,11 @@
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 * from '../ReactFlightServerConfigStream';
11
+export * from 'react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig';
packages/react-server/src/forks/ReactServerFormatConfig.bun.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 * from 'react-dom-bindings/src/server/ReactDOMServerFormatConfig';
packages/react-server/src/forks/ReactServerStreamConfig.bun.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 * from '../ReactServerStreamConfigBun';
scripts/error-codes/codes.json
+3
-1
@@ -447,5 +447,7 @@
447
"459": "Expected a suspended thenable. This is a bug in React. Please file an issue.",
448
"460": "Suspense Exception: This is not a real error! It's an implementation detail of `use` to interrupt the current render. You must either rethrow it immediately, or move the `use` call outside of the `try/catch` block. Capturing without rethrowing will lead to unexpected behavior.\n\nTo handle async errors, wrap your component in an error boundary, or call the promise's `.catch` method and pass the result to `use`",
449
"461": "This is not a real error. It's an implementation detail of React's selective hydration feature. If this leaks into userspace, it's a bug in React. Please file an issue.",
450
- "462": "Unexpected SuspendedReason. This is a bug in React."
450
+ "462": "Unexpected SuspendedReason. This is a bug in React.",
451
+ "463": "ReactDOMServer.renderToNodeStream(): The Node Stream API is not available in Bun. Use ReactDOMServer.renderToReadableStream() instead.",
452
+ "464": "ReactDOMServer.renderToStaticNodeStream(): The Node Stream API is not available in Bun. Use ReactDOMServer.renderToReadableStream() instead."
453
}
scripts/rollup/build.js
+11
@@ -51,6 +51,8 @@ const {
51
NODE_DEV,
52
NODE_PROD,
53
NODE_PROFILING,
54
+ BUN_DEV,
55
+ BUN_PROD,
56
FB_WWW_DEV,
57
FB_WWW_PROD,
58
FB_WWW_PROFILING,
@@ -200,6 +202,8 @@ function getFormat(bundleType) {
202
case NODE_DEV:
203
case NODE_PROD:
204
case NODE_PROFILING:
205
+ case BUN_DEV:
206
+ case BUN_PROD:
207
case FB_WWW_DEV:
208
case FB_WWW_PROD:
209
case FB_WWW_PROFILING:
@@ -223,12 +227,14 @@ function isProductionBundleType(bundleType) {
227
case NODE_ESM:
228
case UMD_DEV:
229
case NODE_DEV:
230
+ case BUN_DEV:
231
case FB_WWW_DEV:
232
case RN_OSS_DEV:
233
case RN_FB_DEV:
234
return false;
235
case UMD_PROD:
236
case NODE_PROD:
237
+ case BUN_PROD:
238
case UMD_PROFILING:
239
case NODE_PROFILING:
240
case FB_WWW_PROD:
@@ -252,6 +258,8 @@ function isProfilingBundleType(bundleType) {
258
case FB_WWW_PROD:
259
case NODE_DEV:
260
case NODE_PROD:
261
+ case BUN_DEV:
262
+ case BUN_PROD:
263
case RN_FB_DEV:
264
case RN_FB_PROD:
265
case RN_OSS_DEV:
@@ -589,6 +597,7 @@ async function createBundle(bundle, bundleType) {
597
filename,
598
packageName
599
);
600
+
601
const rollupOutputOptions = getRollupOutputOptions(
602
mainOutputPath,
603
format,
@@ -719,6 +728,8 @@ async function buildEverything() {
728
[bundle, NODE_DEV],
729
[bundle, NODE_PROD],
730
[bundle, NODE_PROFILING],
731
+ [bundle, BUN_DEV],
732
+ [bundle, BUN_PROD],
733
[bundle, FB_WWW_DEV],
734
[bundle, FB_WWW_PROD],
735
[bundle, FB_WWW_PROFILING],
scripts/rollup/bundles.js
+21
@@ -16,6 +16,8 @@ const bundleTypes = {
16
NODE_DEV: 'NODE_DEV',
17
NODE_PROD: 'NODE_PROD',
18
NODE_PROFILING: 'NODE_PROFILING',
19
+ BUN_DEV: 'BUN_DEV',
20
+ BUN_PROD: 'BUN_PROD',
21
FB_WWW_DEV: 'FB_WWW_DEV',
22
FB_WWW_PROD: 'FB_WWW_PROD',
23
FB_WWW_PROFILING: 'FB_WWW_PROFILING',
@@ -37,6 +39,8 @@ const {
39
NODE_DEV,
40
NODE_PROD,
41
NODE_PROFILING,
42
+ BUN_DEV,
43
+ BUN_PROD,
44
FB_WWW_DEV,
45
FB_WWW_PROD,
46
FB_WWW_PROFILING,
@@ -266,6 +270,19 @@ const bundles = [
270
externals: ['react', 'react-dom'],
271
},
272
273
+ /******* React DOM Fizz Server Bun *******/
274
+ {
275
+ bundleTypes: [BUN_DEV, BUN_PROD],
276
+ moduleType: RENDERER,
277
+ entry: 'react-dom/src/server/ReactDOMFizzServerBun.js',
278
+ name: 'react-dom-server.bun', // 'node_modules/react/*.js',
279
+
280
+ global: 'ReactDOMServer',
281
+ minifyWithProdErrorCodes: false,
282
+ wrapWithModuleBoundaries: false,
283
+ externals: ['react', 'react-dom'],
284
+ },
285
+
286
/******* React DOM Fizz Static *******/
287
{
288
bundleTypes: __EXPERIMENTAL__ ? [NODE_DEV, NODE_PROD] : [],
@@ -955,6 +972,10 @@ function getOriginalFilename(bundle, bundleType) {
972
return `${name}.js`;
973
case NODE_ESM:
974
return `${name}.js`;
975
+ case BUN_DEV:
976
+ return `${name}.development.js`;
977
+ case BUN_PROD:
978
+ return `${name}.production.min.js`;
979
case UMD_DEV:
980
return `${name}.development.js`;
981
case UMD_PROD:
scripts/rollup/packaging.js
+5
@@ -24,6 +24,8 @@ const {
24
NODE_DEV,
25
NODE_PROD,
26
NODE_PROFILING,
27
+ BUN_DEV,
28
+ BUN_PROD,
29
FB_WWW_DEV,
30
FB_WWW_PROD,
31
FB_WWW_PROFILING,
@@ -49,6 +51,9 @@ function getBundleOutputPath(bundle, bundleType, filename, packageName) {
51
return `build/node_modules/${packageName}/cjs/${filename}`;
52
case NODE_ESM:
53
return `build/node_modules/${packageName}/esm/${filename}`;
54
+ case BUN_DEV:
55
+ case BUN_PROD:
56
+ return `build/node_modules/${packageName}/cjs/${filename}`;
57
case NODE_DEV:
58
case NODE_PROD:
59
case NODE_PROFILING:
scripts/rollup/wrappers.js
+26
@@ -13,6 +13,8 @@ const {
13
NODE_DEV,
14
NODE_PROD,
15
NODE_PROFILING,
16
+ BUN_DEV,
17
+ BUN_PROD,
18
FB_WWW_DEV,
19
FB_WWW_PROD,
20
FB_WWW_PROFILING,
@@ -75,6 +77,30 @@ ${source}`;
77
${license}
78
*/
79
80
+${source}`;
81
+ },
82
+
83
+ /***************** BUN_DEV *****************/
84
+ [BUN_DEV](source, globalName, filename, moduleType) {
85
+ return `/**
86
+* @license React
87
+ * ${filename}
88
+ *
89
+${license}
90
+ */
91
+
92
+${source}`;
93
+ },
94
+
95
+ /***************** BUN_PROD *****************/
96
+ [BUN_PROD](source, globalName, filename, moduleType) {
97
+ return `/**
98
+* @license React
99
+ * ${filename}
100
+ *
101
+${license}
102
+ */
103
+
104
${source}`;
105
},
106
scripts/shared/inlinedHostConfigs.js
+19
@@ -45,6 +45,25 @@ module.exports = [
45
isFlowTyped: true,
46
isServerSupported: true,
47
},
48
+ {
49
+ shortName: 'bun',
50
+ entryPoints: ['react-dom', 'react-dom/src/server/ReactDOMFizzServerBun.js'],
51
+ paths: [
52
+ 'react-dom',
53
+ 'react-dom/server',
54
+ 'react-dom/server.bun',
55
+ 'react-dom/src/server/ReactDOMFizzServerBun.js',
56
+ 'react-dom-bindings',
57
+ 'react-dom/server.node',
58
+ 'react-server-dom-webpack',
59
+ 'react-server-dom-webpack/client',
60
+ 'react-server-dom-webpack/server',
61
+ 'react-client/src/ReactFlightClientStream.js', // We can only type check this in streaming configurations.
62
+ 'shared/ReactDOMSharedInternals',
63
+ ],
64
+ isFlowTyped: true,
65
+ isServerSupported: true,
66
+ },
67
{
68
shortName: 'dom-browser',
69
entryPoints: [