@samitouri / QOS-React-2 / commits / c17a27ef49

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.

Andrey Lunyov committed Nov 27, 2023 at 18:34 UTC c17a27ef492d9812351aecdfb017488e8e8404ce
18 files changed +960 -9
.eslintrc.js
+1
@@ -327,6 +327,7 @@ module.exports = {
327 'packages/react-server-dom-esm/**/*.js',
328 'packages/react-server-dom-webpack/**/*.js',
329 'packages/react-server-dom-turbopack/**/*.js',
330 + 'packages/react-server-dom-fb/**/*.js',
331 'packages/react-test-renderer/**/*.js',
332 'packages/react-debug-tools/**/*.js',
333 'packages/react-devtools-extensions/**/*.js',
packages/react-client/src/forks/ReactFlightClientConfig.dom-fb-experimental.js new
+14
@@ -0,0 +1,14 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export * from 'react-client/src/ReactFlightClientConfigBrowser';
11 +export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
12 +export * from 'react-server-dom-fb/src/ReactFlightClientConfigFBBundler';
13 +
14 +export const usedWithSSR = false;
packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js
+2 -2
@@ -8,7 +8,7 @@
8 */
9
10 // This client file is in the shared folder because it applies to both SSR and browser contexts.
11 -// It is the configuraiton of the FlightClient behavior which can run in either environment.
11 +// It is the configuration of the FlightClient behavior which can run in either environment.
12
13 import type {HintCode, HintModel} from '../server/ReactFlightServerConfigDOM';
14
@@ -107,7 +107,7 @@ export function dispatchHint<Code: HintCode>(
107 }
108 }
109
110 -// Flow is having troulbe refining the HintModels so we help it a bit.
110 +// Flow is having trouble refining the HintModels so we help it a bit.
111 // This should be compiled out in the production build.
112 function refineModel<T>(code: T, model: HintModel<any>): HintModel<T> {
113 return model;
packages/react-server-dom-fb/src/ReactFlightClientConfigFBBundler.js new
+112
@@ -0,0 +1,112 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {
11 + Thenable,
12 + FulfilledThenable,
13 + RejectedThenable,
14 +} from 'shared/ReactTypes';
15 +
16 +export type ModuleLoading = mixed;
17 +
18 +type ResolveClientReferenceFn<T> =
19 + ClientReferenceMetadata => ClientReference<T>;
20 +
21 +export type SSRModuleMap = {
22 + resolveClientReference?: ResolveClientReferenceFn<any>,
23 +};
24 +export type ServerManifest = string;
25 +export type {
26 + ClientManifest,
27 + ServerReferenceId,
28 + ClientReferenceMetadata,
29 +} from './ReactFlightReferencesFB';
30 +
31 +import type {
32 + ServerReferenceId,
33 + ClientReferenceMetadata,
34 +} from './ReactFlightReferencesFB';
35 +
36 +export type ClientReference<T> = {
37 + getModuleId: () => string,
38 + load: () => Thenable<T>,
39 +};
40 +
41 +export function prepareDestinationForModule(
42 + moduleLoading: ModuleLoading,
43 + nonce: ?string,
44 + metadata: ClientReferenceMetadata,
45 +) {
46 + return;
47 +}
48 +
49 +export function resolveClientReference<T>(
50 + moduleMap: SSRModuleMap,
51 + metadata: ClientReferenceMetadata,
52 +): ClientReference<T> {
53 + if (typeof moduleMap.resolveClientReference === 'function') {
54 + return moduleMap.resolveClientReference(metadata);
55 + } else {
56 + throw new Error(
57 + 'Expected `resolveClientReference` to be defined on the moduleMap.',
58 + );
59 + }
60 +}
61 +
62 +export function resolveServerReference<T>(
63 + config: ServerManifest,
64 + id: ServerReferenceId,
65 +): ClientReference<T> {
66 + throw new Error('Not implemented');
67 +}
68 +
69 +const asyncModuleCache: Map<string, Thenable<any>> = new Map();
70 +
71 +export function preloadModule<T>(
72 + clientReference: ClientReference<T>,
73 +): null | Thenable<any> {
74 + const existingPromise = asyncModuleCache.get(clientReference.getModuleId());
75 + if (existingPromise) {
76 + if (existingPromise.status === 'fulfilled') {
77 + return null;
78 + }
79 + return existingPromise;
80 + } else {
81 + const modulePromise: Thenable<T> = clientReference.load();
82 + modulePromise.then(
83 + value => {
84 + const fulfilledThenable: FulfilledThenable<mixed> =
85 + (modulePromise: any);
86 + fulfilledThenable.status = 'fulfilled';
87 + fulfilledThenable.value = value;
88 + },
89 + reason => {
90 + const rejectedThenable: RejectedThenable<mixed> = (modulePromise: any);
91 + rejectedThenable.status = 'rejected';
92 + rejectedThenable.reason = reason;
93 + },
94 + );
95 + asyncModuleCache.set(clientReference.getModuleId(), modulePromise);
96 + return modulePromise;
97 + }
98 +}
99 +
100 +export function requireModule<T>(clientReference: ClientReference<T>): T {
101 + let module;
102 + // We assume that preloadModule has been called before, which
103 + // should have added something to the module cache.
104 + const promise: any = asyncModuleCache.get(clientReference.getModuleId());
105 + if (promise.status === 'fulfilled') {
106 + module = promise.value;
107 + } else {
108 + throw promise.reason;
109 + }
110 + // We are currently only support default exports for client components
111 + return module;
112 +}
packages/react-server-dom-fb/src/ReactFlightDOMClientFB.js new
+91
@@ -0,0 +1,91 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import {enableBinaryFlight} from 'shared/ReactFeatureFlags';
11 +import type {Thenable} from 'shared/ReactTypes';
12 +import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
13 +
14 +import {
15 + createResponse,
16 + getRoot,
17 + reportGlobalError,
18 + processBinaryChunk,
19 + close,
20 +} from 'react-client/src/ReactFlightClient';
21 +
22 +import type {SSRModuleMap} from './ReactFlightClientConfigFBBundler';
23 +
24 +type Options = {
25 + moduleMap: SSRModuleMap,
26 +};
27 +
28 +function createResponseFromOptions(options: void | Options) {
29 + const moduleMap = options && options.moduleMap;
30 + if (moduleMap == null) {
31 + throw new Error('Expected `moduleMap` to be defined.');
32 + }
33 +
34 + return createResponse(moduleMap, null, undefined, undefined);
35 +}
36 +
37 +function processChunk(response: FlightResponse, chunk: string | Uint8Array) {
38 + if (enableBinaryFlight) {
39 + if (typeof chunk === 'string') {
40 + throw new Error(
41 + '`enableBinaryFlight` flag is enabled, expected a Uint8Array as input, got string.',
42 + );
43 + }
44 + }
45 + const buffer = typeof chunk !== 'string' ? chunk : encodeString(chunk);
46 +
47 + processBinaryChunk(response, buffer);
48 +}
49 +
50 +function encodeString(string: string) {
51 + const textEncoder = new TextEncoder();
52 + return textEncoder.encode(string);
53 +}
54 +
55 +function startReadingFromStream(
56 + response: FlightResponse,
57 + stream: ReadableStream,
58 +): void {
59 + const reader = stream.getReader();
60 + function progress({
61 + done,
62 + value,
63 + }: {
64 + done: boolean,
65 + value: ?any,
66 + ...
67 + }): void | Promise<void> {
68 + if (done) {
69 + close(response);
70 + return;
71 + }
72 + const buffer: Uint8Array = (value: any);
73 + processChunk(response, buffer);
74 + return reader.read().then(progress).catch(error);
75 + }
76 + function error(e: any) {
77 + reportGlobalError(response, e);
78 + }
79 + reader.read().then(progress).catch(error);
80 +}
81 +
82 +function createFromReadableStream<T>(
83 + stream: ReadableStream,
84 + options?: Options,
85 +): Thenable<T> {
86 + const response: FlightResponse = createResponseFromOptions(options);
87 + startReadingFromStream(response, stream);
88 + return getRoot(response);
89 +}
90 +
91 +export {createFromReadableStream};
packages/react-server-dom-fb/src/ReactFlightDOMServerFB.js new
+68
@@ -0,0 +1,68 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
11 +import type {
12 + Destination,
13 + Chunk,
14 + PrecomputedChunk,
15 +} from 'react-server/src/ReactServerStreamConfig';
16 +import type {ClientManifest} from './ReactFlightReferencesFB';
17 +
18 +import {
19 + createRequest,
20 + startWork,
21 + startFlowing,
22 +} from 'react-server/src/ReactFlightServer';
23 +
24 +import {setByteLengthOfChunkImplementation} from 'react-server/src/ReactServerStreamConfig';
25 +
26 +export {
27 + registerClientReference,
28 + registerServerReference,
29 + getRequestedClientReferencesKeys,
30 + clearRequestedClientReferencesKeysSet,
31 +} from './ReactFlightReferencesFB';
32 +
33 +type Options = {
34 + onError?: (error: mixed) => void,
35 +};
36 +
37 +function renderToDestination(
38 + destination: Destination,
39 + model: ReactClientValue,
40 + bundlerConfig: ClientManifest,
41 + options?: Options,
42 +): void {
43 + if (!configured) {
44 + throw new Error(
45 + 'Please make sure to call `setConfig(...)` before calling `renderToDestination`.',
46 + );
47 + }
48 + const request = createRequest(
49 + model,
50 + bundlerConfig,
51 + options ? options.onError : undefined,
52 + );
53 + startWork(request);
54 + startFlowing(request, destination);
55 +}
56 +
57 +type Config = {
58 + byteLength: (chunk: Chunk | PrecomputedChunk) => number,
59 +};
60 +
61 +let configured = false;
62 +
63 +function setConfig(config: Config): void {
64 + setByteLengthOfChunkImplementation(config.byteLength);
65 + configured = true;
66 +}
67 +
68 +export {renderToDestination, setConfig};
packages/react-server-dom-fb/src/ReactFlightReferencesFB.js new
+98
@@ -0,0 +1,98 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export opaque type ClientManifest = mixed;
11 +
12 +// eslint-disable-next-line no-unused-vars
13 +export type ServerReference<T> = string;
14 +
15 +// eslint-disable-next-line no-unused-vars
16 +export type ClientReference<T> = string;
17 +
18 +const registeredClientReferences = new Map<mixed, ClientReferenceMetadata>();
19 +const requestedClientReferencesKeys = new Set<ClientReferenceKey>();
20 +
21 +export type ClientReferenceKey = string;
22 +export type ClientReferenceMetadata = {
23 + moduleId: ClientReferenceKey,
24 + exportName: string,
25 +};
26 +
27 +export type ServerReferenceId = string;
28 +
29 +export function registerClientReference<T>(
30 + clientReference: ClientReference<T>,
31 + moduleId: ClientReferenceKey,
32 +): ClientReference<T> {
33 + const exportName = 'default'; // Currently, we only support modules with `default` export
34 + registeredClientReferences.set(clientReference, {
35 + moduleId,
36 + exportName,
37 + });
38 +
39 + return clientReference;
40 +}
41 +
42 +export function isClientReference<T>(reference: T): boolean {
43 + return registeredClientReferences.has(reference);
44 +}
45 +
46 +export function getClientReferenceKey<T>(
47 + clientReference: ClientReference<T>,
48 +): ClientReferenceKey {
49 + const reference = registeredClientReferences.get(clientReference);
50 + if (reference != null) {
51 + requestedClientReferencesKeys.add(reference.moduleId);
52 + return reference.moduleId;
53 + }
54 +
55 + throw new Error(
56 + 'Expected client reference ' + clientReference + ' to be registered.',
57 + );
58 +}
59 +
60 +export function resolveClientReferenceMetadata<T>(
61 + config: ClientManifest,
62 + clientReference: ClientReference<T>,
63 +): ClientReferenceMetadata {
64 + const metadata = registeredClientReferences.get(clientReference);
65 + if (metadata != null) {
66 + return metadata;
67 + }
68 +
69 + throw new Error(
70 + 'Expected client reference ' + clientReference + ' to be registered.',
71 + );
72 +}
73 +
74 +export function registerServerReference<T>(
75 + serverReference: ServerReference<T>,
76 + exportName: string,
77 +): ServerReference<T> {
78 + throw new Error('registerServerReference: Not Implemented.');
79 +}
80 +
81 +export function isServerReference<T>(reference: T): boolean {
82 + throw new Error('isServerReference: Not Implemented.');
83 +}
84 +
85 +export function getServerReferenceId<T>(
86 + config: ClientManifest,
87 + serverReference: ServerReference<T>,
88 +): ServerReferenceId {
89 + throw new Error('getServerReferenceId: Not Implemented.');
90 +}
91 +
92 +export function getRequestedClientReferencesKeys(): $ReadOnlyArray<ClientReferenceKey> {
93 + return Array.from(requestedClientReferencesKeys);
94 +}
95 +
96 +export function clearRequestedClientReferencesKeysSet(): void {
97 + requestedClientReferencesKeys.clear();
98 +}
packages/react-server-dom-fb/src/ReactFlightServerConfigFBBundler.js new
+36
@@ -0,0 +1,36 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
11 +
12 +import type {
13 + ClientManifest,
14 + ClientReference,
15 + ServerReference,
16 +} from './ReactFlightReferencesFB';
17 +
18 +export type {ClientManifest, ClientReference, ServerReference};
19 +
20 +export {
21 + ClientReferenceKey,
22 + ClientReferenceMetadata,
23 + getClientReferenceKey,
24 + isClientReference,
25 + resolveClientReferenceMetadata,
26 + isServerReference,
27 + ServerReferenceId,
28 + getServerReferenceId,
29 +} from './ReactFlightReferencesFB';
30 +
31 +export function getServerReferenceBoundArguments<T>(
32 + config: ClientManifest,
33 + serverReference: ServerReference<T>,
34 +): null | Array<ReactClientValue> {
35 + throw new Error('getServerReferenceBoundArguments: Not Implemented.');
36 +}
packages/react-server-dom-fb/src/__tests__/ReactFlightDOMServerFB-test.internal.js new
+364
@@ -0,0 +1,364 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +// Polyfills for test environment
13 +global.ReadableStream =
14 + require('web-streams-polyfill/ponyfill/es6').ReadableStream;
15 +global.TextEncoder = require('util').TextEncoder;
16 +global.TextDecoder = require('util').TextDecoder;
17 +
18 +// Don't wait before processing work on the server.
19 +// TODO: we can replace this with FlightServer.act().
20 +global.setImmediate = cb => cb();
21 +
22 +let act;
23 +let use;
24 +let clientExports;
25 +let moduleMap;
26 +let React;
27 +let ReactDOMClient;
28 +let ReactServerDOMServer;
29 +let ReactServerDOMClient;
30 +let Suspense;
31 +let registerClientReference;
32 +
33 +class Destination {
34 + #buffer = '';
35 + #controller = null;
36 + constructor() {
37 + const self = this;
38 + this.stream = new ReadableStream({
39 + start(controller) {
40 + self.#controller = controller;
41 + },
42 + });
43 + }
44 + write(chunk) {
45 + this.#buffer += chunk;
46 + }
47 + beginWriting() {}
48 + completeWriting() {}
49 + flushBuffered() {
50 + if (!this.#controller) {
51 + throw new Error('Expected a controller.');
52 + }
53 + this.#controller.enqueue(this.#buffer);
54 + this.#buffer = '';
55 + }
56 + close() {}
57 + onError() {}
58 +}
59 +
60 +describe('ReactFlightDOM for FB', () => {
61 + beforeEach(() => {
62 + // For this first reset we are going to load the dom-node version of react-server-dom-turbopack/server
63 + // This can be thought of as essentially being the React Server Components scope with react-server
64 + // condition
65 + jest.resetModules();
66 + registerClientReference =
67 + require('../ReactFlightReferencesFB').registerClientReference;
68 +
69 + jest.mock('react', () => require('react/src/ReactSharedSubsetFB'));
70 +
71 + jest.mock('shared/ReactFeatureFlags', () => {
72 + jest.mock(
73 + 'ReactFeatureFlags',
74 + () => jest.requireActual('shared/forks/ReactFeatureFlags.www-dynamic'),
75 + {virtual: true},
76 + );
77 + return jest.requireActual('shared/forks/ReactFeatureFlags.www');
78 + });
79 +
80 + clientExports = value => {
81 + registerClientReference(value, value.name);
82 + return value;
83 + };
84 +
85 + moduleMap = {
86 + resolveClientReference(metadata) {
87 + throw new Error('Do not expect to load client components.');
88 + },
89 + };
90 +
91 + ReactServerDOMServer = require('../ReactFlightDOMServerFB');
92 + ReactServerDOMServer.setConfig({
93 + byteLength: str => Buffer.byteLength(str),
94 + });
95 +
96 + // This reset is to load modules for the SSR/Browser scope.
97 + jest.resetModules();
98 + __unmockReact();
99 + act = require('internal-test-utils').act;
100 + React = require('react');
101 + use = React.use;
102 + Suspense = React.Suspense;
103 + ReactDOMClient = require('react-dom/client');
104 + ReactServerDOMClient = require('../ReactFlightDOMClientFB');
105 + });
106 +
107 + it('should resolve HTML with renderToDestination', async () => {
108 + function Text({children}) {
109 + return <span>{children}</span>;
110 + }
111 + function HTML() {
112 + return (
113 + <div>
114 + <Text>hello</Text>
115 + <Text>world</Text>
116 + </div>
117 + );
118 + }
119 +
120 + function App() {
121 + const model = {
122 + html: <HTML />,
123 + };
124 + return model;
125 + }
126 + const destination = new Destination();
127 + ReactServerDOMServer.renderToDestination(destination, <App />);
128 + const response = ReactServerDOMClient.createFromReadableStream(
129 + destination.stream,
130 + {
131 + moduleMap,
132 + },
133 + );
134 + const model = await response;
135 + expect(model).toEqual({
136 + html: (
137 + <div>
138 + <span>hello</span>
139 + <span>world</span>
140 + </div>
141 + ),
142 + });
143 + });
144 +
145 + it('should resolve the root', async () => {
146 + // Model
147 + function Text({children}) {
148 + return <span>{children}</span>;
149 + }
150 + function HTML() {
151 + return (
152 + <div>
153 + <Text>hello</Text>
154 + <Text>world</Text>
155 + </div>
156 + );
157 + }
158 + function RootModel() {
159 + return {
160 + html: <HTML />,
161 + };
162 + }
163 +
164 + // View
165 + function Message({response}) {
166 + return <section>{use(response).html}</section>;
167 + }
168 + function App({response}) {
169 + return (
170 + <Suspense fallback={<h1>Loading...</h1>}>
171 + <Message response={response} />
172 + </Suspense>
173 + );
174 + }
175 +
176 + const destination = new Destination();
177 + ReactServerDOMServer.renderToDestination(destination, <RootModel />);
178 + const response = ReactServerDOMClient.createFromReadableStream(
179 + destination.stream,
180 + {
181 + moduleMap,
182 + },
183 + );
184 +
185 + const container = document.createElement('div');
186 + const root = ReactDOMClient.createRoot(container);
187 + await act(() => {
188 + root.render(<App response={response} />);
189 + });
190 + expect(container.innerHTML).toBe(
191 + '<section><div><span>hello</span><span>world</span></div></section>',
192 + );
193 + });
194 +
195 + it('should not get confused by $', async () => {
196 + // Model
197 + function RootModel() {
198 + return {text: '$1'};
199 + }
200 +
201 + // View
202 + function Message({response}) {
203 + return <p>{use(response).text}</p>;
204 + }
205 + function App({response}) {
206 + return (
207 + <Suspense fallback={<h1>Loading...</h1>}>
208 + <Message response={response} />
209 + </Suspense>
210 + );
211 + }
212 + const destination = new Destination();
213 + ReactServerDOMServer.renderToDestination(destination, <RootModel />);
214 + const response = ReactServerDOMClient.createFromReadableStream(
215 + destination.stream,
216 + {
217 + moduleMap,
218 + },
219 + );
220 +
221 + const container = document.createElement('div');
222 + const root = ReactDOMClient.createRoot(container);
223 + await act(() => {
224 + root.render(<App response={response} />);
225 + });
226 + expect(container.innerHTML).toBe('<p>$1</p>');
227 + });
228 +
229 + it('should not get confused by @', async () => {
230 + // Model
231 + function RootModel() {
232 + return {text: '@div'};
233 + }
234 +
235 + // View
236 + function Message({response}) {
237 + return <p>{use(response).text}</p>;
238 + }
239 + function App({response}) {
240 + return (
241 + <Suspense fallback={<h1>Loading...</h1>}>
242 + <Message response={response} />
243 + </Suspense>
244 + );
245 + }
246 + const destination = new Destination();
247 + ReactServerDOMServer.renderToDestination(destination, <RootModel />);
248 + const response = ReactServerDOMClient.createFromReadableStream(
249 + destination.stream,
250 + {
251 + moduleMap,
252 + },
253 + );
254 +
255 + const container = document.createElement('div');
256 + const root = ReactDOMClient.createRoot(container);
257 + await act(() => {
258 + root.render(<App response={response} />);
259 + });
260 + expect(container.innerHTML).toBe('<p>@div</p>');
261 + });
262 +
263 + it('should be able to render a client component', async () => {
264 + const Component = function ({greeting}) {
265 + return greeting + ' World';
266 + };
267 +
268 + function Print({response}) {
269 + return <p>{use(response)}</p>;
270 + }
271 +
272 + function App({response}) {
273 + return (
274 + <Suspense fallback={<h1>Loading...</h1>}>
275 + <Print response={response} />
276 + </Suspense>
277 + );
278 + }
279 +
280 + const ClientComponent = clientExports(Component);
281 +
282 + const destination = new Destination();
283 + ReactServerDOMServer.renderToDestination(
284 + destination,
285 + <ClientComponent greeting={'Hello'} />,
286 + moduleMap,
287 + );
288 + const response = ReactServerDOMClient.createFromReadableStream(
289 + destination.stream,
290 + {
291 + moduleMap: {
292 + resolveClientReference(metadata) {
293 + return {
294 + getModuleId() {
295 + return metadata.moduleId;
296 + },
297 + load() {
298 + return Promise.resolve(Component);
299 + },
300 + };
301 + },
302 + },
303 + },
304 + );
305 +
306 + const container = document.createElement('div');
307 + const root = ReactDOMClient.createRoot(container);
308 + await act(() => {
309 + root.render(<App response={response} />);
310 + });
311 + expect(container.innerHTML).toBe('<p>Hello World</p>');
312 + });
313 +
314 + it('should render long strings', async () => {
315 + // Model
316 + const longString = 'Lorem Ipsum ❤️ '.repeat(100);
317 +
318 + function RootModel() {
319 + return {text: longString};
320 + }
321 +
322 + // View
323 + function Message({response}) {
324 + return <p>{use(response).text}</p>;
325 + }
326 + function App({response}) {
327 + return (
328 + <Suspense fallback={<h1>Loading...</h1>}>
329 + <Message response={response} />
330 + </Suspense>
331 + );
332 + }
333 + const destination = new Destination();
334 + ReactServerDOMServer.renderToDestination(destination, <RootModel />);
335 + const response = ReactServerDOMClient.createFromReadableStream(
336 + destination.stream,
337 + {
338 + moduleMap,
339 + },
340 + );
341 +
342 + const container = document.createElement('div');
343 + const root = ReactDOMClient.createRoot(container);
344 + await act(() => {
345 + root.render(<App response={response} />);
346 + });
347 + expect(container.innerHTML).toBe('<p>' + longString + '</p>');
348 + });
349 +
350 + // TODO: `registerClientComponent` need to be able to support this
351 + it.skip('throws when accessing a member below the client exports', () => {
352 + const ClientModule = clientExports({
353 + Component: {deep: 'thing'},
354 + });
355 + function dotting() {
356 + return ClientModule.Component.deep;
357 + }
358 + expect(dotting).toThrowError(
359 + 'Cannot access Component.deep on the server. ' +
360 + 'You cannot dot into a client module from a server component. ' +
361 + 'You can only pass the imported name through.',
362 + );
363 + });
364 +});
packages/react-server/src/ReactServerStreamConfigFB.js
-4
@@ -18,10 +18,6 @@ export opaque type PrecomputedChunk = string;
18 export opaque type Chunk = string;
19 export opaque type BinaryChunk = string;
20
21 -export function scheduleWork(callback: () => void) {
22 - // We don't schedule work in this model, and instead expect performWork to always be called repeatedly.
23 -}
24 -
21 export function flushBuffered(destination: Destination) {}
22
23 export const supportsRequestStorage = false;
packages/react-server/src/forks/ReactFlightServerConfig.dom-fb-experimental.js new
+16
@@ -0,0 +1,16 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {Request} from 'react-server/src/ReactFlightServer';
11 +
12 +export * from 'react-server-dom-fb/src/ReactFlightServerConfigFBBundler';
13 +export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
14 +
15 +export const supportsRequestStorage = false;
16 +export const requestStorage: AsyncLocalStorage<Request> = (null: any);
packages/react-server/src/forks/ReactServerStreamConfig.dom-fb-experimental.js new
+83
@@ -0,0 +1,83 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export * from '../ReactServerStreamConfigFB';
11 +
12 +import type {
13 + PrecomputedChunk,
14 + Chunk,
15 + BinaryChunk,
16 +} from '../ReactServerStreamConfigFB';
17 +
18 +let byteLengthImpl: null | ((chunk: Chunk | PrecomputedChunk) => number) = null;
19 +
20 +export function setByteLengthOfChunkImplementation(
21 + impl: (chunk: Chunk | PrecomputedChunk) => number,
22 +): void {
23 + byteLengthImpl = impl;
24 +}
25 +
26 +export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
27 + if (byteLengthImpl == null) {
28 + // eslint-disable-next-line react-internal/prod-error-codes
29 + throw new Error(
30 + 'byteLengthOfChunk implementation is not configured. Please, provide the implementation via ReactFlightDOMServer.setConfig(...);',
31 + );
32 + }
33 + return byteLengthImpl(chunk);
34 +}
35 +
36 +export interface Destination {
37 + beginWriting(): void;
38 + write(chunk: Chunk | PrecomputedChunk | BinaryChunk): void;
39 + completeWriting(): void;
40 + flushBuffered(): void;
41 + close(): void;
42 + onError(error: mixed): void;
43 +}
44 +
45 +export function scheduleWork(callback: () => void) {
46 + callback();
47 +}
48 +
49 +export function beginWriting(destination: Destination) {
50 + destination.beginWriting();
51 +}
52 +
53 +export function writeChunk(
54 + destination: Destination,
55 + chunk: Chunk | PrecomputedChunk | BinaryChunk,
56 +): void {
57 + destination.write(chunk);
58 +}
59 +
60 +export function writeChunkAndReturn(
61 + destination: Destination,
62 + chunk: Chunk | PrecomputedChunk | BinaryChunk,
63 +): boolean {
64 + destination.write(chunk);
65 + return true;
66 +}
67 +
68 +export function completeWriting(destination: Destination) {
69 + destination.completeWriting();
70 +}
71 +
72 +export function flushBuffered(destination: Destination) {
73 + destination.flushBuffered();
74 +}
75 +
76 +export function close(destination: Destination) {
77 + destination.close();
78 +}
79 +
80 +export function closeWithError(destination: Destination, error: mixed): void {
81 + destination.onError(error);
82 + destination.close();
83 +}
packages/react-server/src/forks/ReactServerStreamConfig.dom-fb.js
+4
@@ -8,3 +8,7 @@
8 */
9
10 export * from '../ReactServerStreamConfigFB';
11 +
12 +export function scheduleWork(callback: () => void) {
13 + // We don't schedule work in this model, and instead expect performWork to always be called repeatedly.
14 +}
packages/react/src/ReactSharedSubsetFB.js new
+11
@@ -0,0 +1,11 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export * from './ReactSharedSubset';
11 +export {jsx, jsxs, jsxDEV} from './jsx/ReactJSX';
packages/shared/forks/ReactFeatureFlags.www.js
+1 -1
@@ -75,7 +75,7 @@ export const enableFetchInstrumentation = false;
75
76 export const enableFormActions = false;
77
78 -export const enableBinaryFlight = true;
78 +export const enableBinaryFlight = false;
79 export const enableTaint = false;
80
81 export const enablePostpone = false;
scripts/rollup/bundles.js
+33
@@ -104,6 +104,17 @@ const bundles = [
104 externals: [],
105 },
106
107 + /******* Isomorphic Shared Subset for FB *******/
108 + {
109 + bundleTypes: __EXPERIMENTAL__ ? [FB_WWW_DEV, FB_WWW_PROD] : [],
110 + moduleType: ISOMORPHIC,
111 + entry: 'react/src/ReactSharedSubsetFB.js',
112 + global: 'ReactSharedSubset',
113 + minifyWithProdErrorCodes: true,
114 + wrapWithModuleBoundaries: false,
115 + externals: [],
116 + },
117 +
118 /******* React JSX Runtime *******/
119 {
120 bundleTypes: [
@@ -574,6 +585,28 @@ const bundles = [
585 externals: ['acorn'],
586 },
587
588 + /******* React Server DOM FB Server *******/
589 + {
590 + bundleTypes: __EXPERIMENTAL__ ? [FB_WWW_DEV, FB_WWW_PROD] : [],
591 + moduleType: RENDERER,
592 + entry: 'react-server-dom-fb/src/ReactFlightDOMServerFB.js',
593 + global: 'ReactFlightDOMServer',
594 + minifyWithProdErrorCodes: false,
595 + wrapWithModuleBoundaries: false,
596 + externals: ['react', 'react-dom'],
597 + },
598 +
599 + /******* React Server DOM FB Client *******/
600 + {
601 + bundleTypes: __EXPERIMENTAL__ ? [FB_WWW_DEV, FB_WWW_PROD] : [],
602 + moduleType: RENDERER,
603 + entry: 'react-server-dom-fb/src/ReactFlightDOMClientFB.js',
604 + global: 'ReactFlightDOMClient',
605 + minifyWithProdErrorCodes: false,
606 + wrapWithModuleBoundaries: false,
607 + externals: ['react', 'react-dom'],
608 + },
609 +
610 /******* React Suspense Test Utils *******/
611 {
612 bundleTypes: [NODE_ES2015],
scripts/rollup/forks.js
+4 -1
@@ -63,7 +63,10 @@ const forks = Object.freeze({
63 if (entry === 'react') {
64 return './packages/react/src/ReactSharedInternalsClient.js';
65 }
66 - if (entry === 'react/src/ReactSharedSubset.js') {
66 + if (
67 + entry === 'react/src/ReactSharedSubset.js' ||
68 + entry === 'react/src/ReactSharedSubsetFB.js'
69 + ) {
70 return './packages/react/src/ReactSharedInternalsServer.js';
71 }
72 if (!entry.startsWith('react/') && dependencies.indexOf('react') === -1) {
scripts/shared/inlinedHostConfigs.js
+22 -1
@@ -396,13 +396,34 @@ module.exports = [
396 'react-dom',
397 'react-dom/src/ReactDOMSharedSubset.js',
398 'react-dom-bindings',
399 - 'react-server-dom-fb',
399 + 'react-server-dom-fb/src/ReactDOMServerFB.js',
400 'shared/ReactDOMSharedInternals',
401 ],
402 isFlowTyped: true,
403 isServerSupported: true,
404 isFlightSupported: false,
405 },
406 + {
407 + shortName: 'dom-fb-experimental',
408 + entryPoints: [
409 + 'react-server-dom-fb/src/ReactFlightDOMClientFB.js',
410 + 'react-server-dom-fb/src/ReactFlightDOMServerFB.js',
411 + ],
412 + paths: [
413 + 'react-dom',
414 + 'react-dom-bindings',
415 + 'react-server-dom-fb/src/ReactFlightClientConfigFBBundler.js',
416 + 'react-server-dom-fb/src/ReactFlightClientConfigFBBundler.js',
417 + 'react-server-dom-fb/src/ReactFlightReferencesFB.js',
418 + 'react-server-dom-fb/src/ReactFlightServerConfigFBBundler.js',
419 + 'react-server-dom-fb/src/ReactFlightDOMClientFB.js',
420 + 'react-server-dom-fb/src/ReactFlightDOMServerFB.js',
421 + 'shared/ReactDOMSharedInternals',
422 + ],
423 + isFlowTyped: true,
424 + isServerSupported: true,
425 + isFlightSupported: true,
426 + },
427 {
428 shortName: 'native',
429 entryPoints: ['react-native-renderer'],