diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index dabb215c708f6..13fb0897e747b 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -15,6 +15,7 @@ import type { ModuleMetaData, UninitializedModel, Response, + BundlerConfig, } from './ReactFlightClientHostConfig'; import { @@ -97,6 +98,7 @@ Chunk.prototype.then = function(resolve: () => mixed) { }; export type ResponseBase = { + _bundlerConfig: BundlerConfig, _chunks: Map>, readRoot(): T, ... @@ -338,9 +340,10 @@ export function parseModelTuple( return value; } -export function createResponse(): ResponseBase { +export function createResponse(bundlerConfig: BundlerConfig): ResponseBase { const chunks: Map> = new Map(); const response = { + _bundlerConfig: bundlerConfig, _chunks: chunks, readRoot: readRoot, }; @@ -384,7 +387,10 @@ export function resolveModule( const chunks = response._chunks; const chunk = chunks.get(id); const moduleMetaData: ModuleMetaData = parseModel(response, model); - const moduleReference = resolveModuleReference(moduleMetaData); + const moduleReference = resolveModuleReference( + response._bundlerConfig, + moduleMetaData, + ); // TODO: Add an option to encode modules that are lazy loaded. // For now we preload all modules as early as possible since it's likely diff --git a/packages/react-client/src/ReactFlightClientStream.js b/packages/react-client/src/ReactFlightClientStream.js index 8af1734de6b71..ed27a10f6e339 100644 --- a/packages/react-client/src/ReactFlightClientStream.js +++ b/packages/react-client/src/ReactFlightClientStream.js @@ -9,6 +9,8 @@ import type {Response} from './ReactFlightClientHostConfigStream'; +import type {BundlerConfig} from './ReactFlightClientHostConfig'; + import { resolveModule, resolveModel, @@ -121,11 +123,11 @@ function createFromJSONCallback(response: Response) { }; } -export function createResponse(): Response { +export function createResponse(bundlerConfig: BundlerConfig): Response { // NOTE: CHECK THE COMPILER OUTPUT EACH TIME YOU CHANGE THIS. // It should be inlined to one object literal but minor changes can break it. const stringDecoder = supportsBinaryStreams ? createStringDecoder() : null; - const response: any = createResponseBase(); + const response: any = createResponseBase(bundlerConfig); response._partialRow = ''; if (supportsBinaryStreams) { response._stringDecoder = stringDecoder; diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js index 5f0b9d2c7114a..829e5e65e9997 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js @@ -26,6 +26,7 @@ declare var $$$hostConfig: any; export type Response = any; +export opaque type BundlerConfig = mixed; // eslint-disable-line no-undef export opaque type ModuleMetaData = mixed; // eslint-disable-line no-undef export opaque type ModuleReference = mixed; // eslint-disable-line no-undef export const resolveModuleReference = $$$hostConfig.resolveModuleReference; diff --git a/packages/react-noop-renderer/src/ReactNoopFlightClient.js b/packages/react-noop-renderer/src/ReactNoopFlightClient.js index df586c6efb2cb..52af83c5ef62a 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightClient.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightClient.js @@ -22,7 +22,7 @@ type Source = Array; const {createResponse, processStringChunk, close} = ReactFlightClient({ supportsBinaryStreams: false, - resolveModuleReference(idx: string) { + resolveModuleReference(bundlerConfig: null, idx: string) { return idx; }, preloadModule(idx: string) {}, @@ -35,7 +35,7 @@ const {createResponse, processStringChunk, close} = ReactFlightClient({ }); function read(source: Source): T { - const response = createResponse(source); + const response = createResponse(source, null); for (let i = 0; i < source.length; i++) { processStringChunk(response, source[i], 0); } diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js index 30718865ccdad..308bbef0ed234 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js @@ -11,6 +11,8 @@ import type {JSONValue, ResponseBase} from 'react-client/src/ReactFlightClient'; import type {JSResourceReference} from 'JSResourceReference'; +import type {ModuleMetaData} from 'ReactFlightDOMRelayClientIntegration'; + export type ModuleReference = JSResourceReference; import { @@ -19,19 +21,29 @@ import { } from 'react-client/src/ReactFlightClient'; export { - resolveModuleReference, preloadModule, requireModule, } from 'ReactFlightDOMRelayClientIntegration'; +import {resolveModuleReference as resolveModuleReferenceImpl} from 'ReactFlightDOMRelayClientIntegration'; + import isArray from 'shared/isArray'; export type {ModuleMetaData} from 'ReactFlightDOMRelayClientIntegration'; +export type BundlerConfig = null; + export type UninitializedModel = JSONValue; export type Response = ResponseBase; +export function resolveModuleReference( + bundlerConfig: BundlerConfig, + moduleData: ModuleMetaData, +): ModuleReference { + return resolveModuleReferenceImpl(moduleData); +} + function parseModelRecursively(response: Response, parentObj, value) { if (typeof value === 'string') { return parseModelString(response, parentObj, value); diff --git a/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js b/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js index daf01d5d69bef..0444add037907 100644 --- a/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js +++ b/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js @@ -31,7 +31,7 @@ describe('ReactFlightDOMRelay', () => { }); function readThrough(data) { - const response = ReactDOMFlightRelayClient.createResponse(); + const response = ReactDOMFlightRelayClient.createResponse(null); for (let i = 0; i < data.length; i++) { const chunk = data[i]; ReactDOMFlightRelayClient.resolveRow(response, chunk); diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js b/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js index f3c4e1bf1c16d..d36642532f56a 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js +++ b/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js @@ -7,6 +7,14 @@ * @flow */ +export type WebpackSSRMap = { + [clientId: string]: { + [clientExportName: string]: ModuleMetaData, + }, +}; + +export type BundlerConfig = null | WebpackSSRMap; + export opaque type ModuleMetaData = { id: string, chunks: Array, @@ -17,8 +25,12 @@ export opaque type ModuleMetaData = { export opaque type ModuleReference = ModuleMetaData; export function resolveModuleReference( + bundlerConfig: BundlerConfig, moduleData: ModuleMetaData, ): ModuleReference { + if (bundlerConfig) { + return bundlerConfig[moduleData.id][moduleData.name]; + } return moduleData; } diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClient.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClient.js index 9c9d17c11f532..d8b5def41e041 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClient.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClient.js @@ -9,6 +9,8 @@ import type {Response as FlightResponse} from 'react-client/src/ReactFlightClientStream'; +import type {BundlerConfig} from './ReactFlightClientWebpackBundlerConfig'; + import { createResponse, reportGlobalError, @@ -17,6 +19,10 @@ import { close, } from 'react-client/src/ReactFlightClientStream'; +export type Options = { + moduleMap?: BundlerConfig, +}; + function startReadingFromStream( response: FlightResponse, stream: ReadableStream, @@ -37,16 +43,24 @@ function startReadingFromStream( reader.read().then(progress, error); } -function createFromReadableStream(stream: ReadableStream): FlightResponse { - const response: FlightResponse = createResponse(); +function createFromReadableStream( + stream: ReadableStream, + options?: Options, +): FlightResponse { + const response: FlightResponse = createResponse( + options && options.moduleMap ? options.moduleMap : null, + ); startReadingFromStream(response, stream); return response; } function createFromFetch( promiseForResponse: Promise, + options?: Options, ): FlightResponse { - const response: FlightResponse = createResponse(); + const response: FlightResponse = createResponse( + options && options.moduleMap ? options.moduleMap : null, + ); promiseForResponse.then( function(r) { startReadingFromStream(response, (r.body: any)); @@ -58,8 +72,13 @@ function createFromFetch( return response; } -function createFromXHR(request: XMLHttpRequest): FlightResponse { - const response: FlightResponse = createResponse(); +function createFromXHR( + request: XMLHttpRequest, + options?: Options, +): FlightResponse { + const response: FlightResponse = createResponse( + options && options.moduleMap ? options.moduleMap : null, + ); let processedLength = 0; function progress(e: ProgressEvent): void { const chunk = request.responseText; diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index c7b97707c166f..3337b19d11fcd 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -24,6 +24,7 @@ global.__webpack_require__ = function(id) { let act; let React; let ReactDOMClient; +let ReactDOMServer; let ReactServerDOMWriter; let ReactServerDOMReader; @@ -35,6 +36,7 @@ describe('ReactFlightDOMBrowser', () => { act = require('jest-react').act; React = require('react'); ReactDOMClient = require('react-dom/client'); + ReactDOMServer = require('react-dom/server.browser'); ReactServerDOMWriter = require('react-server-dom-webpack/writer.browser.server'); ReactServerDOMReader = require('react-server-dom-webpack'); }); @@ -69,6 +71,18 @@ describe('ReactFlightDOMBrowser', () => { } } + async function readResult(stream) { + const reader = stream.getReader(); + let result = ''; + while (true) { + const {done, value} = await reader.read(); + if (done) { + return result; + } + result += Buffer.from(value).toString('utf8'); + } + } + function makeDelayedText(Model) { let error, _resolve, _reject; let promise = new Promise((resolve, reject) => { @@ -453,4 +467,49 @@ describe('ReactFlightDOMBrowser', () => { // Final pending chunk is written; stream should be closed. expect(isDone).toBeTruthy(); }); + + it('should allow an alternative module mapping to be used for SSR', async () => { + function ClientComponent() { + return Client Component; + } + // The Client build may not have the same IDs as the Server bundles for the same + // component. + const ClientComponentOnTheClient = moduleReference(ClientComponent); + const ClientComponentOnTheServer = moduleReference(ClientComponent); + + // In the SSR bundle this module won't exist. We simulate this by deleting it. + const clientId = webpackMap[ClientComponentOnTheClient.filepath].default.id; + delete webpackModules[clientId]; + + // Instead, we have to provide a translation from the client meta data to the SSR + // meta data. + const ssrMetaData = webpackMap[ClientComponentOnTheServer.filepath].default; + const translationMap = { + [clientId]: { + d: ssrMetaData, + }, + }; + + function App() { + return ; + } + + const stream = ReactServerDOMWriter.renderToReadableStream( + , + webpackMap, + ); + const response = ReactServerDOMReader.createFromReadableStream(stream, { + moduleMap: translationMap, + }); + + function ClientRoot() { + return response.readRoot(); + } + + const ssrStream = await ReactDOMServer.renderToReadableStream( + , + ); + const result = await readResult(ssrStream); + expect(result).toEqual('Client Component'); + }); }); diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js index eb6f0080e3718..83ab8800d4599 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js @@ -11,6 +11,8 @@ import type {JSONValue, ResponseBase} from 'react-client/src/ReactFlightClient'; import type {JSResourceReference} from 'JSResourceReference'; +import type {ModuleMetaData} from 'ReactFlightNativeRelayClientIntegration'; + export type ModuleReference = JSResourceReference; import { @@ -19,19 +21,29 @@ import { } from 'react-client/src/ReactFlightClient'; export { - resolveModuleReference, preloadModule, requireModule, } from 'ReactFlightNativeRelayClientIntegration'; +import {resolveModuleReference as resolveModuleReferenceImpl} from 'ReactFlightNativeRelayClientIntegration'; + import isArray from 'shared/isArray'; export type {ModuleMetaData} from 'ReactFlightNativeRelayClientIntegration'; +export type BundlerConfig = null; + export type UninitializedModel = JSONValue; export type Response = ResponseBase; +export function resolveModuleReference( + bundlerConfig: BundlerConfig, + moduleData: ModuleMetaData, +): ModuleReference { + return resolveModuleReferenceImpl(moduleData); +} + function parseModelRecursively(response: Response, parentObj, value) { if (typeof value === 'string') { return parseModelString(response, parentObj, value);