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'],