Skip to content

Commit 56ffca8

Browse files
authored
Add Bun streaming server renderer (#25597)
Add support for Bun server renderer
1 parent f31005d commit 56ffca8

16 files changed

+422
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -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

+18
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"server.js",
3333
"server.browser.js",
3434
"server.node.js",
35+
"server.bun.js",
3536
"static.js",
3637
"static.browser.js",
3738
"static.node.js",
@@ -46,6 +47,7 @@
4647
".": "./index.js",
4748
"./client": "./client.js",
4849
"./server": {
50+
"bun": "./server.bun.js",
4951
"deno": "./server.browser.js",
5052
"worker": "./server.browser.js",
5153
"browser": "./server.browser.js",

packages/react-dom/server.bun.js

+47
Original file line numberDiff line numberDiff line change
@@ -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+
}
Original file line numberDiff line numberDiff line change
@@ -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+
};
Original file line numberDiff line numberDiff line change
@@ -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';
Original file line numberDiff line numberDiff line change
@@ -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+
}
Original file line numberDiff line numberDiff line change
@@ -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';
Original file line numberDiff line numberDiff line change
@@ -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';
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -447,5 +447,7 @@
447447
"459": "Expected a suspended thenable. This is a bug in React. Please file an issue.",
448448
"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`",
449449
"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."
451453
}

0 commit comments

Comments
 (0)