Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Bun streaming server renderer #25597

Merged
merged 11 commits into from
Nov 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
colinhacks marked this conversation as resolved.
Show resolved Hide resolved
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';
4 changes: 3 additions & 1 deletion scripts/error-codes/codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
Loading