Add react-flight-server-fb package for Meta's internal bundler (#36309)

<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

- Adds a new react-flight-server-fb package providing RSC Flight
bindings for Meta's internal bundler stack
- Unlike webpack/turbopack integrations, this uses no manifest. Module
metadata is self-contained in ClientReference objects and sent over the
wire as-is
- Registers dom-browser-fb and dom-node-fb host configs for Rollup
builds targeting FB_WWW_DEV and FB_WWW_PROD

 Key design differences from other bundler

 - No build-time manifest
- Module IDs use Haste module names (e.g. `"MyComponent"`), with named
exports encoded as `"Module#export"`, rather than file paths resolved
through a manifest
- Client-side loading uses `Bootloader.handlePayload()` +
`JSResource().load()`
- `resolveClientReferenceMetadata` and `resolveClientReference` are
pass-throughs

## How did you test this change?

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->

E2E integration test is set up on Meta's internal system.
This commit is contained in:
Xinzi Zhou
2026-04-27 09:48:37 -07:00
committed by GitHub
parent 727d77afb3
commit 4c56e37e58
24 changed files with 1733 additions and 23 deletions
+1
View File
@@ -331,6 +331,7 @@ module.exports = {
'packages/react-server-dom-turbopack/**/*.js',
'packages/react-server-dom-parcel/**/*.js',
'packages/react-server-dom-fb/**/*.js',
'packages/react-flight-server-fb/**/*.js',
'packages/react-server-dom-unbundled/**/*.js',
'packages/react-test-renderer/**/*.js',
'packages/react-debug-tools/**/*.js',
+19 -19
View File
@@ -45,7 +45,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
lookup-only: true
- uses: actions/setup-node@v4
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -59,7 +59,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -69,7 +69,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
runtime_compiler_node_modules_cache:
name: Cache Runtime, Compiler node_modules
@@ -84,7 +84,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
lookup-only: true
- uses: actions/setup-node@v4
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -100,7 +100,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -112,7 +112,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# ----- FLOW -----
discover_flow_inline_configs:
@@ -154,7 +154,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
@@ -182,7 +182,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
@@ -212,7 +212,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -270,7 +270,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
@@ -301,7 +301,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
- name: Install runtime dependencies
run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -344,7 +344,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
@@ -430,7 +430,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
@@ -477,7 +477,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
@@ -517,7 +517,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
@@ -580,7 +580,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
@@ -618,7 +618,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
@@ -768,7 +768,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
@@ -828,7 +828,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v8-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
+4 -4
View File
@@ -33,7 +33,7 @@ jobs:
with:
path: |
**/node_modules
key: shared-lint-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
key: shared-lint-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -56,7 +56,7 @@ jobs:
with:
path: |
**/node_modules
key: shared-lint-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
key: shared-lint-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -79,7 +79,7 @@ jobs:
with:
path: |
**/node_modules
key: shared-lint-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
key: shared-lint-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -102,7 +102,7 @@ jobs:
with:
path: |
**/node_modules
key: shared-lint-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
key: shared-lint-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -0,0 +1,19 @@
/**
* 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 {default as rendererVersion} from 'shared/ReactVersion';
export const rendererPackageName = 'react-flight-server-fb';
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
export * from 'react-client/src/ReactClientConsoleConfigBrowser';
export * from 'react-client/src/ReactClientDebugConfigBrowser';
export * from 'react-flight-server-fb/src/client/ReactFlightClientConfigBundlerFB';
export * from 'react-flight-server-fb/src/client/ReactFlightClientConfigTargetFBBrowser';
export * from 'react-flight-server-fb/src/client/ReactFlightClientConfigDOMFB';
export const usedWithSSR = false;
@@ -0,0 +1,19 @@
/**
* 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 {default as rendererVersion} from 'shared/ReactVersion';
export const rendererPackageName = 'react-flight-server-fb';
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
export * from 'react-client/src/ReactClientConsoleConfigServer';
export * from 'react-client/src/ReactClientDebugConfigNode';
export * from 'react-flight-server-fb/src/client/ReactFlightClientConfigBundlerFB';
export * from 'react-flight-server-fb/src/client/ReactFlightClientConfigTargetFBBrowser';
export * from 'react-flight-server-fb/src/client/ReactFlightClientConfigDOMFB';
export const usedWithSSR = false;
@@ -0,0 +1,5 @@
# react-flight-server-fb
React Flight bindings for DOM using Meta's internal bundler.
**Use it at your own risk.**
@@ -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 './src/client/ReactFlightDOMClientBrowser';
@@ -0,0 +1,34 @@
{
"name": "react-flight-server-fb",
"description": "React Server Components bindings for DOM using Meta's internal bundler. It is not intended to be imported directly.",
"version": "19.3.0",
"keywords": [
"react"
],
"homepage": "https://react.dev/",
"bugs": "https://github.com/facebook/react/issues",
"license": "MIT",
"files": [
"LICENSE",
"README.md",
"client.browser.js"
],
"exports": {
"./client": "./client.browser.js",
"./client.browser": "./client.browser.js",
"./src/*": "./src/*.js",
"./package.json": "./package.json"
},
"repository": {
"type" : "git",
"url" : "https://github.com/facebook/react.git",
"directory": "packages/react-flight-server-fb"
},
"engines": {
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
@@ -0,0 +1,131 @@
/**
* 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 {ReactClientValue} from 'react-server/src/ReactFlightServer';
export type ServerReference<T: Function> = T & {
$$typeof: symbol,
$$id: string,
$$bound: null | Array<ReactClientValue>,
$$location?: Error,
};
// eslint-disable-next-line no-unused-vars
export type ClientReference<T> = {
$$typeof: symbol,
$$id: string,
$$hblp: mixed,
};
const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference');
const SERVER_REFERENCE_TAG = Symbol.for('react.server.reference');
export function isClientReference(reference: Object): boolean {
return reference.$$typeof === CLIENT_REFERENCE_TAG;
}
export function isServerReference(reference: Object): boolean {
return reference.$$typeof === SERVER_REFERENCE_TAG;
}
export function registerClientReference<T>(
proxyImplementation: any,
id: string,
hblp: mixed,
): ClientReference<T> {
return Object.defineProperties(proxyImplementation, {
$$typeof: {value: CLIENT_REFERENCE_TAG},
$$id: {value: id},
$$hblp: {value: hblp},
});
}
// $FlowFixMe[method-unbinding]
const FunctionBind = Function.prototype.bind;
// $FlowFixMe[method-unbinding]
const ArraySlice = Array.prototype.slice;
function bind(this: ServerReference<any>): any {
// $FlowFixMe[incompatible-call]
const newFn = FunctionBind.apply(this, arguments);
if (this.$$typeof === SERVER_REFERENCE_TAG) {
if (__DEV__) {
const thisBind = arguments[0];
if (thisBind != null) {
console.error(
'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().',
);
}
}
const args = ArraySlice.call(arguments, 1);
const $$typeof = {value: SERVER_REFERENCE_TAG};
const $$id = {value: this.$$id};
const $$bound = {value: this.$$bound ? this.$$bound.concat(args) : args};
return Object.defineProperties(
(newFn: any),
(__DEV__
? {
$$typeof,
$$id,
$$bound,
$$location: {
value: this.$$location,
configurable: true,
},
bind: {value: bind, configurable: true},
}
: {
$$typeof,
$$id,
$$bound,
bind: {value: bind, configurable: true},
}) as PropertyDescriptorMap,
);
}
return newFn;
}
const serverReferenceToString = {
value: () => 'function () { [omitted code] }',
configurable: true,
writable: true,
};
export function registerServerReference<T: Function>(
reference: T,
id: string,
): ServerReference<T> {
const $$typeof = {value: SERVER_REFERENCE_TAG};
const $$id = {
value: id,
configurable: true,
};
const $$bound = {value: null, configurable: true};
return Object.defineProperties(
(reference: any),
(__DEV__
? {
$$typeof,
$$id,
$$bound,
$$location: {
value: Error('react-stack-top-frame'),
configurable: true,
},
bind: {value: bind, configurable: true},
toString: serverReferenceToString,
}
: {
$$typeof,
$$id,
$$bound,
bind: {value: bind, configurable: true},
toString: serverReferenceToString,
}) as PropertyDescriptorMap,
);
}
@@ -0,0 +1,240 @@
/**
* 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
*/
// Fork of ReactServerStreamConfigNode for the FB Flight server.
// Avoids importing from 'util' and 'crypto'. Uses the global TextEncoder
// and stubs createFastHash.
import type {Writable} from 'stream';
import type {TextEncoder as TextEncoderType} from 'util';
interface MightBeFlushable {
flush?: () => void;
}
export type Destination = Writable & MightBeFlushable;
export type PrecomputedChunk = Uint8Array;
export opaque type Chunk = string;
export type BinaryChunk = Uint8Array;
export function scheduleWork(callback: () => void) {
setImmediate(callback);
}
export function scheduleMicrotask(callback: () => void) {
Promise.resolve().then(callback);
}
export function flushBuffered(destination: Destination) {
// If we don't have any more data to send right now.
// Flush whatever is in the buffer to the wire.
if (typeof destination.flush === 'function') {
// By convention the Zlib streams provide a flush function for this purpose.
// For Express, compression middleware adds this method.
destination.flush();
}
}
const VIEW_SIZE = 4096;
let currentView = null;
let writtenBytes = 0;
let destinationHasCapacity = true;
export function beginWriting(destination: Destination) {
currentView = new Uint8Array(VIEW_SIZE);
writtenBytes = 0;
destinationHasCapacity = true;
}
function writeStringChunk(destination: Destination, stringChunk: string) {
if (stringChunk.length === 0) {
return;
}
// maximum possible view needed to encode entire string
if (stringChunk.length * 3 > VIEW_SIZE) {
if (writtenBytes > 0) {
writeToDestination(
destination,
((currentView: any): Uint8Array).subarray(0, writtenBytes),
);
currentView = new Uint8Array(VIEW_SIZE);
writtenBytes = 0;
}
// Write the raw string chunk and let the consumer handle the encoding.
writeToDestination(destination, stringChunk);
return;
}
let target: Uint8Array = (currentView: any);
if (writtenBytes > 0) {
target = ((currentView: any): Uint8Array).subarray(writtenBytes);
}
const {read, written} = textEncoder.encodeInto(stringChunk, target);
writtenBytes += written;
if (read < stringChunk.length) {
writeToDestination(
destination,
(currentView: any).subarray(0, writtenBytes),
);
currentView = new Uint8Array(VIEW_SIZE);
writtenBytes = textEncoder.encodeInto(
stringChunk.slice(read),
(currentView: any),
).written;
}
if (writtenBytes === VIEW_SIZE) {
writeToDestination(destination, (currentView: any));
currentView = new Uint8Array(VIEW_SIZE);
writtenBytes = 0;
}
}
function writeViewChunk(
destination: Destination,
chunk: PrecomputedChunk | BinaryChunk,
) {
if (chunk.byteLength === 0) {
return;
}
if (chunk.byteLength > VIEW_SIZE) {
if (writtenBytes > 0) {
writeToDestination(
destination,
((currentView: any): Uint8Array).subarray(0, writtenBytes),
);
currentView = new Uint8Array(VIEW_SIZE);
writtenBytes = 0;
}
writeToDestination(destination, chunk);
return;
}
let bytesToWrite = chunk;
const allowableBytes = ((currentView: any): Uint8Array).length - writtenBytes;
if (allowableBytes < bytesToWrite.byteLength) {
if (allowableBytes === 0) {
writeToDestination(destination, (currentView: any));
} else {
((currentView: any): Uint8Array).set(
bytesToWrite.subarray(0, allowableBytes),
writtenBytes,
);
writtenBytes += allowableBytes;
writeToDestination(destination, (currentView: any));
bytesToWrite = bytesToWrite.subarray(allowableBytes);
}
currentView = new Uint8Array(VIEW_SIZE);
writtenBytes = 0;
}
((currentView: any): Uint8Array).set(bytesToWrite, writtenBytes);
writtenBytes += bytesToWrite.byteLength;
if (writtenBytes === VIEW_SIZE) {
writeToDestination(destination, (currentView: any));
currentView = new Uint8Array(VIEW_SIZE);
writtenBytes = 0;
}
}
export function writeChunk(
destination: Destination,
chunk: PrecomputedChunk | Chunk | BinaryChunk,
): void {
if (typeof chunk === 'string') {
writeStringChunk(destination, chunk);
} else {
writeViewChunk(destination, ((chunk: any): PrecomputedChunk | BinaryChunk));
}
}
function writeToDestination(
destination: Destination,
view: string | Uint8Array,
) {
const currentHasCapacity = destination.write(view);
destinationHasCapacity = destinationHasCapacity && currentHasCapacity;
}
export function writeChunkAndReturn(
destination: Destination,
chunk: PrecomputedChunk | Chunk,
): boolean {
writeChunk(destination, chunk);
return destinationHasCapacity;
}
export function completeWriting(destination: Destination) {
if (currentView && writtenBytes > 0) {
destination.write(currentView.subarray(0, writtenBytes));
}
currentView = null;
writtenBytes = 0;
destinationHasCapacity = true;
}
export function close(destination: Destination) {
destination.end();
}
export const textEncoder: TextEncoderType = (new TextEncoder(): any);
export function stringToChunk(content: string): Chunk {
return content;
}
export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
const precomputedChunk = textEncoder.encode(content);
if (__DEV__) {
if (precomputedChunk.byteLength > VIEW_SIZE) {
console.error(
'precomputed chunks must be smaller than the view size configured for this host. This is a bug in React.',
);
}
}
return precomputedChunk;
}
export function typedArrayToBinaryChunk(
content: $ArrayBufferView,
): BinaryChunk {
return new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
}
export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
return typeof chunk === 'string'
? Buffer.byteLength(chunk, 'utf8')
: chunk.byteLength;
}
export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number {
return chunk.byteLength;
}
export function closeWithError(destination: Destination, error: mixed): void {
// $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.
destination.destroy(error);
}
export function createFastHash(_input: string): string | number {
throw new Error('createFastHash is not supported in this environment.');
}
export function readAsDataURL(blob: Blob): Promise<string> {
return blob.arrayBuffer().then(arrayBuffer => {
const encoded = Buffer.from(arrayBuffer).toString('base64');
const mimeType = blob.type || 'application/octet-stream';
return 'data:' + mimeType + ';base64,' + encoded;
});
}
@@ -0,0 +1,239 @@
/**
* 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 {
Thenable,
FulfilledThenable,
RejectedThenable,
ReactDebugInfo,
ReactIOInfo,
ReactAsyncInfo,
} from 'shared/ReactTypes';
import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig';
import {canUseDOM} from 'shared/ExecutionEnvironment';
export type ServerConsumerModuleMap = null;
export type ServerManifest = null;
export type ServerReferenceId = string;
import {prepareDestinationForModuleImpl} from 'react-client/src/ReactFlightClientConfig';
export opaque type ClientReferenceMetadata = {
$$typeof: symbol,
$$id: string,
$$hblp: mixed,
};
// eslint-disable-next-line no-unused-vars
export opaque type ClientReference<T> = {
$$typeof: symbol,
$$id: string,
$$hblp: mixed,
};
export function prepareDestinationForModule(
moduleLoading: ModuleLoading,
nonce: ?string,
metadata: ClientReferenceMetadata,
) {
prepareDestinationForModuleImpl(moduleLoading, metadata.$$hblp, nonce);
}
export function resolveClientReference<T>(
bundlerConfig: ServerConsumerModuleMap,
metadata: ClientReferenceMetadata,
): ClientReference<T> {
return metadata;
}
// Called on the server when decoding a client's action reply (via decodeReply).
// Maps the $$id string sent by the client back to module metadata so the server
// can locate and execute the actual function. $$hblp is null because this runs
// on the server — there is no client module to download.
export function resolveServerReference<T>(
config: ServerManifest,
id: ServerReferenceId,
): ClientReference<T> {
return ({
$$typeof: Symbol.for('react.client.reference'),
$$id: id,
$$hblp: null,
}: any);
}
const asyncModuleCache: Map<string, Thenable<any>> = new Map();
export function preloadModule<T>(
metadata: ClientReference<T>,
): null | Thenable<any> {
if (!canUseDOM) {
// Server environment: modules are synchronously available via require().
return null;
}
// $FlowFixMe[cannot-resolve-module] JSResource is a Meta-internal module
const jsr: any = require('JSResource')(metadata.$$id);
const previouslyLoadedModule = jsr.getModuleIfRequireable();
if (previouslyLoadedModule != null) {
return null;
}
if (metadata.$$hblp != null) {
// Register our updates with the bootloader.
window.Bootloader.handlePayload(metadata.$$hblp);
}
const modulePromise: Thenable<T> = jsr.load();
modulePromise.then(
value => {
const fulfilledThenable: FulfilledThenable<mixed> = (modulePromise: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = value;
},
reason => {
const rejectedThenable: RejectedThenable<mixed> = (modulePromise: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = reason;
},
);
asyncModuleCache.set(metadata.$$id, modulePromise);
return modulePromise;
}
export function requireModule<T>(metadata: ClientReference<T>): T {
if (!canUseDOM) {
// When the Flight client runs on the server to consume a Flight stream,
// modules are resolved synchronously via require() with Haste module names.
const id = metadata.$$id;
const idx = id.lastIndexOf('#');
if (idx !== -1) {
const moduleName = id.slice(0, idx);
const exportName = id.slice(idx + 1);
// Use .call to prevent bundlers from statically resolving this require.
const mod = require.call(null, moduleName); // eslint-disable-line no-useless-call
if (exportName === '' || exportName === 'default') {
return mod.__esModule ? mod.default : mod;
}
return mod[exportName];
}
// Use .call to prevent bundlers from statically resolving this require.
return require.call(null, id); // eslint-disable-line no-useless-call
}
// $FlowFixMe[cannot-resolve-module] JSResource is a Meta-internal module
const jsr: any = require('JSResource')(metadata.$$id);
const moduleExports = jsr.getModuleIfRequireable();
if (moduleExports != null) {
return moduleExports;
}
// Fall back to the async cache if JSResource doesn't have it yet.
const promise: any = asyncModuleCache.get(metadata.$$id);
if (promise && promise.status === 'fulfilled') {
return promise.value;
} else {
throw promise.reason;
}
}
// We cache ReactIOInfo across requests so that inner refreshes can dedupe with outer.
const moduleIOInfoCache: Map<string, ReactIOInfo> = __DEV__
? new Map()
: (null: any);
export function getModuleDebugInfo<T>(
metadata: ClientReference<T>,
): null | ReactDebugInfo {
if (!__DEV__) {
return null;
}
const filename = metadata.$$id;
let ioInfo = moduleIOInfoCache.get(filename);
if (ioInfo === undefined) {
let href;
try {
// $FlowFixMe
href = new URL(filename, document.baseURI).href;
} catch (_) {
href = filename;
}
let start = -1;
let end = -1;
let byteSize = 0;
// $FlowFixMe[method-unbinding]
if (typeof performance.getEntriesByType === 'function') {
// We may be able to collect the start and end time of this resource from Performance Observer.
const resourceEntries = performance.getEntriesByType('resource');
for (let i = 0; i < resourceEntries.length; i++) {
const resourceEntry = resourceEntries[i];
if (resourceEntry.name === href) {
start = resourceEntry.startTime;
end = start + resourceEntry.duration;
// $FlowFixMe[prop-missing]
byteSize = (resourceEntry.transferSize: any) || 0;
}
}
}
const value = Promise.resolve(href);
// $FlowFixMe
value.status = 'fulfilled';
// Is there some more useful representation for the chunk?
// $FlowFixMe
value.value = href;
// Create a fake stack frame that points to the beginning of the chunk. This is
// probably not source mapped so will link to the compiled source rather than
// any individual file that goes into the chunks.
const fakeStack = new Error('react-stack-top-frame');
if (fakeStack.stack.startsWith('Error: react-stack-top-frame')) {
// Looks like V8
fakeStack.stack =
'Error: react-stack-top-frame\n' +
// Add two frames since we always trim one off the top.
' at Client Component Bundle (' +
href +
':1:1)\n' +
' at Client Component Bundle (' +
href +
':1:1)';
} else {
// Looks like Firefox or Safari.
// Add two frames since we always trim one off the top.
fakeStack.stack =
'Client Component Bundle@' +
href +
':1:1\n' +
'Client Component Bundle@' +
href +
':1:1';
}
ioInfo = ({
name: 'script',
start: start,
end: end,
value: value,
debugStack: fakeStack,
}: ReactIOInfo);
if (byteSize > 0) {
// $FlowFixMe[cannot-write]
ioInfo.byteSize = byteSize;
}
moduleIOInfoCache.set(filename, ioInfo);
}
// We could dedupe the async info too but conceptually each request is its own await.
const asyncInfo: ReactAsyncInfo = {
awaited: ioInfo,
};
return [asyncInfo];
}
@@ -0,0 +1,34 @@
/**
* 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
*/
// Custom implementation of FlightClientConfigDOM.
// Resource hint dispatching is a no-op since we handle resource loading
// through our own runtime.
import type {
HintCode,
HintModel,
} from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
export function dispatchHint<Code: HintCode>(
code: Code,
model: HintModel<Code>,
): void {}
export function preinitModuleForSSR(
href: string,
nonce: ?string,
crossOrigin: ?string,
) {}
export function preinitScriptForSSR(
href: string,
nonce: ?string,
crossOrigin: ?string,
) {}
@@ -0,0 +1,18 @@
/**
* 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 type ModuleLoading = null;
export function prepareDestinationForModuleImpl(
moduleLoading: ModuleLoading,
chunks: mixed,
nonce: ?string,
) {
// In the browser we don't need to prepare our destination since the browser is the Destination
}
@@ -0,0 +1,295 @@
/**
* 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 {Thenable} from 'shared/ReactTypes.js';
import type {
DebugChannel,
DebugChannelCallback,
FindSourceMapURLCallback,
Response as FlightResponse,
} from 'react-client/src/ReactFlightClient';
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
import {
createResponse,
createStreamState,
getRoot,
reportGlobalError,
processBinaryChunk,
processStringChunk,
close,
injectIntoDevTools,
} from 'react-client/src/ReactFlightClient';
import {processReply} from 'react-client/src/ReactFlightReplyClient';
export {
createServerReference,
registerServerReference,
} from 'react-client/src/ReactFlightReplyClient';
import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
export type {TemporaryReferenceSet};
type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
export type Options = {
callServer?: CallServerCallback,
debugChannel?: {writable?: WritableStream, readable?: ReadableStream, ...},
temporaryReferences?: TemporaryReferenceSet,
unstable_allowPartialStream?: boolean,
findSourceMapURL?: FindSourceMapURLCallback,
replayConsoleLogs?: boolean,
environmentName?: string,
startTime?: number,
endTime?: number,
};
function createDebugCallbackFromWritableStream(
debugWritable: WritableStream,
): DebugChannelCallback {
const textEncoder = new TextEncoder();
const writer = debugWritable.getWriter();
return message => {
if (message === '') {
writer.close();
} else {
// Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
// Therefore, we can't report errors from this write back to the Response object.
if (__DEV__) {
writer.write(textEncoder.encode(message + '\n')).catch(console.error);
}
}
};
}
function createResponseFromOptions(options: void | Options) {
const debugChannel: void | DebugChannel =
__DEV__ && options && options.debugChannel !== undefined
? {
hasReadable: options.debugChannel.readable !== undefined,
callback:
options.debugChannel.writable !== undefined
? createDebugCallbackFromWritableStream(
options.debugChannel.writable,
)
: null,
}
: undefined;
return createResponse(
null,
null,
null,
options && options.callServer ? options.callServer : undefined,
undefined, // encodeFormAction
undefined, // nonce
options && options.temporaryReferences
? options.temporaryReferences
: undefined,
options && options.unstable_allowPartialStream
? options.unstable_allowPartialStream
: false,
__DEV__ && options && options.findSourceMapURL
? options.findSourceMapURL
: undefined,
__DEV__ ? (options ? options.replayConsoleLogs !== false : true) : false, // defaults to true
__DEV__ && options && options.environmentName
? options.environmentName
: undefined,
__DEV__ && options && options.startTime != null
? options.startTime
: undefined,
__DEV__ && options && options.endTime != null ? options.endTime : undefined,
debugChannel,
);
}
function startReadingFromUniversalStream(
response: FlightResponse,
stream: ReadableStream,
onDone: () => void,
): void {
// This is the same as startReadingFromStream except this allows WebSocketStreams which
// return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
// always allow streams with variable chunk types.
const streamState = createStreamState(response, stream);
const reader = stream.getReader();
function progress({
done,
value,
}: {
done: boolean,
value: any,
...
}): void | Promise<void> {
if (done) {
return onDone();
}
if (value instanceof ArrayBuffer) {
// WebSockets can produce ArrayBuffer values in ReadableStreams.
processBinaryChunk(response, streamState, new Uint8Array(value));
} else if (typeof value === 'string') {
// WebSockets can produce string values in ReadableStreams.
processStringChunk(response, streamState, value);
} else {
processBinaryChunk(response, streamState, value);
}
return reader.read().then(progress).catch(error);
}
function error(e: any) {
reportGlobalError(response, e);
}
reader.read().then(progress).catch(error);
}
function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
onDone: () => void,
debugValue: mixed,
): void {
const streamState = createStreamState(response, debugValue);
const reader = stream.getReader();
function progress({
done,
value,
}: {
done: boolean,
value: ?any,
...
}): void | Promise<void> {
if (done) {
return onDone();
}
const buffer: Uint8Array = (value: any);
processBinaryChunk(response, streamState, buffer);
return reader.read().then(progress).catch(error);
}
function error(e: any) {
reportGlobalError(response, e);
}
reader.read().then(progress).catch(error);
}
function createFromReadableStream<T>(
stream: ReadableStream,
options?: Options,
): Thenable<T> {
const response: FlightResponse = createResponseFromOptions(options);
if (
__DEV__ &&
options &&
options.debugChannel &&
options.debugChannel.readable
) {
let streamDoneCount = 0;
const handleDone = () => {
if (++streamDoneCount === 2) {
close(response);
}
};
startReadingFromUniversalStream(
response,
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, stream, handleDone, stream);
} else {
startReadingFromStream(
response,
stream,
close.bind(null, response),
stream,
);
}
return getRoot(response);
}
function createFromFetch<T>(
promiseForResponse: Promise<Response>,
options?: Options,
): Thenable<T> {
const response: FlightResponse = createResponseFromOptions(options);
promiseForResponse.then(
function (r) {
if (
__DEV__ &&
options &&
options.debugChannel &&
options.debugChannel.readable
) {
let streamDoneCount = 0;
const handleDone = () => {
if (++streamDoneCount === 2) {
close(response);
}
};
startReadingFromUniversalStream(
response,
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, (r.body: any), handleDone, r);
} else {
startReadingFromStream(
response,
(r.body: any),
close.bind(null, response),
r,
);
}
},
function (e) {
reportGlobalError(response, e);
},
);
return getRoot(response);
}
function encodeReply(
value: ReactServerValue,
options?: {temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal},
): Promise<
string | URLSearchParams | FormData,
> /* We don't use URLSearchParams yet but maybe */ {
return new Promise((resolve, reject) => {
const abort = processReply(
value,
'',
options && options.temporaryReferences
? options.temporaryReferences
: undefined,
resolve,
reject,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort((signal: any).reason);
} else {
const listener = () => {
abort((signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
});
}
export {createFromFetch, createFromReadableStream, encodeReply};
if (__DEV__) {
injectIntoDevTools();
}
@@ -0,0 +1,379 @@
/**
* 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 {
Request,
ReactClientValue,
} from 'react-server/src/ReactFlightServer';
import type {Destination} from 'react-server/src/ReactServerStreamConfigNode';
import type {Busboy} from 'busboy';
import type {Writable} from 'stream';
import type {Thenable} from 'shared/ReactTypes';
import type {Duplex} from 'stream';
import {Readable} from 'stream';
import {
createRequest,
startWork,
startFlowing,
startFlowingDebug,
stopFlowing,
abort,
resolveDebugMessage,
closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
createResponse,
reportGlobalError,
close,
resolveField,
resolveFileInfo,
resolveFileChunk,
resolveFileComplete,
getRoot,
} from 'react-server/src/ReactFlightReplyServer';
import {
decodeAction,
decodeFormState,
} from 'react-server/src/ReactFlightActionServer';
export {
registerServerReference,
registerClientReference,
} from '../ReactFlightFBReferences';
// Buffer-based string decoder helpers. The FB server environment does not
// have TextDecoder, so we use Buffer.toString('utf8') instead.
type BufferDecoder = {_pendingBytes: Array<Uint8Array>};
function createStringDecoder(): BufferDecoder {
return {_pendingBytes: []};
}
function readPartialStringChunk(
decoder: BufferDecoder,
buffer: Uint8Array,
): string {
return Buffer.from(
buffer.buffer,
buffer.byteOffset,
buffer.byteLength,
).toString('utf8');
}
function readFinalStringChunk(
decoder: BufferDecoder,
buffer: Uint8Array,
): string {
return Buffer.from(
buffer.buffer,
buffer.byteOffset,
buffer.byteLength,
).toString('utf8');
}
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export type {TemporaryReferenceSet};
function createDrainHandler(destination: Destination, request: Request) {
return () => startFlowing(request, destination);
}
function createCancelHandler(request: Request, reason: string) {
return () => {
stopFlowing(request);
abort(request, new Error(reason));
};
}
function startReadingFromDebugChannelReadable(
request: Request,
stream: Readable | WebSocket,
): void {
const stringDecoder = createStringDecoder();
let lastWasPartial = false;
let stringBuffer = '';
function onData(chunk: string | Uint8Array) {
if (typeof chunk === 'string') {
if (lastWasPartial) {
stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
lastWasPartial = false;
}
stringBuffer += chunk;
} else {
const buffer: Uint8Array = (chunk: any);
stringBuffer += readPartialStringChunk(stringDecoder, buffer);
lastWasPartial = true;
}
const messages = stringBuffer.split('\n');
for (let i = 0; i < messages.length - 1; i++) {
resolveDebugMessage(request, messages[i]);
}
stringBuffer = messages[messages.length - 1];
}
function onError(error: mixed) {
abort(
request,
new Error('Lost connection to the Debug Channel.', {
cause: error,
}),
);
}
function onClose() {
closeDebugChannel(request);
}
if (
// $FlowFixMe[method-unbinding]
typeof stream.addEventListener === 'function' &&
// $FlowFixMe[method-unbinding]
typeof stream.binaryType === 'string'
) {
const ws: WebSocket = (stream: any);
ws.binaryType = 'arraybuffer';
ws.addEventListener('message', event => {
// $FlowFixMe
onData(event.data);
});
ws.addEventListener('error', event => {
// $FlowFixMe
onError(event.error);
});
ws.addEventListener('close', onClose);
} else {
const readable: Readable = (stream: any);
readable.on('data', onData);
readable.on('error', onError);
readable.on('end', onClose);
}
}
type Options = {
debugChannel?: Readable | Writable | Duplex | WebSocket,
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
onError?: (error: mixed) => void,
identifierPrefix?: string,
temporaryReferences?: TemporaryReferenceSet,
startTime?: number,
};
type PipeableStream = {
abort(reason: mixed): void,
pipe<T: Writable>(destination: T): T,
};
function renderToPipeableStream(
model: ReactClientValue,
options?: Options,
): PipeableStream {
const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
const debugChannelReadable: void | Readable | WebSocket =
__DEV__ &&
debugChannel !== undefined &&
// $FlowFixMe[method-unbinding]
(typeof debugChannel.read === 'function' ||
typeof debugChannel.readyState === 'number')
? (debugChannel: any)
: undefined;
const debugChannelWritable: void | Writable =
__DEV__ && debugChannel !== undefined
? // $FlowFixMe[method-unbinding]
typeof debugChannel.write === 'function'
? (debugChannel: any)
: // $FlowFixMe[method-unbinding]
typeof debugChannel.send === 'function'
? createFakeWritableFromWebSocket((debugChannel: any))
: undefined
: undefined;
const request = createRequest(
model,
null,
options ? options.onError : undefined,
options ? options.identifierPrefix : undefined,
options ? options.temporaryReferences : undefined,
options ? options.startTime : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
debugChannelReadable !== undefined,
);
let hasStartedFlowing = false;
startWork(request);
if (debugChannelWritable !== undefined) {
startFlowingDebug(request, debugChannelWritable);
}
if (debugChannelReadable !== undefined) {
startReadingFromDebugChannelReadable(request, debugChannelReadable);
}
return {
pipe<T: Writable>(destination: T): T {
if (hasStartedFlowing) {
throw new Error(
'React currently only supports piping to one writable stream.',
);
}
hasStartedFlowing = true;
startFlowing(request, destination);
destination.on('drain', createDrainHandler(destination, request));
destination.on(
'error',
createCancelHandler(
request,
'The destination stream errored while writing data.',
),
);
// We don't close until the debug channel closes.
if (!__DEV__ || debugChannelReadable === undefined) {
destination.on(
'close',
createCancelHandler(request, 'The destination stream closed early.'),
);
}
return destination;
},
abort(reason: mixed) {
abort(request, reason);
},
};
}
function createFakeWritableFromWebSocket(webSocket: WebSocket): Writable {
return ({
write(chunk: string | Uint8Array) {
webSocket.send((chunk: any));
return true;
},
end() {
webSocket.close();
},
destroy(reason) {
if (typeof reason === 'object' && reason !== null) {
reason = reason.message;
}
if (typeof reason === 'string') {
webSocket.close(1011, reason);
} else {
webSocket.close(1011);
}
},
}: any);
}
function decodeReplyFromBusboy<T>(
busboyStream: Busboy,
options?: {
temporaryReferences?: TemporaryReferenceSet,
arraySizeLimit?: number,
},
): Thenable<T> {
const response = createResponse(
null,
'',
options ? options.temporaryReferences : undefined,
undefined,
options ? options.arraySizeLimit : undefined,
);
let pendingFiles = 0;
const queuedFields: Array<string> = [];
busboyStream.on('field', (name, value) => {
if (pendingFiles > 0) {
// Because the 'end' event fires two microtasks after the next 'field'
// we would resolve files and fields out of order. To handle this properly
// we queue any fields we receive until the previous file is done.
queuedFields.push(name, value);
} else {
try {
resolveField(response, name, value);
} catch (error) {
busboyStream.destroy(error);
}
}
});
busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => {
if (encoding.toLowerCase() === 'base64') {
busboyStream.destroy(
new Error(
"React doesn't accept base64 encoded file uploads because we don't expect " +
"form data passed from a browser to ever encode data that way. If that's " +
'the wrong assumption, we can easily fix it.',
),
);
return;
}
pendingFiles++;
const file = resolveFileInfo(response, name, filename, mimeType);
value.on('data', chunk => {
resolveFileChunk(response, file, chunk);
});
value.on('end', () => {
try {
resolveFileComplete(response, name, file);
pendingFiles--;
if (pendingFiles === 0) {
// Release any queued fields
for (let i = 0; i < queuedFields.length; i += 2) {
resolveField(response, queuedFields[i], queuedFields[i + 1]);
}
queuedFields.length = 0;
}
} catch (error) {
busboyStream.destroy(error);
}
});
});
busboyStream.on('finish', () => {
close(response);
});
busboyStream.on('error', err => {
reportGlobalError(
response,
// $FlowFixMe[incompatible-call] types Error and mixed are incompatible
err,
);
});
return getRoot(response);
}
function decodeReply<T>(
body: string | FormData,
options?: {
temporaryReferences?: TemporaryReferenceSet,
arraySizeLimit?: number,
},
): Thenable<T> {
if (typeof body === 'string') {
const form = new FormData();
form.append('0', body);
body = form;
}
const response = createResponse(
null,
'',
options ? options.temporaryReferences : undefined,
body,
options ? options.arraySizeLimit : undefined,
);
const root = getRoot<T>(response);
close(response);
return root;
}
export {
renderToPipeableStream,
decodeReply,
decodeReplyFromBusboy,
decodeAction,
decodeFormState,
};
@@ -0,0 +1,48 @@
/**
* 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
*/
// Custom implementation of FlightServerConfigDOM.
// Resource hint dispatching is a no-op since we handle resource loading
// through our own runtime.
// We keep the same type definitions so the Flight server protocol is compatible.
// prettier-ignore
type TypeMap = {
'D': string,
'C': string | [string, string],
'L': [string, string] | [string, string, any],
'm': string | [string, any],
'S': string | [string, string] | [string, string | 0, any],
'X': string | [string, any],
'M': string | [string, any],
}
export type HintCode = $Keys<TypeMap>;
export type HintModel<T: HintCode> = TypeMap[T];
export type Hints = Set<string>;
export function createHints(): Hints {
return new Set();
}
export opaque type FormatContext = number;
export function createRootFormatContext(): FormatContext {
return 0;
}
export function getChildFormatContext(
parentContext: FormatContext,
type: string,
props: Object,
): FormatContext {
return parentContext;
}
@@ -0,0 +1,61 @@
/**
* 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 {ReactClientValue} from 'react-server/src/ReactFlightServer';
import type {
ClientReference,
ServerReference,
} from '../ReactFlightFBReferences';
export type {ClientReference, ServerReference};
export type ClientManifest = null;
export type ServerReferenceId = string;
export type ClientReferenceMetadata = ClientReference<any>;
export type ClientReferenceKey = string;
export {isClientReference, isServerReference} from '../ReactFlightFBReferences';
export function getClientReferenceKey(
reference: ClientReference<any>,
): ClientReferenceKey {
return reference.$$id;
}
export function resolveClientReferenceMetadata<T>(
config: ClientManifest,
clientReference: ClientReference<T>,
): ClientReferenceMetadata {
return clientReference;
}
export function getServerReferenceId<T>(
config: ClientManifest,
serverReference: ServerReference<T>,
): ServerReferenceId {
return serverReference.$$id;
}
export function getServerReferenceBoundArguments<T>(
config: ClientManifest,
serverReference: ServerReference<T>,
): null | Array<ReactClientValue> {
return serverReference.$$bound;
}
export function getServerReferenceLocation<T>(
config: ClientManifest,
serverReference: ServerReference<T>,
): void | Error {
return serverReference.$$location;
}
@@ -0,0 +1,18 @@
/**
* 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 {
renderToPipeableStream,
decodeReply,
decodeAction,
decodeFormState,
registerServerReference,
registerClientReference,
createTemporaryReferenceSet,
} from './ReactFlightDOMServerNode';
@@ -0,0 +1,26 @@
/**
* 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 {Request} from 'react-server/src/ReactFlightServer';
import type {ReactComponentInfo} from 'shared/ReactTypes';
export * from 'react-flight-server-fb/src/server/ReactFlightServerConfigFBBundler';
export * from 'react-flight-server-fb/src/server/ReactFlightServerConfigDOMFB';
export const supportsRequestStorage = false;
export const requestStorage: AsyncLocalStorage<Request | void> = (null: any);
export const supportsComponentStorage = false;
export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
(null: any);
export * from '../ReactFlightServerConfigDebugNoop';
export * from '../ReactFlightStackConfigV8';
export * from '../ReactServerConsoleConfigBrowser';
@@ -0,0 +1,26 @@
/**
* 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 {Request} from 'react-server/src/ReactFlightServer';
import type {ReactComponentInfo} from 'shared/ReactTypes';
export * from 'react-flight-server-fb/src/server/ReactFlightServerConfigFBBundler';
export * from 'react-flight-server-fb/src/server/ReactFlightServerConfigDOMFB';
export const supportsRequestStorage = false;
export const requestStorage: AsyncLocalStorage<Request | void> = (null: any);
export const supportsComponentStorage = false;
export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
(null: any);
export * from '../ReactFlightServerConfigDebugNoop';
export * from '../ReactFlightStackConfigV8';
export * from '../ReactServerConsoleConfigServer';
@@ -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-flight-server-fb/src/ReactServerStreamConfigFB';
@@ -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-flight-server-fb/src/ReactServerStreamConfigFB';
+23
View File
@@ -756,6 +756,29 @@ const bundles = [
externals: ['acorn'],
},
/******* React Flight Server FB Server *******/
{
bundleTypes: [FB_WWW_DEV, FB_WWW_PROD],
moduleType: RENDERER,
entry: 'react-flight-server-fb/src/server/react-flight-dom-server.node',
global: 'ReactFlightServer',
condition: 'react-server',
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false,
externals: ['react', 'react-dom', 'stream'],
},
/******* React Flight Client FB *******/
{
bundleTypes: [FB_WWW_DEV, FB_WWW_PROD],
moduleType: RENDERER,
entry: 'react-flight-server-fb/client.browser',
global: 'ReactFlightClient',
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false,
externals: ['react', 'react-dom'],
},
/******* React Server DOM Unbundled Server *******/
{
bundleTypes: [NODE_DEV, NODE_PROD],
+64
View File
@@ -326,6 +326,36 @@ module.exports = [
isFlowTyped: true,
isServerSupported: true,
},
{
shortName: 'dom-browser-fb',
entryPoints: ['react-flight-server-fb/client.browser'],
paths: [
'react-dom',
'react-dom/src/ReactDOMReactServer.js',
'react-dom/client',
'react-dom/profiling',
'react-dom/server',
'react-dom/server.node',
'react-dom-bindings',
'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js',
'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js',
'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js',
'react-flight-server-fb',
'react-flight-server-fb/client',
'react-flight-server-fb/client.browser',
'react-flight-server-fb/src/client/ReactFlightDOMClientBrowser.js', // react-flight-server-fb/client.browser
'react-flight-server-fb/src/server/react-flight-dom-server.node',
'react-flight-server-fb/src/server/ReactFlightDOMServerNode.js', // react-flight-server-fb/src/server/react-flight-dom-server.node
'react-devtools',
'react-devtools-core',
'react-devtools-shell',
'react-devtools-shared',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNoop.js',
],
isFlowTyped: true,
isServerSupported: true,
},
{
shortName: 'dom-browser-turbopack',
entryPoints: [
@@ -564,6 +594,40 @@ module.exports = [
isFlowTyped: true,
isServerSupported: true,
},
{
shortName: 'dom-node-fb',
entryPoints: [
'react-flight-server-fb/src/server/react-flight-dom-server.node',
],
paths: [
'react-dom',
'react-dom/src/ReactDOMReactServer.js',
'react-dom-bindings',
'react-dom/client',
'react-dom/profiling',
'react-dom/server',
'react-dom/server.node',
'react-dom/static',
'react-dom/static.node',
'react-dom/src/server/react-dom-server.node',
'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node
'react-dom/src/server/ReactDOMFizzStaticNode.js',
'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js',
'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js',
'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js',
'react-flight-server-fb',
'react-flight-server-fb/src/server/react-flight-dom-server.node',
'react-flight-server-fb/src/server/ReactFlightDOMServerNode.js', // react-flight-server-fb/src/server/react-flight-dom-server.node
'react-devtools',
'react-devtools-core',
'react-devtools-shell',
'react-devtools-shared',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNoop.js',
],
isFlowTyped: true,
isServerSupported: true,
},
{
shortName: 'dom-legacy',
entryPoints: [