From c17a27ef492d9812351aecdfb017488e8e8404ce Mon Sep 17 00:00:00 2001 From: Andrey Lunyov Date: Mon, 27 Nov 2023 18:34:58 -0500 Subject: [PATCH] FB-specific builds of Flight Server, Flight Client, and React Shared Subset (#27579) This PR adds a new FB-specific configuration of Flight. We also need to bundle a version of ReactSharedSubset that will be used for running Flight on the server. This initial implementation does not support server actions yet. The FB-Flight still uses the text protocol on the server (the flag `enableBinaryFlight` is set to false). It looks like we need some changes in Hermes to properly support this binary format. --- .eslintrc.js | 1 + ...tFlightClientConfig.dom-fb-experimental.js | 14 + .../src/shared/ReactFlightClientConfigDOM.js | 4 +- .../src/ReactFlightClientConfigFBBundler.js | 112 ++++++ .../src/ReactFlightDOMClientFB.js | 91 +++++ .../src/ReactFlightDOMServerFB.js | 68 ++++ .../src/ReactFlightReferencesFB.js | 98 +++++ .../src/ReactFlightServerConfigFBBundler.js | 36 ++ .../ReactFlightDOMServerFB-test.internal.js | 364 ++++++++++++++++++ .../src/ReactServerStreamConfigFB.js | 4 - ...tFlightServerConfig.dom-fb-experimental.js | 16 + ...tServerStreamConfig.dom-fb-experimental.js | 83 ++++ .../forks/ReactServerStreamConfig.dom-fb.js | 4 + packages/react/src/ReactSharedSubsetFB.js | 11 + .../shared/forks/ReactFeatureFlags.www.js | 2 +- scripts/rollup/bundles.js | 33 ++ scripts/rollup/forks.js | 5 +- scripts/shared/inlinedHostConfigs.js | 23 +- 18 files changed, 960 insertions(+), 9 deletions(-) create mode 100644 packages/react-client/src/forks/ReactFlightClientConfig.dom-fb-experimental.js create mode 100644 packages/react-server-dom-fb/src/ReactFlightClientConfigFBBundler.js create mode 100644 packages/react-server-dom-fb/src/ReactFlightDOMClientFB.js create mode 100644 packages/react-server-dom-fb/src/ReactFlightDOMServerFB.js create mode 100644 packages/react-server-dom-fb/src/ReactFlightReferencesFB.js create mode 100644 packages/react-server-dom-fb/src/ReactFlightServerConfigFBBundler.js create mode 100644 packages/react-server-dom-fb/src/__tests__/ReactFlightDOMServerFB-test.internal.js create mode 100644 packages/react-server/src/forks/ReactFlightServerConfig.dom-fb-experimental.js create mode 100644 packages/react-server/src/forks/ReactServerStreamConfig.dom-fb-experimental.js create mode 100644 packages/react/src/ReactSharedSubsetFB.js diff --git a/.eslintrc.js b/.eslintrc.js index 9d88915811..29ffa94255 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -327,6 +327,7 @@ module.exports = { 'packages/react-server-dom-esm/**/*.js', 'packages/react-server-dom-webpack/**/*.js', 'packages/react-server-dom-turbopack/**/*.js', + 'packages/react-server-dom-fb/**/*.js', 'packages/react-test-renderer/**/*.js', 'packages/react-debug-tools/**/*.js', 'packages/react-devtools-extensions/**/*.js', diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-fb-experimental.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-fb-experimental.js new file mode 100644 index 0000000000..9c6ee84ba6 --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-fb-experimental.js @@ -0,0 +1,14 @@ +/** + * 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/ReactFlightClientConfigBrowser'; +export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; +export * from 'react-server-dom-fb/src/ReactFlightClientConfigFBBundler'; + +export const usedWithSSR = false; diff --git a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js index 3ef89a47f9..18ef25b068 100644 --- a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js +++ b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js @@ -8,7 +8,7 @@ */ // This client file is in the shared folder because it applies to both SSR and browser contexts. -// It is the configuraiton of the FlightClient behavior which can run in either environment. +// It is the configuration of the FlightClient behavior which can run in either environment. import type {HintCode, HintModel} from '../server/ReactFlightServerConfigDOM'; @@ -107,7 +107,7 @@ export function dispatchHint( } } -// Flow is having troulbe refining the HintModels so we help it a bit. +// Flow is having trouble refining the HintModels so we help it a bit. // This should be compiled out in the production build. function refineModel(code: T, model: HintModel): HintModel { return model; diff --git a/packages/react-server-dom-fb/src/ReactFlightClientConfigFBBundler.js b/packages/react-server-dom-fb/src/ReactFlightClientConfigFBBundler.js new file mode 100644 index 0000000000..443f513dc6 --- /dev/null +++ b/packages/react-server-dom-fb/src/ReactFlightClientConfigFBBundler.js @@ -0,0 +1,112 @@ +/** + * 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, +} from 'shared/ReactTypes'; + +export type ModuleLoading = mixed; + +type ResolveClientReferenceFn = + ClientReferenceMetadata => ClientReference; + +export type SSRModuleMap = { + resolveClientReference?: ResolveClientReferenceFn, +}; +export type ServerManifest = string; +export type { + ClientManifest, + ServerReferenceId, + ClientReferenceMetadata, +} from './ReactFlightReferencesFB'; + +import type { + ServerReferenceId, + ClientReferenceMetadata, +} from './ReactFlightReferencesFB'; + +export type ClientReference = { + getModuleId: () => string, + load: () => Thenable, +}; + +export function prepareDestinationForModule( + moduleLoading: ModuleLoading, + nonce: ?string, + metadata: ClientReferenceMetadata, +) { + return; +} + +export function resolveClientReference( + moduleMap: SSRModuleMap, + metadata: ClientReferenceMetadata, +): ClientReference { + if (typeof moduleMap.resolveClientReference === 'function') { + return moduleMap.resolveClientReference(metadata); + } else { + throw new Error( + 'Expected `resolveClientReference` to be defined on the moduleMap.', + ); + } +} + +export function resolveServerReference( + config: ServerManifest, + id: ServerReferenceId, +): ClientReference { + throw new Error('Not implemented'); +} + +const asyncModuleCache: Map> = new Map(); + +export function preloadModule( + clientReference: ClientReference, +): null | Thenable { + const existingPromise = asyncModuleCache.get(clientReference.getModuleId()); + if (existingPromise) { + if (existingPromise.status === 'fulfilled') { + return null; + } + return existingPromise; + } else { + const modulePromise: Thenable = clientReference.load(); + modulePromise.then( + value => { + const fulfilledThenable: FulfilledThenable = + (modulePromise: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = value; + }, + reason => { + const rejectedThenable: RejectedThenable = (modulePromise: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = reason; + }, + ); + asyncModuleCache.set(clientReference.getModuleId(), modulePromise); + return modulePromise; + } +} + +export function requireModule(clientReference: ClientReference): T { + let module; + // We assume that preloadModule has been called before, which + // should have added something to the module cache. + const promise: any = asyncModuleCache.get(clientReference.getModuleId()); + if (promise.status === 'fulfilled') { + module = promise.value; + } else { + throw promise.reason; + } + // We are currently only support default exports for client components + return module; +} diff --git a/packages/react-server-dom-fb/src/ReactFlightDOMClientFB.js b/packages/react-server-dom-fb/src/ReactFlightDOMClientFB.js new file mode 100644 index 0000000000..de3b84094a --- /dev/null +++ b/packages/react-server-dom-fb/src/ReactFlightDOMClientFB.js @@ -0,0 +1,91 @@ +/** + * 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 {enableBinaryFlight} from 'shared/ReactFeatureFlags'; +import type {Thenable} from 'shared/ReactTypes'; +import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient'; + +import { + createResponse, + getRoot, + reportGlobalError, + processBinaryChunk, + close, +} from 'react-client/src/ReactFlightClient'; + +import type {SSRModuleMap} from './ReactFlightClientConfigFBBundler'; + +type Options = { + moduleMap: SSRModuleMap, +}; + +function createResponseFromOptions(options: void | Options) { + const moduleMap = options && options.moduleMap; + if (moduleMap == null) { + throw new Error('Expected `moduleMap` to be defined.'); + } + + return createResponse(moduleMap, null, undefined, undefined); +} + +function processChunk(response: FlightResponse, chunk: string | Uint8Array) { + if (enableBinaryFlight) { + if (typeof chunk === 'string') { + throw new Error( + '`enableBinaryFlight` flag is enabled, expected a Uint8Array as input, got string.', + ); + } + } + const buffer = typeof chunk !== 'string' ? chunk : encodeString(chunk); + + processBinaryChunk(response, buffer); +} + +function encodeString(string: string) { + const textEncoder = new TextEncoder(); + return textEncoder.encode(string); +} + +function startReadingFromStream( + response: FlightResponse, + stream: ReadableStream, +): void { + const reader = stream.getReader(); + function progress({ + done, + value, + }: { + done: boolean, + value: ?any, + ... + }): void | Promise { + if (done) { + close(response); + return; + } + const buffer: Uint8Array = (value: any); + processChunk(response, buffer); + return reader.read().then(progress).catch(error); + } + function error(e: any) { + reportGlobalError(response, e); + } + reader.read().then(progress).catch(error); +} + +function createFromReadableStream( + stream: ReadableStream, + options?: Options, +): Thenable { + const response: FlightResponse = createResponseFromOptions(options); + startReadingFromStream(response, stream); + return getRoot(response); +} + +export {createFromReadableStream}; diff --git a/packages/react-server-dom-fb/src/ReactFlightDOMServerFB.js b/packages/react-server-dom-fb/src/ReactFlightDOMServerFB.js new file mode 100644 index 0000000000..a0ab868688 --- /dev/null +++ b/packages/react-server-dom-fb/src/ReactFlightDOMServerFB.js @@ -0,0 +1,68 @@ +/** + * 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 { + Destination, + Chunk, + PrecomputedChunk, +} from 'react-server/src/ReactServerStreamConfig'; +import type {ClientManifest} from './ReactFlightReferencesFB'; + +import { + createRequest, + startWork, + startFlowing, +} from 'react-server/src/ReactFlightServer'; + +import {setByteLengthOfChunkImplementation} from 'react-server/src/ReactServerStreamConfig'; + +export { + registerClientReference, + registerServerReference, + getRequestedClientReferencesKeys, + clearRequestedClientReferencesKeysSet, +} from './ReactFlightReferencesFB'; + +type Options = { + onError?: (error: mixed) => void, +}; + +function renderToDestination( + destination: Destination, + model: ReactClientValue, + bundlerConfig: ClientManifest, + options?: Options, +): void { + if (!configured) { + throw new Error( + 'Please make sure to call `setConfig(...)` before calling `renderToDestination`.', + ); + } + const request = createRequest( + model, + bundlerConfig, + options ? options.onError : undefined, + ); + startWork(request); + startFlowing(request, destination); +} + +type Config = { + byteLength: (chunk: Chunk | PrecomputedChunk) => number, +}; + +let configured = false; + +function setConfig(config: Config): void { + setByteLengthOfChunkImplementation(config.byteLength); + configured = true; +} + +export {renderToDestination, setConfig}; diff --git a/packages/react-server-dom-fb/src/ReactFlightReferencesFB.js b/packages/react-server-dom-fb/src/ReactFlightReferencesFB.js new file mode 100644 index 0000000000..27e51d06c1 --- /dev/null +++ b/packages/react-server-dom-fb/src/ReactFlightReferencesFB.js @@ -0,0 +1,98 @@ +/** + * 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 opaque type ClientManifest = mixed; + +// eslint-disable-next-line no-unused-vars +export type ServerReference = string; + +// eslint-disable-next-line no-unused-vars +export type ClientReference = string; + +const registeredClientReferences = new Map(); +const requestedClientReferencesKeys = new Set(); + +export type ClientReferenceKey = string; +export type ClientReferenceMetadata = { + moduleId: ClientReferenceKey, + exportName: string, +}; + +export type ServerReferenceId = string; + +export function registerClientReference( + clientReference: ClientReference, + moduleId: ClientReferenceKey, +): ClientReference { + const exportName = 'default'; // Currently, we only support modules with `default` export + registeredClientReferences.set(clientReference, { + moduleId, + exportName, + }); + + return clientReference; +} + +export function isClientReference(reference: T): boolean { + return registeredClientReferences.has(reference); +} + +export function getClientReferenceKey( + clientReference: ClientReference, +): ClientReferenceKey { + const reference = registeredClientReferences.get(clientReference); + if (reference != null) { + requestedClientReferencesKeys.add(reference.moduleId); + return reference.moduleId; + } + + throw new Error( + 'Expected client reference ' + clientReference + ' to be registered.', + ); +} + +export function resolveClientReferenceMetadata( + config: ClientManifest, + clientReference: ClientReference, +): ClientReferenceMetadata { + const metadata = registeredClientReferences.get(clientReference); + if (metadata != null) { + return metadata; + } + + throw new Error( + 'Expected client reference ' + clientReference + ' to be registered.', + ); +} + +export function registerServerReference( + serverReference: ServerReference, + exportName: string, +): ServerReference { + throw new Error('registerServerReference: Not Implemented.'); +} + +export function isServerReference(reference: T): boolean { + throw new Error('isServerReference: Not Implemented.'); +} + +export function getServerReferenceId( + config: ClientManifest, + serverReference: ServerReference, +): ServerReferenceId { + throw new Error('getServerReferenceId: Not Implemented.'); +} + +export function getRequestedClientReferencesKeys(): $ReadOnlyArray { + return Array.from(requestedClientReferencesKeys); +} + +export function clearRequestedClientReferencesKeysSet(): void { + requestedClientReferencesKeys.clear(); +} diff --git a/packages/react-server-dom-fb/src/ReactFlightServerConfigFBBundler.js b/packages/react-server-dom-fb/src/ReactFlightServerConfigFBBundler.js new file mode 100644 index 0000000000..ee0c08d1db --- /dev/null +++ b/packages/react-server-dom-fb/src/ReactFlightServerConfigFBBundler.js @@ -0,0 +1,36 @@ +/** + * 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 { + ClientManifest, + ClientReference, + ServerReference, +} from './ReactFlightReferencesFB'; + +export type {ClientManifest, ClientReference, ServerReference}; + +export { + ClientReferenceKey, + ClientReferenceMetadata, + getClientReferenceKey, + isClientReference, + resolveClientReferenceMetadata, + isServerReference, + ServerReferenceId, + getServerReferenceId, +} from './ReactFlightReferencesFB'; + +export function getServerReferenceBoundArguments( + config: ClientManifest, + serverReference: ServerReference, +): null | Array { + throw new Error('getServerReferenceBoundArguments: Not Implemented.'); +} diff --git a/packages/react-server-dom-fb/src/__tests__/ReactFlightDOMServerFB-test.internal.js b/packages/react-server-dom-fb/src/__tests__/ReactFlightDOMServerFB-test.internal.js new file mode 100644 index 0000000000..d83ef8aa0b --- /dev/null +++ b/packages/react-server-dom-fb/src/__tests__/ReactFlightDOMServerFB-test.internal.js @@ -0,0 +1,364 @@ +/** + * 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. + * + * @emails react-core + */ + +'use strict'; + +// Polyfills for test environment +global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; +global.TextEncoder = require('util').TextEncoder; +global.TextDecoder = require('util').TextDecoder; + +// Don't wait before processing work on the server. +// TODO: we can replace this with FlightServer.act(). +global.setImmediate = cb => cb(); + +let act; +let use; +let clientExports; +let moduleMap; +let React; +let ReactDOMClient; +let ReactServerDOMServer; +let ReactServerDOMClient; +let Suspense; +let registerClientReference; + +class Destination { + #buffer = ''; + #controller = null; + constructor() { + const self = this; + this.stream = new ReadableStream({ + start(controller) { + self.#controller = controller; + }, + }); + } + write(chunk) { + this.#buffer += chunk; + } + beginWriting() {} + completeWriting() {} + flushBuffered() { + if (!this.#controller) { + throw new Error('Expected a controller.'); + } + this.#controller.enqueue(this.#buffer); + this.#buffer = ''; + } + close() {} + onError() {} +} + +describe('ReactFlightDOM for FB', () => { + beforeEach(() => { + // For this first reset we are going to load the dom-node version of react-server-dom-turbopack/server + // This can be thought of as essentially being the React Server Components scope with react-server + // condition + jest.resetModules(); + registerClientReference = + require('../ReactFlightReferencesFB').registerClientReference; + + jest.mock('react', () => require('react/src/ReactSharedSubsetFB')); + + jest.mock('shared/ReactFeatureFlags', () => { + jest.mock( + 'ReactFeatureFlags', + () => jest.requireActual('shared/forks/ReactFeatureFlags.www-dynamic'), + {virtual: true}, + ); + return jest.requireActual('shared/forks/ReactFeatureFlags.www'); + }); + + clientExports = value => { + registerClientReference(value, value.name); + return value; + }; + + moduleMap = { + resolveClientReference(metadata) { + throw new Error('Do not expect to load client components.'); + }, + }; + + ReactServerDOMServer = require('../ReactFlightDOMServerFB'); + ReactServerDOMServer.setConfig({ + byteLength: str => Buffer.byteLength(str), + }); + + // This reset is to load modules for the SSR/Browser scope. + jest.resetModules(); + __unmockReact(); + act = require('internal-test-utils').act; + React = require('react'); + use = React.use; + Suspense = React.Suspense; + ReactDOMClient = require('react-dom/client'); + ReactServerDOMClient = require('../ReactFlightDOMClientFB'); + }); + + it('should resolve HTML with renderToDestination', async () => { + function Text({children}) { + return {children}; + } + function HTML() { + return ( +
+ hello + world +
+ ); + } + + function App() { + const model = { + html: , + }; + return model; + } + const destination = new Destination(); + ReactServerDOMServer.renderToDestination(destination, ); + const response = ReactServerDOMClient.createFromReadableStream( + destination.stream, + { + moduleMap, + }, + ); + const model = await response; + expect(model).toEqual({ + html: ( +
+ hello + world +
+ ), + }); + }); + + it('should resolve the root', async () => { + // Model + function Text({children}) { + return {children}; + } + function HTML() { + return ( +
+ hello + world +
+ ); + } + function RootModel() { + return { + html: , + }; + } + + // View + function Message({response}) { + return
{use(response).html}
; + } + function App({response}) { + return ( + Loading...}> + + + ); + } + + const destination = new Destination(); + ReactServerDOMServer.renderToDestination(destination, ); + const response = ReactServerDOMClient.createFromReadableStream( + destination.stream, + { + moduleMap, + }, + ); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe( + '
helloworld
', + ); + }); + + it('should not get confused by $', async () => { + // Model + function RootModel() { + return {text: '$1'}; + } + + // View + function Message({response}) { + return

{use(response).text}

; + } + function App({response}) { + return ( + Loading...}> + + + ); + } + const destination = new Destination(); + ReactServerDOMServer.renderToDestination(destination, ); + const response = ReactServerDOMClient.createFromReadableStream( + destination.stream, + { + moduleMap, + }, + ); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

