@samitouri / QOS-React / commits / 56ffca8b9e

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: [