Skip to content

Commit

Permalink
Add Bun streaming server renderer (facebook#25597)
Browse files Browse the repository at this point in the history
Add support for Bun server renderer
  • Loading branch information
colinhacks authored and mofeiZ committed Dec 5, 2022
1 parent 738aeee commit 8aa9b0b
Show file tree
Hide file tree
Showing 17 changed files with 428 additions and 2 deletions.
12 changes: 12 additions & 0 deletions packages/react-client/src/forks/ReactFlightClientHostConfig.bun.js
Original file line number Diff line number Diff line change
@@ -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';
18 changes: 18 additions & 0 deletions packages/react-dom/npm/server.bun.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions packages/react-dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"server.js",
"server.browser.js",
"server.node.js",
"server.bun.js",
"static.js",
"static.browser.js",
"static.node.js",
Expand All @@ -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",
Expand Down
47 changes: 47 additions & 0 deletions packages/react-dom/server.bun.js
Original file line number Diff line number Diff line change
@@ -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,
);
}
136 changes: 136 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerBun.js
Original file line number Diff line number Diff line change
@@ -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<string | BootstrapScriptDescriptor>,
bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
};

// TODO: Move to sub-classing ReadableStream.
type ReactDOMServerReadableStream = ReadableStream & {
allReady: Promise<void>,
};

function renderToReadableStream(
children: ReactNodeList,
options?: Options,
): Promise<ReactDOMServerReadableStream> {
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<void> => {
// $FlowIgnore
startFlowing(request, controller);
},
cancel: (reason): ?Promise<void> => {
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,
};
10 changes: 10 additions & 0 deletions packages/react-reconciler/src/forks/ReactFiberHostConfig.bun.js
Original file line number Diff line number Diff line change
@@ -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';
81 changes: 81 additions & 0 deletions packages/react-server/src/ReactServerStreamConfigBun.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
11 changes: 11 additions & 0 deletions packages/react-server/src/forks/ReactFlightServerConfig.bun.js
Original file line number Diff line number Diff line change
@@ -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';
10 changes: 10 additions & 0 deletions packages/react-server/src/forks/ReactServerFormatConfig.bun.js
Original file line number Diff line number Diff line change
@@ -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';
10 changes: 10 additions & 0 deletions packages/react-server/src/forks/ReactServerStreamConfig.bun.js
Original file line number Diff line number Diff line change
@@ -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';
2 changes: 1 addition & 1 deletion packages/shared/forks/ReactFeatureFlags.www.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const enableUpdaterTracking = __PROFILE__;
export const enableSuspenseAvoidThisFallback = true;
export const enableSuspenseAvoidThisFallbackFizz = false;
export const enableCPUSuspense = true;
export const enableFloat = false;
export const enableFloat = true;
export const enableUseHook = false;
export const enableUseMemoCacheHook = true;
export const enableUseEventHook = true;
Expand Down
9 changes: 8 additions & 1 deletion scripts/error-codes/codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -442,5 +442,12 @@
"454": "React expected a <body> element (document.body) to exist in the Document but one was not found. React never removes the body for any Document it renders into so the cause is likely in some other script running on this page.",
"455": "This CacheSignal was requested outside React which means that it is immediately aborted.",
"456": "Calling Offscreen.detach before instance handle has been set.",
"457": "acquireHeadResource encountered a resource type it did not expect: \"%s\". This is a bug in React."
"457": "acquireHeadResource encountered a resource type it did not expect: \"%s\". This is a bug in React.",
"458": "Currently React only supports one RSC renderer at a time.",
"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.",
"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."
}
Loading

0 comments on commit 8aa9b0b

Please sign in to comment.