@samitouri / QOS-React / commits / ffd8423356

[Flight] Add support for Module References in transport protocol (#20121)

* Refactor Flight to require a module reference to be brand checked This exposes a host environment (bundler) specific hook to check if an object is a module reference. This will be used so that they can be passed directly into Flight without needing additional wrapper objects. * Emit module references as a special type of value We already have JSON and errors as special types of "rows". This encodes module references as a special type of row value. This was always the intention because it allows those values to be emitted first in the stream so that as a large models stream down, we can start preloading as early as possible. We preload the module when they resolve but we lazily require them as they are referenced. * Emit module references where ever they occur This emits module references where ever they occur. In blocks or even directly in elements. * Don't special case the root row I originally did this so that a simple stream is also just plain JSON. However, since we might want to emit things like modules before the root module in the stream, this gets unnecessarily complicated. We could add this back as a special case if it's the first byte written but meh. * Update the protocol * Add test for using a module reference as a client component * Relax element type check Since Flight now accepts a module reference as returned by any bundler system, depending on the renderer running. We need to drastically relax the check to include all of them. We can add more as we discover them. * Move flow annotation Seems like our compiler is not happy with stripping this. * Some bookkeeping bug * Can't use the private field to check

Sebastian Markbåge committed Oct 29, 2020 at 20:57 UTC ffd84233568fd9d277eaba7c01d53d516f6f132f
20 files changed +367 -65
packages/react-client/src/ReactFlightClient.js
+68 -13
@@ -41,8 +41,9 @@ export type JSONValue =
41
42 const PENDING = 0;
43 const RESOLVED_MODEL = 1;
44 -const INITIALIZED = 2;
45 -const ERRORED = 3;
44 +const RESOLVED_MODULE = 2;
45 +const INITIALIZED = 3;
46 +const ERRORED = 4;
47
48 type PendingChunk = {
49 _status: 0,
@@ -56,14 +57,20 @@ type ResolvedModelChunk = {
57 _response: Response,
58 then(resolve: () => mixed): void,
59 };
59 -type InitializedChunk<T> = {
60 +type ResolvedModuleChunk<T> = {
61 _status: 2,
62 + _value: ModuleReference<T>,
63 + _response: Response,
64 + then(resolve: () => mixed): void,
65 +};
66 +type InitializedChunk<T> = {
67 + _status: 3,
68 _value: T,
69 _response: Response,
70 then(resolve: () => mixed): void,
71 };
72 type ErroredChunk = {
66 - _status: 3,
73 + _status: 4,
74 _value: Error,
75 _response: Response,
76 then(resolve: () => mixed): void,
@@ -71,6 +78,7 @@ type ErroredChunk = {
78 type SomeChunk<T> =
79 | PendingChunk
80 | ResolvedModelChunk
81 + | ResolvedModuleChunk<T>
82 | InitializedChunk<T>
83 | ErroredChunk;
84
@@ -105,6 +113,8 @@ function readChunk<T>(chunk: SomeChunk<T>): T {
113 return chunk._value;
114 case RESOLVED_MODEL:
115 return initializeModelChunk(chunk);
116 + case RESOLVED_MODULE:
117 + return initializeModuleChunk(chunk);
118 case PENDING:
119 // eslint-disable-next-line no-throw-literal
120 throw (chunk: Wakeable);
@@ -155,6 +165,13 @@ function createResolvedModelChunk(
165 return new Chunk(RESOLVED_MODEL, value, response);
166 }
167
168 +function createResolvedModuleChunk<T>(
169 + response: Response,
170 + value: ModuleReference<T>,
171 +): ResolvedModuleChunk<T> {
172 + return new Chunk(RESOLVED_MODULE, value, response);
173 +}
174 +
175 function resolveModelChunk<T>(
176 chunk: SomeChunk<T>,
177 value: UninitializedModel,
@@ -170,6 +187,21 @@ function resolveModelChunk<T>(
187 wakeChunk(listeners);
188 }
189
190 +function resolveModuleChunk<T>(
191 + chunk: SomeChunk<T>,
192 + value: ModuleReference<T>,
193 +): void {
194 + if (chunk._status !== PENDING) {
195 + // We already resolved. We didn't expect to see this.
196 + return;
197 + }
198 + const listeners = chunk._value;
199 + const resolvedChunk: ResolvedModuleChunk<T> = (chunk: any);
200 + resolvedChunk._status = RESOLVED_MODULE;
201 + resolvedChunk._value = value;
202 + wakeChunk(listeners);
203 +}
204 +
205 function initializeModelChunk<T>(chunk: ResolvedModelChunk): T {
206 const value: T = parseModel(chunk._response, chunk._value);
207 const initializedChunk: InitializedChunk<T> = (chunk: any);
@@ -178,6 +210,14 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk): T {
210 return value;
211 }
212
213 +function initializeModuleChunk<T>(chunk: ResolvedModuleChunk<T>): T {
214 + const value: T = requireModule(chunk._value);
215 + const initializedChunk: InitializedChunk<T> = (chunk: any);
216 + initializedChunk._status = INITIALIZED;
217 + initializedChunk._value = value;
218 + return value;
219 +}
220 +
221 // Report that any missing chunks in the model is now going to throw this
222 // error upon read. Also notify any pending promises.
223 export function reportGlobalError(response: Response, error: Error): void {
@@ -241,7 +281,7 @@ function createElement(type, key, props): React$Element<any> {
281
282 type UninitializedBlockPayload<Data> = [
283 mixed,
244 - ModuleMetaData | SomeChunk<ModuleMetaData>,
284 + BlockRenderFunction<any, Data> | SomeChunk<BlockRenderFunction<any, Data>>,
285 Data | SomeChunk<Data>,
286 Response,
287 ];
@@ -250,14 +290,7 @@ function initializeBlock<Props, Data>(
290 tuple: UninitializedBlockPayload<Data>,
291 ): BlockComponent<Props, Data> {
292 // Require module first and then data. The ordering matters.
253 - const moduleMetaData: ModuleMetaData = readMaybeChunk(tuple[1]);
254 - const moduleReference: ModuleReference<
255 - BlockRenderFunction<Props, Data>,
256 - > = resolveModuleReference(moduleMetaData);
257 - // TODO: Do this earlier, as the chunk is resolved.
258 - preloadModule(moduleReference);
259 -
260 - const moduleExport = requireModule(moduleReference);
293 + const moduleExport = readMaybeChunk(tuple[1]);
294
295 // The ordering here is important because this call might suspend.
296 // We don't want that to prevent the module graph for being initialized.
@@ -363,6 +396,28 @@ export function resolveModel(
396 }
397 }
398
399 +export function resolveModule(
400 + response: Response,
401 + id: number,
402 + model: UninitializedModel,
403 +): void {
404 + const chunks = response._chunks;
405 + const chunk = chunks.get(id);
406 + const moduleMetaData: ModuleMetaData = parseModel(response, model);
407 + const moduleReference = resolveModuleReference(moduleMetaData);
408 +
409 + // TODO: Add an option to encode modules that are lazy loaded.
410 + // For now we preload all modules as early as possible since it's likely
411 + // that we'll need them.
412 + preloadModule(moduleReference);
413 +
414 + if (!chunk) {
415 + chunks.set(id, createResolvedModuleChunk(response, moduleReference));
416 + } else {
417 + resolveModuleChunk(chunk, moduleReference);
418 + }
419 +}
420 +
421 export function resolveError(
422 response: Response,
423 id: number,
packages/react-client/src/ReactFlightClientStream.js
+11 -3
@@ -10,6 +10,7 @@
10 import type {Response} from './ReactFlightClientHostConfigStream';
11
12 import {
13 + resolveModule,
14 resolveModel,
15 resolveError,
16 createResponse as createResponseBase,
@@ -39,6 +40,13 @@ function processFullRow(response: Response, row: string): void {
40 resolveModel(response, id, json);
41 return;
42 }
43 + case 'M': {
44 + const colon = row.indexOf(':', 1);
45 + const id = parseInt(row.substring(1, colon), 16);
46 + const json = row.substring(colon + 1);
47 + resolveModule(response, id, json);
48 + return;
49 + }
50 case 'E': {
51 const colon = row.indexOf(':', 1);
52 const id = parseInt(row.substring(1, colon), 16);
@@ -48,9 +56,9 @@ function processFullRow(response: Response, row: string): void {
56 return;
57 }
58 default: {
51 - // Assume this is the root model.
52 - resolveModel(response, 0, row);
53 - return;
59 + throw new Error(
60 + "Error parsing the data. It's probably an error code or network corruption.",
61 + );
62 }
63 }
64 }
packages/react-client/src/__tests__/ReactFlight-test.js
+43 -2
@@ -53,17 +53,29 @@ describe('ReactFlight', () => {
53 };
54 });
55
56 + function moduleReference(value) {
57 + return {
58 + $$typeof: Symbol.for('react.module.reference'),
59 + value: value,
60 + };
61 + }
62 +
63 function block(render, load) {
64 if (load === undefined) {
65 return () => {
59 - return ReactNoopFlightServerRuntime.serverBlockNoData(render);
66 + return ReactNoopFlightServerRuntime.serverBlockNoData(
67 + moduleReference(render),
68 + );
69 };
70 }
71 return function(...args) {
72 const curriedLoad = () => {
73 return load(...args);
74 };
66 - return ReactNoopFlightServerRuntime.serverBlock(render, curriedLoad);
75 + return ReactNoopFlightServerRuntime.serverBlock(
76 + moduleReference(render),
77 + curriedLoad,
78 + );
79 };
80 }
81
@@ -97,6 +109,35 @@ describe('ReactFlight', () => {
109 });
110 });
111
112 + it('can render a client component using a module reference and render there', () => {
113 + function UserClient(props) {
114 + return (
115 + <span>
116 + {props.greeting}, {props.name}
117 + </span>
118 + );
119 + }
120 + const User = moduleReference(UserClient);
121 +
122 + function Greeting({firstName, lastName}) {
123 + return <User greeting="Hello" name={firstName + ' ' + lastName} />;
124 + }
125 +
126 + const model = {
127 + greeting: <Greeting firstName="Seb" lastName="Smith" />,
128 + };
129 +
130 + const transport = ReactNoopFlightServer.render(model);
131 +
132 + act(() => {
133 + const rootModel = ReactNoopFlightClient.read(transport);
134 + const greeting = rootModel.greeting;
135 + ReactNoop.render(greeting);
136 + });
137 +
138 + expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb Smith</span>);
139 + });
140 +
141 if (ReactFeatureFlags.enableBlocksAPI) {
142 it('can transfer a Block to the client and render there, without data', () => {
143 function User(props, data) {
packages/react-noop-renderer/src/ReactNoopFlightServer.js
+8 -2
@@ -42,8 +42,14 @@ const ReactNoopFlightServer = ReactFlightServer({
42 formatChunk(type: string, props: Object): Uint8Array {
43 return Buffer.from(JSON.stringify({type, props}), 'utf8');
44 },
45 - resolveModuleMetaData(config: void, renderFn: Function) {
46 - return saveModule(renderFn);
45 + isModuleReference(reference: Object): boolean {
46 + return reference.$$typeof === Symbol.for('react.module.reference');
47 + },
48 + resolveModuleMetaData(
49 + config: void,
50 + reference: {$$typeof: Symbol, value: any},
51 + ) {
52 + return saveModule(reference.value);
53 },
54 });
55
packages/react-server/src/ReactFlightServer.js
+60 -14
@@ -24,8 +24,10 @@ import {
24 flushBuffered,
25 close,
26 processModelChunk,
27 + processModuleChunk,
28 processErrorChunk,
29 resolveModuleMetaData,
30 + isModuleReference,
31 } from './ReactFlightServerConfig';
32
33 import {
@@ -83,6 +85,7 @@ export type Request = {
85 nextChunkId: number,
86 pendingChunks: number,
87 pingedSegments: Array<Segment>,
88 + completedModuleChunks: Array<Chunk>,
89 completedJSONChunks: Array<Chunk>,
90 completedErrorChunks: Array<Chunk>,
91 flowing: boolean,
@@ -103,6 +106,7 @@ export function createRequest(
106 nextChunkId: 0,
107 pendingChunks: 0,
108 pingedSegments: pingedSegments,
109 + completedModuleChunks: [],
110 completedJSONChunks: [],
111 completedErrorChunks: [],
112 flowing: false,
@@ -151,6 +155,10 @@ function attemptResolveElement(element: React$Element<any>): ReactModel {
155 ) {
156 return element.props.children;
157 } else if (type != null && typeof type === 'object') {
158 + if (isModuleReference(type)) {
159 + // This is a reference to a client component.
160 + return [REACT_ELEMENT_TYPE, type, element.key, element.props];
161 + }
162 switch (type.$$typeof) {
163 case REACT_FORWARD_REF_TYPE: {
164 const render = type.render;
@@ -391,19 +399,8 @@ export function resolveModelToJSON(
399 switch (key) {
400 case '1': {
401 // Module reference
394 - const moduleReference: ModuleReference<any> = (value: any);
395 - try {
396 - const moduleMetaData: ModuleMetaData = resolveModuleMetaData(
397 - request.bundlerConfig,
398 - moduleReference,
399 - );
400 - return (moduleMetaData: ReactJSONValue);
401 - } catch (x) {
402 - request.pendingChunks++;
403 - const errorId = request.nextChunkId++;
404 - emitErrorChunk(request, errorId, x);
405 - return serializeIDRef(errorId);
406 - }
402 + // Encode as a normal value.
403 + break;
404 }
405 case '2': {
406 // Load function
@@ -467,7 +464,30 @@ export function resolveModelToJSON(
464 }
465 }
466
467 + if (value === null) {
468 + return null;
469 + }
470 +
471 if (typeof value === 'object') {
472 + if (isModuleReference(value)) {
473 + const moduleReference: ModuleReference<any> = (value: any);
474 + try {
475 + const moduleMetaData: ModuleMetaData = resolveModuleMetaData(
476 + request.bundlerConfig,
477 + moduleReference,
478 + );
479 + request.pendingChunks++;
480 + const moduleId = request.nextChunkId++;
481 + emitModuleChunk(request, moduleId, moduleMetaData);
482 + return serializeIDRef(moduleId);
483 + } catch (x) {
484 + request.pendingChunks++;
485 + const errorId = request.nextChunkId++;
486 + emitErrorChunk(request, errorId, x);
487 + return serializeIDRef(errorId);
488 + }
489 + }
490 +
491 if (__DEV__) {
492 if (value !== null && !isArray(value)) {
493 // Verify that this is a simple plain object.
@@ -595,6 +615,15 @@ function emitErrorChunk(request: Request, id: number, error: mixed): void {
615 request.completedErrorChunks.push(processedChunk);
616 }
617
618 +function emitModuleChunk(
619 + request: Request,
620 + id: number,
621 + moduleMetaData: ModuleMetaData,
622 +): void {
623 + const processedChunk = processModuleChunk(request, id, moduleMetaData);
624 + request.completedModuleChunks.push(processedChunk);
625 +}
626 +
627 function retrySegment(request: Request, segment: Segment): void {
628 const query = segment.query;
629 let value;
@@ -654,8 +683,22 @@ function flushCompletedChunks(request: Request): void {
683 const destination = request.destination;
684 beginWriting(destination);
685 try {
657 - const jsonChunks = request.completedJSONChunks;
686 + // We emit module chunks first in the stream so that
687 + // they can be preloaded as early as possible.
688 + const moduleChunks = request.completedModuleChunks;
689 let i = 0;
690 + for (; i < moduleChunks.length; i++) {
691 + request.pendingChunks--;
692 + const chunk = moduleChunks[i];
693 + if (!writeChunk(destination, chunk)) {
694 + request.flowing = false;
695 + i++;
696 + break;
697 + }
698 + }
699 + // Next comes model data.
700 + const jsonChunks = request.completedJSONChunks;
701 + i = 0;
702 for (; i < jsonChunks.length; i++) {
703 request.pendingChunks--;
704 const chunk = jsonChunks[i];
@@ -666,6 +709,9 @@ function flushCompletedChunks(request: Request): void {
709 }
710 }
711 jsonChunks.splice(0, i);
712 + // Finally, errors are sent. The idea is that it's ok to delay
713 + // any error messages and prioritize display of other parts of
714 + // the page.
715 const errorChunks = request.completedErrorChunks;
716 i = 0;
717 for (; i < errorChunks.length; i++) {
packages/react-server/src/ReactFlightServerBundlerConfigCustom.js
+1
@@ -12,4 +12,5 @@ declare var $$$hostConfig: any;
12 export opaque type BundlerConfig = mixed; // eslint-disable-line no-undef
13 export opaque type ModuleReference<T> = mixed; // eslint-disable-line no-undef
14 export opaque type ModuleMetaData: any = mixed; // eslint-disable-line no-undef
15 +export const isModuleReference = $$$hostConfig.isModuleReference;
16 export const resolveModuleMetaData = $$$hostConfig.resolveModuleMetaData;
packages/react-server/src/ReactFlightServerConfigStream.js
+13 -8
@@ -14,8 +14,7 @@
14 FLIGHT PROTOCOL GRAMMAR
15
16 Response
17 -- JSONData RowSequence
18 -- JSONData
17 +- RowSequence
18
19 RowSequence
20 - Row RowSequence
@@ -23,6 +22,7 @@ RowSequence
22
23 Row
24 - "J" RowID JSONData
25 +- "M" RowID JSONModuleData
26 - "H" RowID HTMLData
27 - "B" RowID BlobData
28 - "U" RowID URLData
@@ -95,12 +95,17 @@ export function processModelChunk(
95 model: ReactModel,
96 ): Chunk {
97 const json = stringify(model, request.toJSON);
98 - let row;
99 - if (id === 0) {
100 - row = json + '\n';
101 - } else {
102 - row = serializeRowHeader('J', id) + json + '\n';
103 - }
98 + const row = serializeRowHeader('J', id) + json + '\n';
99 + return convertStringToBuffer(row);
100 +}
101 +
102 +export function processModuleChunk(
103 + request: Request,
104 + id: number,
105 + moduleMetaData: ReactModel,
106 +): Chunk {
107 + const json = stringify(moduleMetaData);
108 + const row = serializeRowHeader('M', id) + json + '\n';
109 return convertStringToBuffer(row);
110 }
111
packages/react-transport-dom-relay/src/ReactFlightDOMRelayClient.js
+1
@@ -10,6 +10,7 @@
10 export {
11 createResponse,
12 resolveModel,
13 + resolveModule,
14 resolveError,
15 close,
16 } from 'react-client/src/ReactFlightClient';
packages/react-transport-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js
+5 -4
@@ -9,6 +9,10 @@
9
10 import type {JSONValue, ResponseBase} from 'react-client/src/ReactFlightClient';
11
12 +import type JSResourceReference from 'JSResourceReference';
13 +
14 +export type ModuleReference<T> = JSResourceReference<T>;
15 +
16 import {
17 parseModelString,
18 parseModelTuple,
@@ -20,10 +24,7 @@ export {
24 requireModule,
25 } from 'ReactFlightDOMRelayClientIntegration';
26
23 -export type {
24 - ModuleReference,
25 - ModuleMetaData,
26 -} from 'ReactFlightDOMRelayClientIntegration';
27 +export type {ModuleMetaData} from 'ReactFlightDOMRelayClientIntegration';
28
29 export opaque type UninitializedModel = JSONValue;
30
packages/react-transport-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js
+29 -2
@@ -9,10 +9,13 @@
9
10 import type {Request, ReactModel} from 'react-server/src/ReactFlightServer';
11
12 +import JSResourceReference from 'JSResourceReference';
13 +
14 +export type ModuleReference<T> = JSResourceReference<T>;
15 +
16 import type {
17 Destination,
18 BundlerConfig,
15 - ModuleReference,
19 ModuleMetaData,
20 } from 'ReactFlightDOMRelayServerIntegration';
21
@@ -20,6 +23,7 @@ import {resolveModelToJSON} from 'react-server/src/ReactFlightServer';
23
24 import {
25 emitModel,
26 + emitModule,
27 emitError,
28 resolveModuleMetaData as resolveModuleMetaDataImpl,
29 } from 'ReactFlightDOMRelayServerIntegration';
@@ -27,10 +31,13 @@ import {
31 export type {
32 Destination,
33 BundlerConfig,
30 - ModuleReference,
34 ModuleMetaData,
35 } from 'ReactFlightDOMRelayServerIntegration';
36
37 +export function isModuleReference(reference: Object): boolean {
38 + return reference instanceof JSResourceReference;
39 +}
40 +
41 export function resolveModuleMetaData<T>(
42 config: BundlerConfig,
43 resource: ModuleReference<T>,
@@ -52,6 +59,11 @@ export type Chunk =
59 id: number,
60 json: JSONValue,
61 }
62 + | {
63 + type: 'module',
64 + id: number,
65 + json: ModuleMetaData,
66 + }
67 | {
68 type: 'error',
69 id: number,
@@ -121,6 +133,19 @@ export function processModelChunk(
133 };
134 }
135
136 +export function processModuleChunk(
137 + request: Request,
138 + id: number,
139 + moduleMetaData: ModuleMetaData,
140 +): Chunk {
141 + // The moduleMetaData is already a JSON serializable value.
142 + return {
143 + type: 'module',
144 + id: id,
145 + json: moduleMetaData,
146 + };
147 +}
148 +
149 export function scheduleWork(callback: () => void) {
150 callback();
151 }
@@ -132,6 +157,8 @@ export function beginWriting(destination: Destination) {}
157 export function writeChunk(destination: Destination, chunk: Chunk): boolean {
158 if (chunk.type === 'json') {
159 emitModel(destination, chunk.id, chunk.json);
160 + } else if (chunk.type === 'module') {
161 + emitModule(destination, chunk.id, chunk.json);
162 } else {
163 emitError(destination, chunk.id, chunk.json.message, chunk.json.stack);
164 }
packages/react-transport-dom-relay/src/__mocks__/JSResourceReference.js new
+19
@@ -0,0 +1,19 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 +
8 +'use strict';
9 +
10 +class JSResourceReference {
11 + constructor(exportedValue) {
12 + this._moduleId = exportedValue;
13 + }
14 + getModuleID() {
15 + return this._moduleId;
16 + }
17 +}
18 +
19 +module.exports = JSResourceReference;
packages/react-transport-dom-relay/src/__mocks__/ReactFlightDOMRelayClientIntegration.js
+4 -2
@@ -7,13 +7,15 @@
7
8 'use strict';
9
10 +import JSResourceReference from 'JSResourceReference';
11 +
12 const ReactFlightDOMRelayClientIntegration = {
13 resolveModuleReference(moduleData) {
12 - return moduleData;
14 + return new JSResourceReference(moduleData);
15 },
16 preloadModule(moduleReference) {},
17 requireModule(moduleReference) {
16 - return moduleReference;
18 + return moduleReference._moduleId;
19 },
20 };
21
packages/react-transport-dom-relay/src/__mocks__/ReactFlightDOMRelayServerIntegration.js
+8 -1
@@ -15,6 +15,13 @@ const ReactFlightDOMRelayServerIntegration = {
15 json: json,
16 });
17 },
18 + emitModule(destination, id, json) {
19 + destination.push({
20 + type: 'module',
21 + id: id,
22 + json: json,
23 + });
24 + },
25 emitError(destination, id, message, stack) {
26 destination.push({
27 type: 'error',
@@ -24,7 +31,7 @@ const ReactFlightDOMRelayServerIntegration = {
31 },
32 close(destination) {},
33 resolveModuleMetaData(config, resource) {
27 - return resource;
34 + return resource._moduleId;
35 },
36 };
37
packages/react-transport-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js
+43 -2
@@ -10,6 +10,7 @@
10 let act;
11 let React;
12 let ReactDOM;
13 +let JSResourceReference;
14 let ReactDOMFlightRelayServer;
15 let ReactDOMFlightRelayServerRuntime;
16 let ReactDOMFlightRelayClient;
@@ -24,6 +25,7 @@ describe('ReactFlightDOMRelay', () => {
25 ReactDOMFlightRelayServer = require('react-transport-dom-relay/server');
26 ReactDOMFlightRelayServerRuntime = require('react-transport-dom-relay/server-runtime');
27 ReactDOMFlightRelayClient = require('react-transport-dom-relay');
28 + JSResourceReference = require('JSResourceReference');
29 });
30
31 function readThrough(data) {
@@ -32,6 +34,8 @@ describe('ReactFlightDOMRelay', () => {
34 const chunk = data[i];
35 if (chunk.type === 'json') {
36 ReactDOMFlightRelayClient.resolveModel(response, chunk.id, chunk.json);
37 + } else if (chunk.type === 'module') {
38 + ReactDOMFlightRelayClient.resolveModule(response, chunk.id, chunk.json);
39 } else {
40 ReactDOMFlightRelayClient.resolveError(
41 response,
@@ -47,14 +51,18 @@ describe('ReactFlightDOMRelay', () => {
51 }
52
53 function block(render, load) {
54 + const reference = new JSResourceReference(render);
55 if (load === undefined) {
51 - return ReactDOMFlightRelayServerRuntime.serverBlock(render);
56 + return ReactDOMFlightRelayServerRuntime.serverBlock(reference);
57 }
58 return function(...args) {
59 const curriedLoad = () => {
60 return load(...args);
61 };
57 - return ReactDOMFlightRelayServerRuntime.serverBlock(render, curriedLoad);
62 + return ReactDOMFlightRelayServerRuntime.serverBlock(
63 + reference,
64 + curriedLoad,
65 + );
66 };
67 }
68
@@ -93,6 +101,39 @@ describe('ReactFlightDOMRelay', () => {
101 });
102 });
103
104 + // @gate experimental
105 + it('can render a client component using a module reference and render there', () => {
106 + function UserClient(props) {
107 + return (
108 + <span>
109 + {props.greeting}, {props.name}
110 + </span>
111 + );
112 + }
113 + const User = new JSResourceReference(UserClient);
114 +
115 + function Greeting({firstName, lastName}) {
116 + return <User greeting="Hello" name={firstName + ' ' + lastName} />;
117 + }
118 +
119 + const model = {
120 + greeting: <Greeting firstName="Seb" lastName="Smith" />,
121 + };
122 +
123 + const transport = [];
124 + ReactDOMFlightRelayServer.render(model, transport);
125 +
126 + const modelClient = readThrough(transport);
127 +
128 + const container = document.createElement('div');
129 + const root = ReactDOM.createRoot(container);
130 + act(() => {
131 + root.render(modelClient.greeting);
132 + });
133 +
134 + expect(container.innerHTML).toEqual('<span>Hello, Seb Smith</span>');
135 + });
136 +
137 // @gate experimental
138 it('can transfer a Block to the client and render there', () => {
139 function load(firstName, lastName) {
packages/react-transport-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js
+12 -3
@@ -14,7 +14,10 @@ type WebpackMap = {
14 export type BundlerConfig = WebpackMap;
15
16 // eslint-disable-next-line no-unused-vars
17 -export type ModuleReference<T> = string;
17 +export type ModuleReference<T> = {
18 + $$typeof: Symbol,
19 + name: string,
20 +};
21
22 export type ModuleMetaData = {
23 id: string,
@@ -22,9 +25,15 @@ export type ModuleMetaData = {
25 name: string,
26 };
27
28 +const MODULE_TAG = Symbol.for('react.module.reference');
29 +
30 +export function isModuleReference(reference: Object): boolean {
31 + return reference.$$typeof === MODULE_TAG;
32 +}
33 +
34 export function resolveModuleMetaData<T>(
35 config: BundlerConfig,
27 - modulePath: ModuleReference<T>,
36 + moduleReference: ModuleReference<T>,
37 ): ModuleMetaData {
29 - return config[modulePath];
38 + return config[moduleReference.name];
39 }
packages/react-transport-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+2 -1
@@ -83,8 +83,9 @@ describe('ReactFlightDOM', () => {
83 const curriedLoad = () => {
84 return load(...args);
85 };
86 + const MODULE_TAG = Symbol.for('react.module.reference');
87 return ReactTransportDOMServerRuntime.serverBlock(
87 - 'path/' + idx,
88 + {$$typeof: MODULE_TAG, name: 'path/' + idx},
89 curriedLoad,
90 );
91 };
packages/shared/isValidElementType.js
+11
@@ -27,6 +27,11 @@ import {
27 } from 'shared/ReactSymbols';
28 import {enableScopeAPI} from './ReactFeatureFlags';
29
30 +let REACT_MODULE_REFERENCE: number | Symbol = 0;
31 +if (typeof Symbol === 'function') {
32 + REACT_MODULE_REFERENCE = Symbol.for('react.module.reference');
33 +}
34 +
35 export default function isValidElementType(type: mixed) {
36 if (typeof type === 'string' || typeof type === 'function') {
37 return true;
@@ -54,6 +59,12 @@ export default function isValidElementType(type: mixed) {
59 type.$$typeof === REACT_CONTEXT_TYPE ||
60 type.$$typeof === REACT_FORWARD_REF_TYPE ||
61 type.$$typeof === REACT_FUNDAMENTAL_TYPE ||
62 + // This needs to include all possible module reference object
63 + // types supported by any Flight configuration anywhere since
64 + // we don't know which Flight build this will end up being used
65 + // with.
66 + type.$$typeof === REACT_MODULE_REFERENCE ||
67 + type.getModuleID !== undefined ||
68 type.$$typeof === REACT_BLOCK_TYPE ||
69 type[(0: any)] === REACT_SERVER_BLOCK_TYPE
70 ) {
scripts/flow/react-relay-hooks.js
+17 -6
@@ -15,6 +15,14 @@ type JSONValue =
15 | {+[key: string]: JSONValue}
16 | $ReadOnlyArray<JSONValue>;
17
18 +declare class JSResourceReference<T> {
19 + _moduleId: T;
20 +}
21 +
22 +declare module 'JSResourceReference' {
23 + declare export default typeof JSResourceReference;
24 +}
25 +
26 declare module 'ReactFlightDOMRelayServerIntegration' {
27 declare export opaque type Destination;
28 declare export opaque type BundlerConfig;
@@ -23,6 +31,11 @@ declare module 'ReactFlightDOMRelayServerIntegration' {
31 id: number,
32 json: JSONValue,
33 ): void;
34 + declare export function emitModule(
35 + destination: Destination,
36 + id: number,
37 + json: ModuleMetaData,
38 + ): void;
39 declare export function emitError(
40 destination: Destination,
41 id: number,
@@ -31,24 +44,22 @@ declare module 'ReactFlightDOMRelayServerIntegration' {
44 ): void;
45 declare export function close(destination: Destination): void;
46
34 - declare export opaque type ModuleReference<T>;
47 declare export type ModuleMetaData = JSONValue;
48 declare export function resolveModuleMetaData<T>(
49 config: BundlerConfig,
38 - resourceReference: ModuleReference<T>,
50 + resourceReference: JSResourceReference<T>,
51 ): ModuleMetaData;
52 }
53
54 declare module 'ReactFlightDOMRelayClientIntegration' {
43 - declare export opaque type ModuleReference<T>;
55 declare export opaque type ModuleMetaData;
56 declare export function resolveModuleReference<T>(
57 moduleData: ModuleMetaData,
47 - ): ModuleReference<T>;
58 + ): JSResourceReference<T>;
59 declare export function preloadModule<T>(
49 - moduleReference: ModuleReference<T>,
60 + moduleReference: JSResourceReference<T>,
61 ): void;
62 declare export function requireModule<T>(
52 - moduleReference: ModuleReference<T>,
63 + moduleReference: JSResourceReference<T>,
64 ): T;
65 }
scripts/jest/setupHostConfigs.js
+1
@@ -34,6 +34,7 @@ jest.mock('react-server/flight', () => {
34 jest.mock(shimServerStreamConfigPath, () => config);
35 jest.mock(shimServerFormatConfigPath, () => config);
36 jest.mock('react-server/src/ReactFlightServerBundlerConfigCustom', () => ({
37 + isModuleReference: config.isModuleReference,
38 resolveModuleMetaData: config.resolveModuleMetaData,
39 }));
40 jest.mock(shimFlightServerConfigPath, () =>
scripts/rollup/bundles.js
+11 -2
@@ -301,6 +301,7 @@ const bundles = [
301 'react',
302 'react-dom/server',
303 'ReactFlightDOMRelayServerIntegration',
304 + 'JSResourceReference',
305 ],
306 },
307 {
@@ -308,7 +309,11 @@ const bundles = [
309 moduleType: RENDERER,
310 entry: 'react-transport-dom-relay/server-runtime',
311 global: 'ReactFlightDOMRelayServerRuntime',
311 - externals: ['react', 'ReactFlightDOMRelayServerIntegration'],
312 + externals: [
313 + 'react',
314 + 'ReactFlightDOMRelayServerIntegration',
315 + 'JSResourceReference',
316 + ],
317 },
318
319 /******* React DOM Flight Client Relay *******/
@@ -317,7 +322,11 @@ const bundles = [
322 moduleType: RENDERER,
323 entry: 'react-transport-dom-relay',
324 global: 'ReactFlightDOMRelayClient',
320 - externals: ['react', 'ReactFlightDOMRelayClientIntegration'],
325 + externals: [
326 + 'react',
327 + 'ReactFlightDOMRelayClientIntegration',
328 + 'JSResourceReference',
329 + ],
330 },
331
332 /******* React ART *******/