$1

'); + }); + + it('should not get confused by @', async () => { + // Model + function RootModel() { + return {text: '@div'}; + } + + // View + function Message({response}) { + return

{use(response).text}

; + } + function App({response}) { + return ( + Loading...}> + + + ); + } + const destination = new Destination(); + ReactServerDOMServer.renderToDestination(destination, ); + const response = ReactServerDOMClient.createFromReadableStream( + destination.stream, + { + moduleMap, + }, + ); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

@div

'); + }); + + it('should be able to render a client component', async () => { + const Component = function ({greeting}) { + return greeting + ' World'; + }; + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const ClientComponent = clientExports(Component); + + const destination = new Destination(); + ReactServerDOMServer.renderToDestination( + destination, + , + moduleMap, + ); + const response = ReactServerDOMClient.createFromReadableStream( + destination.stream, + { + moduleMap: { + resolveClientReference(metadata) { + return { + getModuleId() { + return metadata.moduleId; + }, + load() { + return Promise.resolve(Component); + }, + }; + }, + }, + }, + ); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

Hello World

'); + }); + + it('should render long strings', async () => { + // Model + const longString = 'Lorem Ipsum ❤️ '.repeat(100); + + function RootModel() { + return {text: longString}; + } + + // View + function Message({response}) { + return

{use(response).text}

