@samitouri / QOS-React-2 / commits / 1bed20731f

Add a module map option to the Webpack Flight Client (#24629)

On the server we have a similar translation map from the file path that the loader uses to the refer to the original module and to the bundled module ID. The Flight server is optimized to emit the smallest format for the client. However during SSR, the same client component might go by a different module ID since it's a different bundle than the client bundle. This provides an option to add a translation map from client ID to SSR ID when reading the Flight stream. Ideally we should have a special SSR Flight Client that takes this option but for now we only have one Client for both.

Sebastian Markbåge committed May 27, 2022 at 16:16 UTC 1bed20731fa2789c46d7bff89cd44c3ea9d51abf
10 files changed +137 -14
packages/react-client/src/ReactFlightClient.js
+8 -2
@@ -15,6 +15,7 @@ import type {
15 ModuleMetaData,
16 UninitializedModel,
17 Response,
18 + BundlerConfig,
19 } from './ReactFlightClientHostConfig';
20
21 import {
@@ -97,6 +98,7 @@ Chunk.prototype.then = function<T>(resolve: () => mixed) {
98 };
99
100 export type ResponseBase = {
101 + _bundlerConfig: BundlerConfig,
102 _chunks: Map<number, SomeChunk<any>>,
103 readRoot<T>(): T,
104 ...
@@ -338,9 +340,10 @@ export function parseModelTuple(
340 return value;
341 }
342
341 -export function createResponse(): ResponseBase {
343 +export function createResponse(bundlerConfig: BundlerConfig): ResponseBase {
344 const chunks: Map<number, SomeChunk<any>> = new Map();
345 const response = {
346 + _bundlerConfig: bundlerConfig,
347 _chunks: chunks,
348 readRoot: readRoot,
349 };
@@ -384,7 +387,10 @@ export function resolveModule(
387 const chunks = response._chunks;
388 const chunk = chunks.get(id);
389 const moduleMetaData: ModuleMetaData = parseModel(response, model);
387 - const moduleReference = resolveModuleReference(moduleMetaData);
390 + const moduleReference = resolveModuleReference(
391 + response._bundlerConfig,
392 + moduleMetaData,
393 + );
394
395 // TODO: Add an option to encode modules that are lazy loaded.
396 // For now we preload all modules as early as possible since it's likely
packages/react-client/src/ReactFlightClientStream.js
+4 -2
@@ -9,6 +9,8 @@
9
10 import type {Response} from './ReactFlightClientHostConfigStream';
11
12 +import type {BundlerConfig} from './ReactFlightClientHostConfig';
13 +
14 import {
15 resolveModule,
16 resolveModel,
@@ -121,11 +123,11 @@ function createFromJSONCallback(response: Response) {
123 };
124 }
125
124 -export function createResponse(): Response {
126 +export function createResponse(bundlerConfig: BundlerConfig): Response {
127 // NOTE: CHECK THE COMPILER OUTPUT EACH TIME YOU CHANGE THIS.
128 // It should be inlined to one object literal but minor changes can break it.
129 const stringDecoder = supportsBinaryStreams ? createStringDecoder() : null;
128 - const response: any = createResponseBase();
130 + const response: any = createResponseBase(bundlerConfig);
131 response._partialRow = '';
132 if (supportsBinaryStreams) {
133 response._stringDecoder = stringDecoder;
packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js
+1
@@ -26,6 +26,7 @@
26 declare var $$$hostConfig: any;
27
28 export type Response = any;
29 +export opaque type BundlerConfig = mixed; // eslint-disable-line no-undef
30 export opaque type ModuleMetaData = mixed; // eslint-disable-line no-undef
31 export opaque type ModuleReference<T> = mixed; // eslint-disable-line no-undef
32 export const resolveModuleReference = $$$hostConfig.resolveModuleReference;
packages/react-noop-renderer/src/ReactNoopFlightClient.js
+2 -2
@@ -22,7 +22,7 @@ type Source = Array<string>;
22
23 const {createResponse, processStringChunk, close} = ReactFlightClient({
24 supportsBinaryStreams: false,
25 - resolveModuleReference(idx: string) {
25 + resolveModuleReference(bundlerConfig: null, idx: string) {
26 return idx;
27 },
28 preloadModule(idx: string) {},
@@ -35,7 +35,7 @@ const {createResponse, processStringChunk, close} = ReactFlightClient({
35 });
36
37 function read<T>(source: Source): T {
38 - const response = createResponse(source);
38 + const response = createResponse(source, null);
39 for (let i = 0; i < source.length; i++) {
40 processStringChunk(response, source[i], 0);
41 }
packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js
+13 -1
@@ -11,6 +11,8 @@ import type {JSONValue, ResponseBase} from 'react-client/src/ReactFlightClient';
11
12 import type {JSResourceReference} from 'JSResourceReference';
13
14 +import type {ModuleMetaData} from 'ReactFlightDOMRelayClientIntegration';
15 +
16 export type ModuleReference<T> = JSResourceReference<T>;
17
18 import {
@@ -19,19 +21,29 @@ import {
21 } from 'react-client/src/ReactFlightClient';
22
23 export {
22 - resolveModuleReference,
24 preloadModule,
25 requireModule,
26 } from 'ReactFlightDOMRelayClientIntegration';
27
28 +import {resolveModuleReference as resolveModuleReferenceImpl} from 'ReactFlightDOMRelayClientIntegration';
29 +
30 import isArray from 'shared/isArray';
31
32 export type {ModuleMetaData} from 'ReactFlightDOMRelayClientIntegration';
33
34 +export type BundlerConfig = null;
35 +
36 export type UninitializedModel = JSONValue;
37
38 export type Response = ResponseBase;
39
40 +export function resolveModuleReference<T>(
41 + bundlerConfig: BundlerConfig,
42 + moduleData: ModuleMetaData,
43 +): ModuleReference<T> {
44 + return resolveModuleReferenceImpl(moduleData);
45 +}
46 +
47 function parseModelRecursively(response: Response, parentObj, value) {
48 if (typeof value === 'string') {
49 return parseModelString(response, parentObj, value);
packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js
+1 -1
@@ -31,7 +31,7 @@ describe('ReactFlightDOMRelay', () => {
31 });
32
33 function readThrough(data) {
34 - const response = ReactDOMFlightRelayClient.createResponse();
34 + const response = ReactDOMFlightRelayClient.createResponse(null);
35 for (let i = 0; i < data.length; i++) {
36 const chunk = data[i];
37 ReactDOMFlightRelayClient.resolveRow(response, chunk);
packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js
+12
@@ -7,6 +7,14 @@
7 * @flow
8 */
9
10 +export type WebpackSSRMap = {
11 + [clientId: string]: {
12 + [clientExportName: string]: ModuleMetaData,
13 + },
14 +};
15 +
16 +export type BundlerConfig = null | WebpackSSRMap;
17 +
18 export opaque type ModuleMetaData = {
19 id: string,
20 chunks: Array<string>,
@@ -17,8 +25,12 @@ export opaque type ModuleMetaData = {
25 export opaque type ModuleReference<T> = ModuleMetaData;
26
27 export function resolveModuleReference<T>(
28 + bundlerConfig: BundlerConfig,
29 moduleData: ModuleMetaData,
30 ): ModuleReference<T> {
31 + if (bundlerConfig) {
32 + return bundlerConfig[moduleData.id][moduleData.name];
33 + }
34 return moduleData;
35 }
36
packages/react-server-dom-webpack/src/ReactFlightDOMClient.js
+24 -5
@@ -9,6 +9,8 @@
9
10 import type {Response as FlightResponse} from 'react-client/src/ReactFlightClientStream';
11
12 +import type {BundlerConfig} from './ReactFlightClientWebpackBundlerConfig';
13 +
14 import {
15 createResponse,
16 reportGlobalError,
@@ -17,6 +19,10 @@ import {
19 close,
20 } from 'react-client/src/ReactFlightClientStream';
21
22 +export type Options = {
23 + moduleMap?: BundlerConfig,
24 +};
25 +
26 function startReadingFromStream(
27 response: FlightResponse,
28 stream: ReadableStream,
@@ -37,16 +43,24 @@ function startReadingFromStream(
43 reader.read().then(progress, error);
44 }
45
40 -function createFromReadableStream(stream: ReadableStream): FlightResponse {
41 - const response: FlightResponse = createResponse();
46 +function createFromReadableStream(
47 + stream: ReadableStream,
48 + options?: Options,
49 +): FlightResponse {
50 + const response: FlightResponse = createResponse(
51 + options && options.moduleMap ? options.moduleMap : null,
52 + );
53 startReadingFromStream(response, stream);
54 return response;
55 }
56
57 function createFromFetch(
58 promiseForResponse: Promise<Response>,
59 + options?: Options,
60 ): FlightResponse {
49 - const response: FlightResponse = createResponse();
61 + const response: FlightResponse = createResponse(
62 + options && options.moduleMap ? options.moduleMap : null,
63 + );
64 promiseForResponse.then(
65 function(r) {
66 startReadingFromStream(response, (r.body: any));
@@ -58,8 +72,13 @@ function createFromFetch(
72 return response;
73 }
74
61 -function createFromXHR(request: XMLHttpRequest): FlightResponse {
62 - const response: FlightResponse = createResponse();
75 +function createFromXHR(
76 + request: XMLHttpRequest,
77 + options?: Options,
78 +): FlightResponse {
79 + const response: FlightResponse = createResponse(
80 + options && options.moduleMap ? options.moduleMap : null,
81 + );
82 let processedLength = 0;
83 function progress(e: ProgressEvent): void {
84 const chunk = request.responseText;
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+59
@@ -24,6 +24,7 @@ global.__webpack_require__ = function(id) {
24 let act;
25 let React;
26 let ReactDOMClient;
27 +let ReactDOMServer;
28 let ReactServerDOMWriter;
29 let ReactServerDOMReader;
30
@@ -35,6 +36,7 @@ describe('ReactFlightDOMBrowser', () => {
36 act = require('jest-react').act;
37 React = require('react');
38 ReactDOMClient = require('react-dom/client');
39 + ReactDOMServer = require('react-dom/server.browser');
40 ReactServerDOMWriter = require('react-server-dom-webpack/writer.browser.server');
41 ReactServerDOMReader = require('react-server-dom-webpack');
42 });
@@ -69,6 +71,18 @@ describe('ReactFlightDOMBrowser', () => {
71 }
72 }
73
74 + async function readResult(stream) {
75 + const reader = stream.getReader();
76 + let result = '';
77 + while (true) {
78 + const {done, value} = await reader.read();
79 + if (done) {
80 + return result;
81 + }
82 + result += Buffer.from(value).toString('utf8');
83 + }
84 + }
85 +
86 function makeDelayedText(Model) {
87 let error, _resolve, _reject;
88 let promise = new Promise((resolve, reject) => {
@@ -453,4 +467,49 @@ describe('ReactFlightDOMBrowser', () => {
467 // Final pending chunk is written; stream should be closed.
468 expect(isDone).toBeTruthy();
469 });
470 +
471 + it('should allow an alternative module mapping to be used for SSR', async () => {
472 + function ClientComponent() {
473 + return <span>Client Component</span>;
474 + }
475 + // The Client build may not have the same IDs as the Server bundles for the same
476 + // component.
477 + const ClientComponentOnTheClient = moduleReference(ClientComponent);
478 + const ClientComponentOnTheServer = moduleReference(ClientComponent);
479 +
480 + // In the SSR bundle this module won't exist. We simulate this by deleting it.
481 + const clientId = webpackMap[ClientComponentOnTheClient.filepath].default.id;
482 + delete webpackModules[clientId];
483 +
484 + // Instead, we have to provide a translation from the client meta data to the SSR
485 + // meta data.
486 + const ssrMetaData = webpackMap[ClientComponentOnTheServer.filepath].default;
487 + const translationMap = {
488 + [clientId]: {
489 + d: ssrMetaData,
490 + },
491 + };
492 +
493 + function App() {
494 + return <ClientComponentOnTheClient />;
495 + }
496 +
497 + const stream = ReactServerDOMWriter.renderToReadableStream(
498 + <App />,
499 + webpackMap,
500 + );
501 + const response = ReactServerDOMReader.createFromReadableStream(stream, {
502 + moduleMap: translationMap,
503 + });
504 +
505 + function ClientRoot() {
506 + return response.readRoot();
507 + }
508 +
509 + const ssrStream = await ReactDOMServer.renderToReadableStream(
510 + <ClientRoot />,
511 + );
512 + const result = await readResult(ssrStream);
513 + expect(result).toEqual('<span>Client Component</span>');
514 + });
515 });
packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js
+13 -1
@@ -11,6 +11,8 @@ import type {JSONValue, ResponseBase} from 'react-client/src/ReactFlightClient';
11
12 import type {JSResourceReference} from 'JSResourceReference';
13
14 +import type {ModuleMetaData} from 'ReactFlightNativeRelayClientIntegration';
15 +
16 export type ModuleReference<T> = JSResourceReference<T>;
17
18 import {
@@ -19,19 +21,29 @@ import {
21 } from 'react-client/src/ReactFlightClient';
22
23 export {
22 - resolveModuleReference,
24 preloadModule,
25 requireModule,
26 } from 'ReactFlightNativeRelayClientIntegration';
27
28 +import {resolveModuleReference as resolveModuleReferenceImpl} from 'ReactFlightNativeRelayClientIntegration';
29 +
30 import isArray from 'shared/isArray';
31
32 export type {ModuleMetaData} from 'ReactFlightNativeRelayClientIntegration';
33
34 +export type BundlerConfig = null;
35 +
36 export type UninitializedModel = JSONValue;
37
38 export type Response = ResponseBase;
39
40 +export function resolveModuleReference<T>(
41 + bundlerConfig: BundlerConfig,
42 + moduleData: ModuleMetaData,
43 +): ModuleReference<T> {
44 + return resolveModuleReferenceImpl(moduleData);
45 +}
46 +
47 function parseModelRecursively(response: Response, parentObj, value) {
48 if (typeof value === 'string') {
49 return parseModelString(response, parentObj, value);