Skip to content

Commit 1bed207

Browse files
authored
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.
1 parent 3133dfa commit 1bed207

File tree

10 files changed

+137
-14
lines changed

10 files changed

+137
-14
lines changed

packages/react-client/src/ReactFlightClient.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
ModuleMetaData,
1616
UninitializedModel,
1717
Response,
18+
BundlerConfig,
1819
} from './ReactFlightClientHostConfig';
1920

2021
import {
@@ -97,6 +98,7 @@ Chunk.prototype.then = function<T>(resolve: () => mixed) {
9798
};
9899

99100
export type ResponseBase = {
101+
_bundlerConfig: BundlerConfig,
100102
_chunks: Map<number, SomeChunk<any>>,
101103
readRoot<T>(): T,
102104
...
@@ -338,9 +340,10 @@ export function parseModelTuple(
338340
return value;
339341
}
340342

341-
export function createResponse(): ResponseBase {
343+
export function createResponse(bundlerConfig: BundlerConfig): ResponseBase {
342344
const chunks: Map<number, SomeChunk<any>> = new Map();
343345
const response = {
346+
_bundlerConfig: bundlerConfig,
344347
_chunks: chunks,
345348
readRoot: readRoot,
346349
};
@@ -384,7 +387,10 @@ export function resolveModule(
384387
const chunks = response._chunks;
385388
const chunk = chunks.get(id);
386389
const moduleMetaData: ModuleMetaData = parseModel(response, model);
387-
const moduleReference = resolveModuleReference(moduleMetaData);
390+
const moduleReference = resolveModuleReference(
391+
response._bundlerConfig,
392+
moduleMetaData,
393+
);
388394

389395
// TODO: Add an option to encode modules that are lazy loaded.
390396
// For now we preload all modules as early as possible since it's likely

packages/react-client/src/ReactFlightClientStream.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
import type {Response} from './ReactFlightClientHostConfigStream';
1111

12+
import type {BundlerConfig} from './ReactFlightClientHostConfig';
13+
1214
import {
1315
resolveModule,
1416
resolveModel,
@@ -121,11 +123,11 @@ function createFromJSONCallback(response: Response) {
121123
};
122124
}
123125

124-
export function createResponse(): Response {
126+
export function createResponse(bundlerConfig: BundlerConfig): Response {
125127
// NOTE: CHECK THE COMPILER OUTPUT EACH TIME YOU CHANGE THIS.
126128
// It should be inlined to one object literal but minor changes can break it.
127129
const stringDecoder = supportsBinaryStreams ? createStringDecoder() : null;
128-
const response: any = createResponseBase();
130+
const response: any = createResponseBase(bundlerConfig);
129131
response._partialRow = '';
130132
if (supportsBinaryStreams) {
131133
response._stringDecoder = stringDecoder;

packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
declare var $$$hostConfig: any;
2727

2828
export type Response = any;
29+
export opaque type BundlerConfig = mixed; // eslint-disable-line no-undef
2930
export opaque type ModuleMetaData = mixed; // eslint-disable-line no-undef
3031
export opaque type ModuleReference<T> = mixed; // eslint-disable-line no-undef
3132
export const resolveModuleReference = $$$hostConfig.resolveModuleReference;

packages/react-noop-renderer/src/ReactNoopFlightClient.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type Source = Array<string>;
2222

2323
const {createResponse, processStringChunk, close} = ReactFlightClient({
2424
supportsBinaryStreams: false,
25-
resolveModuleReference(idx: string) {
25+
resolveModuleReference(bundlerConfig: null, idx: string) {
2626
return idx;
2727
},
2828
preloadModule(idx: string) {},
@@ -35,7 +35,7 @@ const {createResponse, processStringChunk, close} = ReactFlightClient({
3535
});
3636

3737
function read<T>(source: Source): T {
38-
const response = createResponse(source);
38+
const response = createResponse(source, null);
3939
for (let i = 0; i < source.length; i++) {
4040
processStringChunk(response, source[i], 0);
4141
}

packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type {JSONValue, ResponseBase} from 'react-client/src/ReactFlightClient';
1111

1212
import type {JSResourceReference} from 'JSResourceReference';
1313

14+
import type {ModuleMetaData} from 'ReactFlightDOMRelayClientIntegration';
15+
1416
export type ModuleReference<T> = JSResourceReference<T>;
1517

1618
import {
@@ -19,19 +21,29 @@ import {
1921
} from 'react-client/src/ReactFlightClient';
2022

2123
export {
22-
resolveModuleReference,
2324
preloadModule,
2425
requireModule,
2526
} from 'ReactFlightDOMRelayClientIntegration';
2627

28+
import {resolveModuleReference as resolveModuleReferenceImpl} from 'ReactFlightDOMRelayClientIntegration';
29+
2730
import isArray from 'shared/isArray';
2831

2932
export type {ModuleMetaData} from 'ReactFlightDOMRelayClientIntegration';
3033

34+
export type BundlerConfig = null;
35+
3136
export type UninitializedModel = JSONValue;
3237

3338
export type Response = ResponseBase;
3439

40+
export function resolveModuleReference<T>(
41+
bundlerConfig: BundlerConfig,
42+
moduleData: ModuleMetaData,
43+
): ModuleReference<T> {
44+
return resolveModuleReferenceImpl(moduleData);
45+
}
46+
3547
function parseModelRecursively(response: Response, parentObj, value) {
3648
if (typeof value === 'string') {
3749
return parseModelString(response, parentObj, value);

packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('ReactFlightDOMRelay', () => {
3131
});
3232

3333
function readThrough(data) {
34-
const response = ReactDOMFlightRelayClient.createResponse();
34+
const response = ReactDOMFlightRelayClient.createResponse(null);
3535
for (let i = 0; i < data.length; i++) {
3636
const chunk = data[i];
3737
ReactDOMFlightRelayClient.resolveRow(response, chunk);

packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js

+12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
* @flow
88
*/
99

10+
export type WebpackSSRMap = {
11+
[clientId: string]: {
12+
[clientExportName: string]: ModuleMetaData,
13+
},
14+
};
15+
16+
export type BundlerConfig = null | WebpackSSRMap;
17+
1018
export opaque type ModuleMetaData = {
1119
id: string,
1220
chunks: Array<string>,
@@ -17,8 +25,12 @@ export opaque type ModuleMetaData = {
1725
export opaque type ModuleReference<T> = ModuleMetaData;
1826

1927
export function resolveModuleReference<T>(
28+
bundlerConfig: BundlerConfig,
2029
moduleData: ModuleMetaData,
2130
): ModuleReference<T> {
31+
if (bundlerConfig) {
32+
return bundlerConfig[moduleData.id][moduleData.name];
33+
}
2234
return moduleData;
2335
}
2436

packages/react-server-dom-webpack/src/ReactFlightDOMClient.js

+24-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
import type {Response as FlightResponse} from 'react-client/src/ReactFlightClientStream';
1111

12+
import type {BundlerConfig} from './ReactFlightClientWebpackBundlerConfig';
13+
1214
import {
1315
createResponse,
1416
reportGlobalError,
@@ -17,6 +19,10 @@ import {
1719
close,
1820
} from 'react-client/src/ReactFlightClientStream';
1921

22+
export type Options = {
23+
moduleMap?: BundlerConfig,
24+
};
25+
2026
function startReadingFromStream(
2127
response: FlightResponse,
2228
stream: ReadableStream,
@@ -37,16 +43,24 @@ function startReadingFromStream(
3743
reader.read().then(progress, error);
3844
}
3945

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+
);
4253
startReadingFromStream(response, stream);
4354
return response;
4455
}
4556

4657
function createFromFetch(
4758
promiseForResponse: Promise<Response>,
59+
options?: Options,
4860
): FlightResponse {
49-
const response: FlightResponse = createResponse();
61+
const response: FlightResponse = createResponse(
62+
options && options.moduleMap ? options.moduleMap : null,
63+
);
5064
promiseForResponse.then(
5165
function(r) {
5266
startReadingFromStream(response, (r.body: any));
@@ -58,8 +72,13 @@ function createFromFetch(
5872
return response;
5973
}
6074

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+
);
6382
let processedLength = 0;
6483
function progress(e: ProgressEvent): void {
6584
const chunk = request.responseText;

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js

+59
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ global.__webpack_require__ = function(id) {
2424
let act;
2525
let React;
2626
let ReactDOMClient;
27+
let ReactDOMServer;
2728
let ReactServerDOMWriter;
2829
let ReactServerDOMReader;
2930

@@ -35,6 +36,7 @@ describe('ReactFlightDOMBrowser', () => {
3536
act = require('jest-react').act;
3637
React = require('react');
3738
ReactDOMClient = require('react-dom/client');
39+
ReactDOMServer = require('react-dom/server.browser');
3840
ReactServerDOMWriter = require('react-server-dom-webpack/writer.browser.server');
3941
ReactServerDOMReader = require('react-server-dom-webpack');
4042
});
@@ -69,6 +71,18 @@ describe('ReactFlightDOMBrowser', () => {
6971
}
7072
}
7173

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+
7286
function makeDelayedText(Model) {
7387
let error, _resolve, _reject;
7488
let promise = new Promise((resolve, reject) => {
@@ -453,4 +467,49 @@ describe('ReactFlightDOMBrowser', () => {
453467
// Final pending chunk is written; stream should be closed.
454468
expect(isDone).toBeTruthy();
455469
});
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+
});
456515
});

packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type {JSONValue, ResponseBase} from 'react-client/src/ReactFlightClient';
1111

1212
import type {JSResourceReference} from 'JSResourceReference';
1313

14+
import type {ModuleMetaData} from 'ReactFlightNativeRelayClientIntegration';
15+
1416
export type ModuleReference<T> = JSResourceReference<T>;
1517

1618
import {
@@ -19,19 +21,29 @@ import {
1921
} from 'react-client/src/ReactFlightClient';
2022

2123
export {
22-
resolveModuleReference,
2324
preloadModule,
2425
requireModule,
2526
} from 'ReactFlightNativeRelayClientIntegration';
2627

28+
import {resolveModuleReference as resolveModuleReferenceImpl} from 'ReactFlightNativeRelayClientIntegration';
29+
2730
import isArray from 'shared/isArray';
2831

2932
export type {ModuleMetaData} from 'ReactFlightNativeRelayClientIntegration';
3033

34+
export type BundlerConfig = null;
35+
3136
export type UninitializedModel = JSONValue;
3237

3338
export type Response = ResponseBase;
3439

40+
export function resolveModuleReference<T>(
41+
bundlerConfig: BundlerConfig,
42+
moduleData: ModuleMetaData,
43+
): ModuleReference<T> {
44+
return resolveModuleReferenceImpl(moduleData);
45+
}
46+
3547
function parseModelRecursively(response: Response, parentObj, value) {
3648
if (typeof value === 'string') {
3749
return parseModelString(response, parentObj, value);

0 commit comments

Comments
 (0)