From 56ffca8b9e4e49ad46136fe705203afc2d20fd9f Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Thu, 17 Nov 2022 13:15:56 -0800 Subject: [PATCH] Add Bun streaming server renderer (#25597) Add support for Bun server renderer --- .../forks/ReactFlightClientHostConfig.bun.js | 12 ++ packages/react-dom/npm/server.bun.js | 18 +++ packages/react-dom/package.json | 2 + packages/react-dom/server.bun.js | 47 ++++++ .../src/server/ReactDOMFizzServerBun.js | 136 ++++++++++++++++++ .../src/forks/ReactFiberHostConfig.bun.js | 10 ++ .../src/ReactServerStreamConfigBun.js | 81 +++++++++++ .../src/forks/ReactFlightServerConfig.bun.js | 11 ++ .../src/forks/ReactServerFormatConfig.bun.js | 10 ++ .../src/forks/ReactServerStreamConfig.bun.js | 10 ++ scripts/error-codes/codes.json | 4 +- scripts/rollup/build.js | 11 ++ scripts/rollup/bundles.js | 21 +++ scripts/rollup/packaging.js | 5 + scripts/rollup/wrappers.js | 26 ++++ scripts/shared/inlinedHostConfigs.js | 19 +++ 16 files changed, 422 insertions(+), 1 deletion(-) create mode 100644 packages/react-client/src/forks/ReactFlightClientHostConfig.bun.js create mode 100644 packages/react-dom/npm/server.bun.js create mode 100644 packages/react-dom/server.bun.js create mode 100644 packages/react-dom/src/server/ReactDOMFizzServerBun.js create mode 100644 packages/react-reconciler/src/forks/ReactFiberHostConfig.bun.js create mode 100644 packages/react-server/src/ReactServerStreamConfigBun.js create mode 100644 packages/react-server/src/forks/ReactFlightServerConfig.bun.js create mode 100644 packages/react-server/src/forks/ReactServerFormatConfig.bun.js create mode 100644 packages/react-server/src/forks/ReactServerStreamConfig.bun.js diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.bun.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.bun.js new file mode 100644 index 0000000000..4aae8141fd --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.bun.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from 'react-client/src/ReactFlightClientHostConfigBrowser'; +export * from 'react-client/src/ReactFlightClientHostConfigStream'; +export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig'; diff --git a/packages/react-dom/npm/server.bun.js b/packages/react-dom/npm/server.bun.js new file mode 100644 index 0000000000..aa27124d58 --- /dev/null +++ b/packages/react-dom/npm/server.bun.js @@ -0,0 +1,18 @@ +'use strict'; + +var b; +var l; +if (process.env.NODE_ENV === 'production') { + b = require('./cjs/react-dom-server.bun.production.min.js'); + l = require('./cjs/react-dom-server-legacy.browser.production.min.js'); +} else { + b = require('./cjs/react-dom-server.bun.development.js'); + l = require('./cjs/react-dom-server-legacy.browser.development.js'); +} + +exports.version = b.version; +exports.renderToReadableStream = b.renderToReadableStream; +exports.renderToNodeStream = b.renderToNodeStream; +exports.renderToStaticNodeStream = b.renderToStaticNodeStream; +exports.renderToString = l.renderToString; +exports.renderToStaticMarkup = l.renderToStaticMarkup; diff --git a/packages/react-dom/package.json b/packages/react-dom/package.json index ee675b040a..3ee587be55 100644 --- a/packages/react-dom/package.json +++ b/packages/react-dom/package.json @@ -32,6 +32,7 @@ "server.js", "server.browser.js", "server.node.js", + "server.bun.js", "static.js", "static.browser.js", "static.node.js", @@ -46,6 +47,7 @@ ".": "./index.js", "./client": "./client.js", "./server": { + "bun": "./server.bun.js", "deno": "./server.browser.js", "worker": "./server.browser.js", "browser": "./server.browser.js", diff --git a/packages/react-dom/server.bun.js b/packages/react-dom/server.bun.js new file mode 100644 index 0000000000..0ac32351ec --- /dev/null +++ b/packages/react-dom/server.bun.js @@ -0,0 +1,47 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// This file is only used for tests. +// It lazily loads the implementation so that we get the correct set of host configs. + +import ReactVersion from 'shared/ReactVersion'; +export {ReactVersion as version}; + +export function renderToReadableStream() { + return require('./src/server/ReactDOMFizzServerBun').renderToReadableStream.apply( + this, + arguments, + ); +} + +export function renderToNodeStream() { + return require('./src/server/ReactDOMFizzServerBun').renderToNodeStream.apply( + this, + arguments, + ); +} + +export function renderToStaticNodeStream() { + return require('./src/server/ReactDOMFizzServerBun').renderToStaticNodeStream.apply( + this, + arguments, + ); +} + +export function renderToString() { + return require('./src/server/ReactDOMLegacyServerBrowser').renderToString.apply( + this, + arguments, + ); +} + +export function renderToStaticMarkup() { + return require('./src/server/ReactDOMLegacyServerBrowser').renderToStaticMarkup.apply( + this, + arguments, + ); +} diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBun.js b/packages/react-dom/src/server/ReactDOMFizzServerBun.js new file mode 100644 index 0000000000..afa168c000 --- /dev/null +++ b/packages/react-dom/src/server/ReactDOMFizzServerBun.js @@ -0,0 +1,136 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactNodeList} from 'shared/ReactTypes'; +import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactDOMServerFormatConfig'; + +import ReactVersion from 'shared/ReactVersion'; + +import { + createRequest, + startWork, + startFlowing, + abort, +} from 'react-server/src/ReactFizzServer'; + +import { + createResponseState, + createRootFormatContext, +} from 'react-dom-bindings/src/server/ReactDOMServerFormatConfig'; + +type Options = { + identifierPrefix?: string, + namespaceURI?: string, + nonce?: string, + bootstrapScriptContent?: string, + bootstrapScripts?: Array, + bootstrapModules?: Array, + progressiveChunkSize?: number, + signal?: AbortSignal, + onError?: (error: mixed) => ?string, + unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, +}; + +// TODO: Move to sub-classing ReadableStream. +type ReactDOMServerReadableStream = ReadableStream & { + allReady: Promise, +}; + +function renderToReadableStream( + children: ReactNodeList, + options?: Options, +): Promise { + return new Promise((resolve, reject) => { + let onFatalError; + let onAllReady; + const allReady = new Promise((res, rej) => { + onAllReady = res; + onFatalError = rej; + }); + + function onShellReady() { + const stream: ReactDOMServerReadableStream = (new ReadableStream( + { + type: 'direct', + pull: (controller): ?Promise => { + // $FlowIgnore + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => { + abort(request); + }, + }, + // $FlowFixMe size() methods are not allowed on byte streams. + {highWaterMark: 2048}, + ): any); + // TODO: Move to sub-classing ReadableStream. + stream.allReady = allReady; + resolve(stream); + } + function onShellError(error: mixed) { + // If the shell errors the caller of `renderToReadableStream` won't have access to `allReady`. + // However, `allReady` will be rejected by `onFatalError` as well. + // So we need to catch the duplicate, uncatchable fatal error in `allReady` to prevent a `UnhandledPromiseRejection`. + allReady.catch(() => {}); + reject(error); + } + const request = createRequest( + children, + createResponseState( + options ? options.identifierPrefix : undefined, + options ? options.nonce : undefined, + options ? options.bootstrapScriptContent : undefined, + options ? options.bootstrapScripts : undefined, + options ? options.bootstrapModules : undefined, + options ? options.unstable_externalRuntimeSrc : undefined, + ), + createRootFormatContext(options ? options.namespaceURI : undefined), + options ? options.progressiveChunkSize : undefined, + options ? options.onError : undefined, + onAllReady, + onShellReady, + onShellError, + onFatalError, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort(request, (signal: any).reason); + } else { + const listener = () => { + abort(request, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + startWork(request); + }); +} + +function renderToNodeStream() { + throw new Error( + 'ReactDOMServer.renderToNodeStream(): The Node Stream API is not available ' + + 'in Bun. Use ReactDOMServer.renderToReadableStream() instead.', + ); +} + +function renderToStaticNodeStream() { + throw new Error( + 'ReactDOMServer.renderToStaticNodeStream(): The Node Stream API is not available ' + + 'in Bun. Use ReactDOMServer.renderToReadableStream() instead.', + ); +} + +export { + renderToReadableStream, + renderToNodeStream, + renderToStaticNodeStream, + ReactVersion as version, +}; diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.bun.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.bun.js new file mode 100644 index 0000000000..aae45be9aa --- /dev/null +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.bun.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from 'react-dom-bindings/src/client/ReactDOMHostConfig'; diff --git a/packages/react-server/src/ReactServerStreamConfigBun.js b/packages/react-server/src/ReactServerStreamConfigBun.js new file mode 100644 index 0000000000..c50ce77fa9 --- /dev/null +++ b/packages/react-server/src/ReactServerStreamConfigBun.js @@ -0,0 +1,81 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +type BunReadableStreamController = ReadableStreamController & { + end(): mixed, + write(data: Chunk): void, + error(error: Error): void, +}; +export type Destination = BunReadableStreamController; + +export type PrecomputedChunk = string; +export opaque type Chunk = string; + +export function scheduleWork(callback: () => void) { + callback(); +} + +export function flushBuffered(destination: Destination) { + // WHATWG Streams do not yet have a way to flush the underlying + // transform streams. https://github.com/whatwg/streams/issues/960 +} + +// AsyncLocalStorage is not available in bun +export const supportsRequestStorage = false; +export const requestStorage = (null: any); + +export function beginWriting(destination: Destination) {} + +export function writeChunk( + destination: Destination, + chunk: PrecomputedChunk | Chunk, +): void { + if (chunk.length === 0) { + return; + } + + destination.write(chunk); +} + +export function writeChunkAndReturn( + destination: Destination, + chunk: PrecomputedChunk | Chunk, +): boolean { + return !!destination.write(chunk); +} + +export function completeWriting(destination: Destination) {} + +export function close(destination: Destination) { + destination.end(); +} + +export function stringToChunk(content: string): Chunk { + return content; +} + +export function stringToPrecomputedChunk(content: string): PrecomputedChunk { + return content; +} + +export function closeWithError(destination: Destination, error: mixed): void { + // $FlowFixMe[method-unbinding] + if (typeof destination.error === 'function') { + // $FlowFixMe: This is an Error object or the destination accepts other types. + destination.error(error); + } else { + // Earlier implementations doesn't support this method. In that environment you're + // supposed to throw from a promise returned but we don't return a promise in our + // approach. We could fork this implementation but this is environment is an edge + // case to begin with. It's even less common to run this in an older environment. + // Even then, this is not where errors are supposed to happen and they get reported + // to a global callback in addition to this anyway. So it's fine just to close this. + destination.close(); + } +} diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.bun.js b/packages/react-server/src/forks/ReactFlightServerConfig.bun.js new file mode 100644 index 0000000000..99c541a937 --- /dev/null +++ b/packages/react-server/src/forks/ReactFlightServerConfig.bun.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from '../ReactFlightServerConfigStream'; +export * from 'react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig'; diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.bun.js b/packages/react-server/src/forks/ReactServerFormatConfig.bun.js new file mode 100644 index 0000000000..485793a689 --- /dev/null +++ b/packages/react-server/src/forks/ReactServerFormatConfig.bun.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from 'react-dom-bindings/src/server/ReactDOMServerFormatConfig'; diff --git a/packages/react-server/src/forks/ReactServerStreamConfig.bun.js b/packages/react-server/src/forks/ReactServerStreamConfig.bun.js new file mode 100644 index 0000000000..0814a2e5c4 --- /dev/null +++ b/packages/react-server/src/forks/ReactServerStreamConfig.bun.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from '../ReactServerStreamConfigBun'; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index b4ea290152..7b9237da86 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -447,5 +447,7 @@ "459": "Expected a suspended thenable. This is a bug in React. Please file an issue.", "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`", "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.", - "462": "Unexpected SuspendedReason. This is a bug in React." + "462": "Unexpected SuspendedReason. This is a bug in React.", + "463": "ReactDOMServer.renderToNodeStream(): The Node Stream API is not available in Bun. Use ReactDOMServer.renderToReadableStream() instead.", + "464": "ReactDOMServer.renderToStaticNodeStream(): The Node Stream API is not available in Bun. Use ReactDOMServer.renderToReadableStream() instead." } diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index 97b949b560..daec05ff1b 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -51,6 +51,8 @@ const { NODE_DEV, NODE_PROD, NODE_PROFILING, + BUN_DEV, + BUN_PROD, FB_WWW_DEV, FB_WWW_PROD, FB_WWW_PROFILING, @@ -200,6 +202,8 @@ function getFormat(bundleType) { case NODE_DEV: case NODE_PROD: case NODE_PROFILING: + case BUN_DEV: + case BUN_PROD: case FB_WWW_DEV: case FB_WWW_PROD: case FB_WWW_PROFILING: @@ -223,12 +227,14 @@ function isProductionBundleType(bundleType) { case NODE_ESM: case UMD_DEV: case NODE_DEV: + case BUN_DEV: case FB_WWW_DEV: case RN_OSS_DEV: case RN_FB_DEV: return false; case UMD_PROD: case NODE_PROD: + case BUN_PROD: case UMD_PROFILING: case NODE_PROFILING: case FB_WWW_PROD: @@ -252,6 +258,8 @@ function isProfilingBundleType(bundleType) { case FB_WWW_PROD: case NODE_DEV: case NODE_PROD: + case BUN_DEV: + case BUN_PROD: case RN_FB_DEV: case RN_FB_PROD: case RN_OSS_DEV: @@ -589,6 +597,7 @@ async function createBundle(bundle, bundleType) { filename, packageName ); + const rollupOutputOptions = getRollupOutputOptions( mainOutputPath, format, @@ -719,6 +728,8 @@ async function buildEverything() { [bundle, NODE_DEV], [bundle, NODE_PROD], [bundle, NODE_PROFILING], + [bundle, BUN_DEV], + [bundle, BUN_PROD], [bundle, FB_WWW_DEV], [bundle, FB_WWW_PROD], [bundle, FB_WWW_PROFILING], diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index df208162b9..23a2089a73 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -16,6 +16,8 @@ const bundleTypes = { NODE_DEV: 'NODE_DEV', NODE_PROD: 'NODE_PROD', NODE_PROFILING: 'NODE_PROFILING', + BUN_DEV: 'BUN_DEV', + BUN_PROD: 'BUN_PROD', FB_WWW_DEV: 'FB_WWW_DEV', FB_WWW_PROD: 'FB_WWW_PROD', FB_WWW_PROFILING: 'FB_WWW_PROFILING', @@ -37,6 +39,8 @@ const { NODE_DEV, NODE_PROD, NODE_PROFILING, + BUN_DEV, + BUN_PROD, FB_WWW_DEV, FB_WWW_PROD, FB_WWW_PROFILING, @@ -266,6 +270,19 @@ const bundles = [ externals: ['react', 'react-dom'], }, + /******* React DOM Fizz Server Bun *******/ + { + bundleTypes: [BUN_DEV, BUN_PROD], + moduleType: RENDERER, + entry: 'react-dom/src/server/ReactDOMFizzServerBun.js', + name: 'react-dom-server.bun', // 'node_modules/react/*.js', + + global: 'ReactDOMServer', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom'], + }, + /******* React DOM Fizz Static *******/ { bundleTypes: __EXPERIMENTAL__ ? [NODE_DEV, NODE_PROD] : [], @@ -955,6 +972,10 @@ function getOriginalFilename(bundle, bundleType) { return `${name}.js`; case NODE_ESM: return `${name}.js`; + case BUN_DEV: + return `${name}.development.js`; + case BUN_PROD: + return `${name}.production.min.js`; case UMD_DEV: return `${name}.development.js`; case UMD_PROD: diff --git a/scripts/rollup/packaging.js b/scripts/rollup/packaging.js index 717210ec45..63affa7ebf 100644 --- a/scripts/rollup/packaging.js +++ b/scripts/rollup/packaging.js @@ -24,6 +24,8 @@ const { NODE_DEV, NODE_PROD, NODE_PROFILING, + BUN_DEV, + BUN_PROD, FB_WWW_DEV, FB_WWW_PROD, FB_WWW_PROFILING, @@ -49,6 +51,9 @@ function getBundleOutputPath(bundle, bundleType, filename, packageName) { return `build/node_modules/${packageName}/cjs/${filename}`; case NODE_ESM: return `build/node_modules/${packageName}/esm/${filename}`; + case BUN_DEV: + case BUN_PROD: + return `build/node_modules/${packageName}/cjs/${filename}`; case NODE_DEV: case NODE_PROD: case NODE_PROFILING: diff --git a/scripts/rollup/wrappers.js b/scripts/rollup/wrappers.js index cbe1dedba2..6c31d45f70 100644 --- a/scripts/rollup/wrappers.js +++ b/scripts/rollup/wrappers.js @@ -13,6 +13,8 @@ const { NODE_DEV, NODE_PROD, NODE_PROFILING, + BUN_DEV, + BUN_PROD, FB_WWW_DEV, FB_WWW_PROD, FB_WWW_PROFILING, @@ -75,6 +77,30 @@ ${source}`; ${license} */ +${source}`; + }, + + /***************** BUN_DEV *****************/ + [BUN_DEV](source, globalName, filename, moduleType) { + return `/** +* @license React + * ${filename} + * +${license} + */ + +${source}`; + }, + + /***************** BUN_PROD *****************/ + [BUN_PROD](source, globalName, filename, moduleType) { + return `/** +* @license React + * ${filename} + * +${license} + */ + ${source}`; }, diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 74ba7bcf4a..ec03dd1647 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -45,6 +45,25 @@ module.exports = [ isFlowTyped: true, isServerSupported: true, }, + { + shortName: 'bun', + entryPoints: ['react-dom', 'react-dom/src/server/ReactDOMFizzServerBun.js'], + paths: [ + 'react-dom', + 'react-dom/server', + 'react-dom/server.bun', + 'react-dom/src/server/ReactDOMFizzServerBun.js', + 'react-dom-bindings', + 'react-dom/server.node', + 'react-server-dom-webpack', + 'react-server-dom-webpack/client', + 'react-server-dom-webpack/server', + 'react-client/src/ReactFlightClientStream.js', // We can only type check this in streaming configurations. + 'shared/ReactDOMSharedInternals', + ], + isFlowTyped: true, + isServerSupported: true, + }, { shortName: 'dom-browser', entryPoints: [