; + } + function App({response}) { + return ( + Loading...}> + + + ); + } + const destination = new Destination(); + ReactServerDOMServer.renderToDestination(destination, ); + const response = ReactServerDOMClient.createFromReadableStream( + destination.stream, + { + moduleMap, + }, + ); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

' + longString + '

'); + }); + + // TODO: `registerClientComponent` need to be able to support this + it.skip('throws when accessing a member below the client exports', () => { + const ClientModule = clientExports({ + Component: {deep: 'thing'}, + }); + function dotting() { + return ClientModule.Component.deep; + } + expect(dotting).toThrowError( + 'Cannot access Component.deep on the server. ' + + 'You cannot dot into a client module from a server component. ' + + 'You can only pass the imported name through.', + ); + }); +}); diff --git a/packages/react-server/src/ReactServerStreamConfigFB.js b/packages/react-server/src/ReactServerStreamConfigFB.js index 781b6a2860..5dfdde467f 100644 --- a/packages/react-server/src/ReactServerStreamConfigFB.js +++ b/packages/react-server/src/ReactServerStreamConfigFB.js @@ -18,10 +18,6 @@ export opaque type PrecomputedChunk = string; export opaque type Chunk = string; export opaque type BinaryChunk = string; -export function scheduleWork(callback: () => void) { - // We don't schedule work in this model, and instead expect performWork to always be called repeatedly. -} - export function flushBuffered(destination: Destination) {} export const supportsRequestStorage = false; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-fb-experimental.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-fb-experimental.js new file mode 100644 index 0000000000..0c265c7c11 --- /dev/null +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-fb-experimental.js @@ -0,0 +1,16 @@ +/** + * 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'; + +export * from 'react-server-dom-fb/src/ReactFlightServerConfigFBBundler'; +export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactServerStreamConfig.dom-fb-experimental.js b/packages/react-server/src/forks/ReactServerStreamConfig.dom-fb-experimental.js new file mode 100644 index 0000000000..03cc3e1b82 --- /dev/null +++ b/packages/react-server/src/forks/ReactServerStreamConfig.dom-fb-experimental.js @@ -0,0 +1,83 @@ +/** + * 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 '../ReactServerStreamConfigFB'; + +import type { + PrecomputedChunk, + Chunk, + BinaryChunk, +} from '../ReactServerStreamConfigFB'; + +let byteLengthImpl: null | ((chunk: Chunk | PrecomputedChunk) => number) = null; + +export function setByteLengthOfChunkImplementation( + impl: (chunk: Chunk | PrecomputedChunk) => number, +): void { + byteLengthImpl = impl; +} + +export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number { + if (byteLengthImpl == null) { + // eslint-disable-next-line react-internal/prod-error-codes + throw new Error( + 'byteLengthOfChunk implementation is not configured. Please, provide the implementation via ReactFlightDOMServer.setConfig(...);', + ); + } + return byteLengthImpl(chunk); +} + +export interface Destination { + beginWriting(): void; + write(chunk: Chunk | PrecomputedChunk | BinaryChunk): void; + completeWriting(): void; + flushBuffered(): void; + close(): void; + onError(error: mixed): void; +} + +export function scheduleWork(callback: () => void) { + callback(); +} + +export function beginWriting(destination: Destination) { + destination.beginWriting(); +} + +export function writeChunk( + destination: Destination, + chunk: Chunk | PrecomputedChunk | BinaryChunk, +): void { + destination.write(chunk); +} + +export function writeChunkAndReturn( + destination: Destination, + chunk: Chunk | PrecomputedChunk | BinaryChunk, +): boolean { + destination.write(chunk); + return true; +} + +export function completeWriting(destination: Destination) { + destination.completeWriting(); +} + +export function flushBuffered(destination: Destination) { + destination.flushBuffered(); +} + +export function close(destination: Destination) { + destination.close(); +} + +export function closeWithError(destination: Destination, error: mixed): void { + destination.onError(error); + destination.close(); +} diff --git a/packages/react-server/src/forks/ReactServerStreamConfig.dom-fb.js b/packages/react-server/src/forks/ReactServerStreamConfig.dom-fb.js index f90c759338..e15f680867 100644 --- a/packages/react-server/src/forks/ReactServerStreamConfig.dom-fb.js +++ b/packages/react-server/src/forks/ReactServerStreamConfig.dom-fb.js @@ -8,3 +8,7 @@ */ export * from '../ReactServerStreamConfigFB'; + +export function scheduleWork(callback: () => void) { + // We don't schedule work in this model, and instead expect performWork to always be called repeatedly. +} diff --git a/packages/react/src/ReactSharedSubsetFB.js b/packages/react/src/ReactSharedSubsetFB.js new file mode 100644 index 0000000000..b904cfc0e9 --- /dev/null +++ b/packages/react/src/ReactSharedSubsetFB.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './ReactSharedSubset'; +export {jsx, jsxs, jsxDEV} from './jsx/ReactJSX'; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 01ae3eae59..9f7724d077 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -75,7 +75,7 @@ export const enableFetchInstrumentation = false; export const enableFormActions = false; -export const enableBinaryFlight = true; +export const enableBinaryFlight = false; export const enableTaint = false; export const enablePostpone = false; diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 31d15ecc7b..0ce6d052f8 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -104,6 +104,17 @@ const bundles = [ externals: [], }, + /******* Isomorphic Shared Subset for FB *******/ + { + bundleTypes: __EXPERIMENTAL__ ? [FB_WWW_DEV, FB_WWW_PROD] : [], + moduleType: ISOMORPHIC, + entry: 'react/src/ReactSharedSubsetFB.js', + global: 'ReactSharedSubset', + minifyWithProdErrorCodes: true, + wrapWithModuleBoundaries: false, + externals: [], + }, + /******* React JSX Runtime *******/ { bundleTypes: [ @@ -574,6 +585,28 @@ const bundles = [ externals: ['acorn'], }, + /******* React Server DOM FB Server *******/ + { + bundleTypes: __EXPERIMENTAL__ ? [FB_WWW_DEV, FB_WWW_PROD] : [], + moduleType: RENDERER, + entry: 'react-server-dom-fb/src/ReactFlightDOMServerFB.js', + global: 'ReactFlightDOMServer', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom'], + }, + + /******* React Server DOM FB Client *******/ + { + bundleTypes: __EXPERIMENTAL__ ? [FB_WWW_DEV, FB_WWW_PROD] : [], + moduleType: RENDERER, + entry: 'react-server-dom-fb/src/ReactFlightDOMClientFB.js', + global: 'ReactFlightDOMClient', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom'], + }, + /******* React Suspense Test Utils *******/ { bundleTypes: [NODE_ES2015], diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js index 22d7d4c3e9..24ba7c2a86 100644 --- a/scripts/rollup/forks.js +++ b/scripts/rollup/forks.js @@ -63,7 +63,10 @@ const forks = Object.freeze({ if (entry === 'react') { return './packages/react/src/ReactSharedInternalsClient.js'; } - if (entry === 'react/src/ReactSharedSubset.js') { + if ( + entry === 'react/src/ReactSharedSubset.js' || + entry === 'react/src/ReactSharedSubsetFB.js' + ) { return './packages/react/src/ReactSharedInternalsServer.js'; } if (!entry.startsWith('react/') && dependencies.indexOf('react') === -1) { diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 6f2ba535a2..adbc4bdabe 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -396,13 +396,34 @@ module.exports = [ 'react-dom', 'react-dom/src/ReactDOMSharedSubset.js', 'react-dom-bindings', - 'react-server-dom-fb', + 'react-server-dom-fb/src/ReactDOMServerFB.js', 'shared/ReactDOMSharedInternals', ], isFlowTyped: true, isServerSupported: true, isFlightSupported: false, }, + { + shortName: 'dom-fb-experimental', + entryPoints: [ + 'react-server-dom-fb/src/ReactFlightDOMClientFB.js', + 'react-server-dom-fb/src/ReactFlightDOMServerFB.js', + ], + paths: [ + 'react-dom', + 'react-dom-bindings', + 'react-server-dom-fb/src/ReactFlightClientConfigFBBundler.js', + 'react-server-dom-fb/src/ReactFlightClientConfigFBBundler.js', + 'react-server-dom-fb/src/ReactFlightReferencesFB.js', + 'react-server-dom-fb/src/ReactFlightServerConfigFBBundler.js', + 'react-server-dom-fb/src/ReactFlightDOMClientFB.js', + 'react-server-dom-fb/src/ReactFlightDOMServerFB.js', + 'shared/ReactDOMSharedInternals', + ], + isFlowTyped: true, + isServerSupported: true, + isFlightSupported: true, + }, { shortName: 'native', entryPoints: ['react-native-renderer'],