Files
react/packages/react-flight-server-fb/src/client/ReactFlightClientConfigBundlerFB.js
T
Xinzi ZhouandGitHub 4c56e37e58 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.
2026-04-27 09:48:37 -07:00

240 lines
7.2 KiB
JavaScript

/**
* 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];
}