From 1f4d167bd972bf622d6975aab67da126e4583ec4 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 5 Sep 2023 13:43:05 -0700 Subject: [PATCH 1/2] During SSR we should preinitialize imports so they can begin to be fetched before bootstrap in the browser --- fixtures/flight/.nvmrc | 1 + fixtures/flight/server/global.js | 41 ++++- .../react-client/src/ReactFlightClient.js | 20 ++- .../forks/ReactFlightClientConfig.custom.js | 5 +- .../ReactFlightClientConfig.dom-browser.js | 4 +- .../forks/ReactFlightClientConfig.dom-bun.js | 4 +- ...eactFlightClientConfig.dom-edge-webpack.js | 4 +- .../ReactFlightClientConfig.dom-legacy.js | 4 +- ...eactFlightClientConfig.dom-node-webpack.js | 4 +- .../forks/ReactFlightClientConfig.dom-node.js | 3 +- .../src/shared/ReactFlightClientConfigDOM.js | 16 ++ .../src/shared/crossOriginStrings.js | 30 ++++ .../react-dom/src/shared/ReactDOMFloat.js | 59 ++++--- .../react-dom/src/shared/ReactDOMTypes.js | 8 +- .../src/ReactNoopFlightClient.js | 1 + ... => ReactFlightClientConfigBundlerNode.js} | 46 ++++-- ... ReactFlightClientConfigBundlerWebpack.js} | 104 ++++++++----- ...FlightClientConfigBundlerWebpackBrowser.js | 28 ++++ ...tFlightClientConfigBundlerWebpackServer.js | 12 ++ ...tFlightClientConfigTargetWebpackBrowser.js | 18 +++ ...ctFlightClientConfigTargetWebpackServer.js | 32 ++++ .../src/ReactFlightDOMClientBrowser.js | 2 + .../src/ReactFlightDOMClientEdge.js | 23 ++- .../src/ReactFlightDOMClientNode.js | 24 ++- .../ReactFlightServerConfigWebpackBundler.js | 25 ++- .../src/ReactFlightWebpackPlugin.js | 66 ++++++-- .../src/__tests__/ReactFlightDOM-test.js | 130 +++++++++------- .../src/__tests__/ReactFlightDOMEdge-test.js | 37 ++++- .../src/__tests__/ReactFlightDOMForm-test.js | 146 +++++++++++++++--- .../src/__tests__/ReactFlightDOMNode-test.js | 113 ++++++++++++-- .../src/__tests__/utils/WebpackMock.js | 21 ++- .../src/shared/ReactFlightImportMetadata.js | 43 ++++++ .../react/src/__tests__/ReactFetch-test.js | 10 +- .../src/__tests__/ReactFetchEdge-test.js | 10 +- scripts/flow/environment.js | 4 +- scripts/jest/setupHostConfigs.js | 3 + scripts/shared/inlinedHostConfigs.js | 11 +- 37 files changed, 872 insertions(+), 240 deletions(-) create mode 100644 fixtures/flight/.nvmrc create mode 100644 packages/react-dom-bindings/src/shared/crossOriginStrings.js rename packages/react-server-dom-webpack/src/{ReactFlightClientConfigNodeBundler.js => ReactFlightClientConfigBundlerNode.js} (73%) rename packages/react-server-dom-webpack/src/{ReactFlightClientConfigWebpackBundler.js => ReactFlightClientConfigBundlerWebpack.js} (68%) create mode 100644 packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackBrowser.js create mode 100644 packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer.js create mode 100644 packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackBrowser.js create mode 100644 packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer.js create mode 100644 packages/react-server-dom-webpack/src/shared/ReactFlightImportMetadata.js diff --git a/fixtures/flight/.nvmrc b/fixtures/flight/.nvmrc new file mode 100644 index 0000000000000..3f430af82b3df --- /dev/null +++ b/fixtures/flight/.nvmrc @@ -0,0 +1 @@ +v18 diff --git a/fixtures/flight/server/global.js b/fixtures/flight/server/global.js index 70ccb05ca83ac..421a7d7527a66 100644 --- a/fixtures/flight/server/global.js +++ b/fixtures/flight/server/global.js @@ -33,6 +33,7 @@ const compress = require('compression'); const chalk = require('chalk'); const express = require('express'); const http = require('http'); +const React = require('react'); const {renderToPipeableStream} = require('react-dom/server'); const {createFromNodeStream} = require('react-server-dom-webpack/client'); @@ -62,6 +63,11 @@ if (process.env.NODE_ENV === 'development') { webpackMiddleware(compiler, { publicPath: paths.publicUrlOrPath.slice(0, -1), serverSideRender: true, + headers: () => { + return { + 'Cache-Control': 'no-store, must-revalidate', + }; + }, }) ); app.use(webpackHotMiddleware(compiler)); @@ -121,12 +127,13 @@ app.all('/', async function (req, res, next) { buildPath = path.join(__dirname, '../build/'); } // Read the module map from the virtual file system. - const moduleMap = JSON.parse( + const ssrManifest = JSON.parse( await virtualFs.readFile( path.join(buildPath, 'react-ssr-manifest.json'), 'utf8' ) ); + // Read the entrypoints containing the initial JS to bootstrap everything. // For other pages, the chunks in the RSC payload are enough. const mainJSChunks = JSON.parse( @@ -138,15 +145,35 @@ app.all('/', async function (req, res, next) { // For HTML, we're a "client" emulator that runs the client code, // so we start by consuming the RSC payload. This needs a module // map that reverse engineers the client-side path to the SSR path. - const {root, formState} = await createFromNodeStream( - rscResponse, - moduleMap - ); + + // This is a bad hack to set the form state after SSR has started. It works + // because we block the root component until we have the form state and + // any form that reads it necessarily will come later. It also only works + // because the formstate type is an object which may change in the future + const lazyFormState = []; + + let cachedResult = null; + async function getRootAndFormState() { + const {root, formState} = await createFromNodeStream( + rscResponse, + ssrManifest + ); + // We shouldn't be assuming formState is an object type but at the moment + // we have no way of setting the form state from within the render + Object.assign(lazyFormState, formState); + return root; + } + let Root = () => { + if (!cachedResult) { + cachedResult = getRootAndFormState(); + } + return React.use(cachedResult); + }; // Render it into HTML by resolving the client components res.set('Content-type', 'text/html'); - const {pipe} = renderToPipeableStream(root, { + const {pipe} = renderToPipeableStream(React.createElement(Root), { bootstrapScripts: mainJSChunks, - experimental_formState: formState, + experimental_formState: lazyFormState, }); pipe(res); } catch (e) { diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index cfbe30e671c78..3fb97089f52be 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -13,8 +13,9 @@ import type {LazyComponent} from 'react/src/ReactLazy'; import type { ClientReference, ClientReferenceMetadata, - SSRManifest, + SSRModuleMap, StringDecoder, + ModuleLoading, } from './ReactFlightClientConfig'; import type { @@ -36,6 +37,7 @@ import { readPartialStringChunk, readFinalStringChunk, createStringDecoder, + prepareDestinationForModule, } from './ReactFlightClientConfig'; import {registerServerReference} from './ReactFlightReplyClient'; @@ -178,8 +180,10 @@ Chunk.prototype.then = function ( }; export type Response = { - _bundlerConfig: SSRManifest, + _bundlerConfig: SSRModuleMap, + _moduleLoading: ModuleLoading, _callServer: CallServerCallback, + _nonce: ?string, _chunks: Map>, _fromJSON: (key: string, value: JSONValue) => any, _stringDecoder: StringDecoder, @@ -706,13 +710,17 @@ function missingCall() { } export function createResponse( - bundlerConfig: SSRManifest, + bundlerConfig: SSRModuleMap, + moduleLoading: ModuleLoading, callServer: void | CallServerCallback, + nonce: void | string, ): Response { const chunks: Map> = new Map(); const response: Response = { _bundlerConfig: bundlerConfig, + _moduleLoading: moduleLoading, _callServer: callServer !== undefined ? callServer : missingCall, + _nonce: nonce, _chunks: chunks, _stringDecoder: createStringDecoder(), _fromJSON: (null: any), @@ -774,6 +782,12 @@ function resolveModule( clientReferenceMetadata, ); + prepareDestinationForModule( + response._moduleLoading, + response._nonce, + clientReferenceMetadata, + ); + // 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 // that we'll need them. diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.custom.js b/packages/react-client/src/forks/ReactFlightClientConfig.custom.js index b5de594d4d46f..152bfd8b1d51b 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.custom.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.custom.js @@ -25,7 +25,8 @@ declare var $$$config: any; -export opaque type SSRManifest = mixed; +export opaque type ModuleLoading = mixed; +export opaque type SSRModuleMap = mixed; export opaque type ServerManifest = mixed; export opaque type ServerReferenceId = string; export opaque type ClientReferenceMetadata = mixed; @@ -35,6 +36,8 @@ export const resolveServerReference = $$$config.resolveServerReference; export const preloadModule = $$$config.preloadModule; export const requireModule = $$$config.requireModule; export const dispatchHint = $$$config.dispatchHint; +export const prepareDestinationForModule = + $$$config.prepareDestinationForModule; export const usedWithSSR = true; export opaque type Source = mixed; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js index 52212d1e0c869..f17151a1a1fa6 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js @@ -8,6 +8,8 @@ */ export * from 'react-client/src/ReactFlightClientConfigBrowser'; -export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackBrowser'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackBrowser'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = false; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js index 0ad00d57cdac4..6b72b535dbd7f 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js @@ -11,7 +11,8 @@ export * from 'react-client/src/ReactFlightClientConfigBrowser'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export type Response = any; -export opaque type SSRManifest = mixed; +export opaque type ModuleLoading = mixed; +export opaque type SSRModuleMap = mixed; export opaque type ServerManifest = mixed; export opaque type ServerReferenceId = string; export opaque type ClientReferenceMetadata = mixed; @@ -20,4 +21,5 @@ export const resolveClientReference: any = null; export const resolveServerReference: any = null; export const preloadModule: any = null; export const requireModule: any = null; +export const prepareDestinationForModule: any = null; export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js index 212290670bd57..954ca1f2a9845 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js @@ -8,6 +8,8 @@ */ export * from 'react-client/src/ReactFlightClientConfigBrowser'; -export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js index 0ad00d57cdac4..6b72b535dbd7f 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js @@ -11,7 +11,8 @@ export * from 'react-client/src/ReactFlightClientConfigBrowser'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export type Response = any; -export opaque type SSRManifest = mixed; +export opaque type ModuleLoading = mixed; +export opaque type SSRModuleMap = mixed; export opaque type ServerManifest = mixed; export opaque type ServerReferenceId = string; export opaque type ClientReferenceMetadata = mixed; @@ -20,4 +21,5 @@ export const resolveClientReference: any = null; export const resolveServerReference: any = null; export const preloadModule: any = null; export const requireModule: any = null; +export const prepareDestinationForModule: any = null; export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js index 4df4617caec67..4b4d77ce0cc54 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js @@ -8,6 +8,8 @@ */ export * from 'react-client/src/ReactFlightClientConfigNode'; -export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js index bf0ddb29fa434..554ddfdc40a66 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js @@ -8,6 +8,7 @@ */ export * from 'react-client/src/ReactFlightClientConfigNode'; -export * from 'react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = true; diff --git a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js index 150f16f48d56f..48f4b24a362f2 100644 --- a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js +++ b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js @@ -15,6 +15,8 @@ import type {HintCode, HintModel} from '../server/ReactFlightServerConfigDOM'; import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher; +import {getCrossOriginString} from './crossOriginStrings'; + export function dispatchHint( code: Code, model: HintModel, @@ -110,3 +112,17 @@ export function dispatchHint( function refineModel(code: T, model: HintModel): HintModel { return model; } + +export function preinitScriptForSSR( + href: string, + nonce: ?string, + crossOrigin: ?string, +) { + const dispatcher = ReactDOMCurrentDispatcher.current; + if (dispatcher) { + dispatcher.preinitScript(href, { + crossOrigin: getCrossOriginString(crossOrigin), + nonce, + }); + } +} diff --git a/packages/react-dom-bindings/src/shared/crossOriginStrings.js b/packages/react-dom-bindings/src/shared/crossOriginStrings.js new file mode 100644 index 0000000000000..ebdb615f39808 --- /dev/null +++ b/packages/react-dom-bindings/src/shared/crossOriginStrings.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export opaque type CrossOriginString: string = string; + +export function getCrossOriginString(input: ?string): ?CrossOriginString { + if (typeof input === 'string') { + return input === 'use-credentials' ? input : ''; + } + return undefined; +} + +export function getCrossOriginStringAs( + as: ?string, + input: ?string, +): ?CrossOriginString { + if (as === 'font') { + return ''; + } + if (typeof input === 'string') { + return input === 'use-credentials' ? input : ''; + } + return undefined; +} diff --git a/packages/react-dom/src/shared/ReactDOMFloat.js b/packages/react-dom/src/shared/ReactDOMFloat.js index 34f36c541b0c9..74701e761179d 100644 --- a/packages/react-dom/src/shared/ReactDOMFloat.js +++ b/packages/react-dom/src/shared/ReactDOMFloat.js @@ -7,7 +7,6 @@ * @flow */ import type { - CrossOriginEnum, PreconnectOptions, PreloadOptions, PreloadModuleOptions, @@ -18,6 +17,11 @@ import type { import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; const Dispatcher = ReactDOMSharedInternals.Dispatcher; +import { + getCrossOriginString, + getCrossOriginStringAs, +} from 'react-dom-bindings/src/shared/crossOriginStrings'; + export function prefetchDNS(href: string) { if (__DEV__) { if (typeof href !== 'string' || !href) { @@ -74,7 +78,7 @@ export function preconnect(href: string, options?: ?PreconnectOptions) { const dispatcher = Dispatcher.current; if (dispatcher && typeof href === 'string') { const crossOrigin = options - ? getCrossOrigin('preconnect', options.crossOrigin) + ? getCrossOriginString(options.crossOrigin) : null; dispatcher.preconnect(href, crossOrigin); } @@ -117,7 +121,7 @@ export function preload(href: string, options: PreloadOptions) { typeof options.as === 'string' ) { const as = options.as; - const crossOrigin = getCrossOrigin(as, options.crossOrigin); + const crossOrigin = getCrossOriginStringAs(as, options.crossOrigin); dispatcher.preload(href, as, { crossOrigin, integrity: @@ -172,7 +176,10 @@ export function preloadModule(href: string, options?: ?PreloadModuleOptions) { const dispatcher = Dispatcher.current; if (dispatcher && typeof href === 'string') { if (options) { - const crossOrigin = getCrossOrigin(options.as, options.crossOrigin); + const crossOrigin = getCrossOriginStringAs( + options.as, + options.crossOrigin, + ); dispatcher.preloadModule(href, { as: typeof options.as === 'string' && options.as !== 'script' @@ -218,7 +225,7 @@ export function preinit(href: string, options: PreinitOptions) { typeof options.as === 'string' ) { const as = options.as; - const crossOrigin = getCrossOrigin(as, options.crossOrigin); + const crossOrigin = getCrossOriginStringAs(as, options.crossOrigin); const integrity = typeof options.integrity === 'string' ? options.integrity : undefined; const fetchPriority = @@ -296,21 +303,23 @@ export function preinitModule(href: string, options?: ?PreinitModuleOptions) { } const dispatcher = Dispatcher.current; if (dispatcher && typeof href === 'string') { - if ( - options == null || - (typeof options === 'object' && - (options.as == null || options.as === 'script')) - ) { - const crossOrigin = options - ? getCrossOrigin(undefined, options.crossOrigin) - : undefined; - dispatcher.preinitModuleScript(href, { - crossOrigin, - integrity: - options && typeof options.integrity === 'string' - ? options.integrity - : undefined, - }); + if (typeof options === 'object' && options !== null) { + if (options.as == null || options.as === 'script') { + const crossOrigin = getCrossOriginStringAs( + options.as, + options.crossOrigin, + ); + dispatcher.preinitModuleScript(href, { + crossOrigin, + integrity: + typeof options.integrity === 'string' + ? options.integrity + : undefined, + nonce: typeof options.nonce === 'string' ? options.nonce : undefined, + }); + } + } else if (options == null) { + dispatcher.preinitModuleScript(href); } } // We don't error because preinit needs to be resilient to being called in a variety of scopes @@ -318,16 +327,6 @@ export function preinitModule(href: string, options?: ?PreinitModuleOptions) { // so we favor silent bailout over warning or erroring. } -function getCrossOrigin(as: ?string, crossOrigin: ?string): ?CrossOriginEnum { - return as === 'font' - ? '' - : typeof crossOrigin === 'string' - ? crossOrigin === 'use-credentials' - ? 'use-credentials' - : '' - : undefined; -} - function getValueDescriptorExpectingObjectForWarning(thing: any): string { return thing === null ? '`null`' diff --git a/packages/react-dom/src/shared/ReactDOMTypes.js b/packages/react-dom/src/shared/ReactDOMTypes.js index c12b5e9969fd5..9c9406122937c 100644 --- a/packages/react-dom/src/shared/ReactDOMTypes.js +++ b/packages/react-dom/src/shared/ReactDOMTypes.js @@ -7,6 +7,8 @@ * @flow */ +import type {CrossOriginString} from 'react-dom-bindings/src/shared/crossOriginStrings'; + export type PrefetchDNSOptions = {}; export type PreconnectOptions = {crossOrigin?: string}; export type PreloadOptions = { @@ -41,7 +43,7 @@ export type PreinitModuleOptions = { nonce?: string, }; -export type CrossOriginEnum = '' | 'use-credentials'; +export type CrossOriginEnum = '' | 'use-credentials' | CrossOriginString; export type FetchPriorityEnum = 'high' | 'low' | 'auto'; export type PreloadImplOptions = { @@ -61,12 +63,12 @@ export type PreloadModuleImplOptions = { nonce?: ?string, }; export type PreinitStyleOptions = { - crossOrigin?: ?string, + crossOrigin?: ?CrossOriginEnum, integrity?: ?string, fetchPriority?: ?FetchPriorityEnum, }; export type PreinitScriptOptions = { - crossOrigin?: ?string, + crossOrigin?: ?CrossOriginEnum, integrity?: ?string, fetchPriority?: ?FetchPriorityEnum, nonce?: ?string, diff --git a/packages/react-noop-renderer/src/ReactNoopFlightClient.js b/packages/react-noop-renderer/src/ReactNoopFlightClient.js index 013c663cb0c4e..3bd3d863ac45e 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightClient.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightClient.js @@ -35,6 +35,7 @@ const {createResponse, processBinaryChunk, getRoot, close} = ReactFlightClient({ resolveClientReference(bundlerConfig: null, idx: string) { return idx; }, + prepareDestinationForModule(moduleLoading: null, metadata: string) {}, preloadModule(idx: string) {}, requireModule(idx: string) { return readModule(idx); diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler.js b/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode.js similarity index 73% rename from packages/react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler.js rename to packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode.js index 0789a52ffc0e1..b2bf9b8ae0b53 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler.js +++ b/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode.js @@ -13,7 +13,18 @@ import type { RejectedThenable, } from 'shared/ReactTypes'; -export type SSRManifest = { +import type {ImportMetadata} from './shared/ReactFlightImportMetadata'; +import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig'; + +import { + ID, + CHUNKS, + NAME, + isAsyncImport, +} from './shared/ReactFlightImportMetadata'; +import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig'; + +export type SSRModuleMap = { [clientId: string]: { [clientExportName: string]: ClientReference, }, @@ -23,12 +34,7 @@ export type ServerManifest = void; export type ServerReferenceId = string; -export opaque type ClientReferenceMetadata = { - id: string, - chunks: Array, - name: string, - async?: boolean, -}; +export opaque type ClientReferenceMetadata = ImportMetadata; // eslint-disable-next-line no-unused-vars export opaque type ClientReference = { @@ -37,12 +43,26 @@ export opaque type ClientReference = { async?: boolean, }; +// The reason this function needs to defined here in this file instead of just +// being exported directly from the WebpackDestination... file is because the +// ClientReferenceMetadata is opaque and we can't unwrap it there. +// This should get inlined and we could also just implement an unwrapping function +// though that risks it getting used in places it shouldn't be. This is unfortunate +// but currently it seems to be the best option we have. +export function prepareDestinationForModule( + moduleLoading: ModuleLoading, + nonce: ?string, + metadata: ClientReferenceMetadata, +) { + prepareDestinationWithChunks(moduleLoading, metadata[CHUNKS], nonce); +} + export function resolveClientReference( - bundlerConfig: SSRManifest, + bundlerConfig: SSRModuleMap, metadata: ClientReferenceMetadata, ): ClientReference { - const moduleExports = bundlerConfig[metadata.id]; - let resolvedModuleData = moduleExports[metadata.name]; + const moduleExports = bundlerConfig[metadata[ID]]; + let resolvedModuleData = moduleExports[metadata[NAME]]; let name; if (resolvedModuleData) { // The potentially aliased name. @@ -53,17 +73,17 @@ export function resolveClientReference( if (!resolvedModuleData) { throw new Error( 'Could not find the module "' + - metadata.id + + metadata[ID] + '" in the React SSR Manifest. ' + 'This is probably a bug in the React Server Components bundler.', ); } - name = metadata.name; + name = metadata[NAME]; } return { specifier: resolvedModuleData.specifier, name: name, - async: metadata.async, + async: isAsyncImport(metadata), }; } diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler.js b/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js similarity index 68% rename from packages/react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler.js rename to packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js index ae94267a672c1..754578e8e7b7f 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler.js +++ b/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js @@ -13,35 +13,62 @@ import type { RejectedThenable, } from 'shared/ReactTypes'; -export type SSRManifest = null | { +import type { + ImportMetadata, + ImportManifestEntry, +} from './shared/ReactFlightImportMetadata'; +import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig'; + +import { + ID, + CHUNKS, + NAME, + isAsyncImport, +} from './shared/ReactFlightImportMetadata'; + +import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig'; + +import {loadChunk} from 'react-client/src/ReactFlightClientConfig'; + +export type SSRModuleMap = null | { [clientId: string]: { - [clientExportName: string]: ClientReferenceMetadata, + [clientExportName: string]: ClientReferenceManifestEntry, }, }; export type ServerManifest = { - [id: string]: ClientReference, + [id: string]: ImportManifestEntry, }; export type ServerReferenceId = string; -export opaque type ClientReferenceMetadata = { - id: string, - chunks: Array, - name: string, - async: boolean, -}; +export opaque type ClientReferenceManifestEntry = ImportManifestEntry; +export opaque type ClientReferenceMetadata = ImportMetadata; // eslint-disable-next-line no-unused-vars export opaque type ClientReference = ClientReferenceMetadata; +// The reason this function needs to defined here in this file instead of just +// being exported directly from the WebpackDestination... file is because the +// ClientReferenceMetadata is opaque and we can't unwrap it there. +// This should get inlined and we could also just implement an unwrapping function +// though that risks it getting used in places it shouldn't be. This is unfortunate +// but currently it seems to be the best option we have. +export function prepareDestinationForModule( + moduleLoading: ModuleLoading, + nonce: ?string, + metadata: ClientReferenceMetadata, +) { + prepareDestinationWithChunks(moduleLoading, metadata[CHUNKS], nonce); +} + export function resolveClientReference( - bundlerConfig: SSRManifest, + bundlerConfig: SSRModuleMap, metadata: ClientReferenceMetadata, ): ClientReference { if (bundlerConfig) { - const moduleExports = bundlerConfig[metadata.id]; - let resolvedModuleData = moduleExports[metadata.name]; + const moduleExports = bundlerConfig[metadata[ID]]; + let resolvedModuleData = moduleExports[metadata[NAME]]; let name; if (resolvedModuleData) { // The potentially aliased name. @@ -52,19 +79,23 @@ export function resolveClientReference( if (!resolvedModuleData) { throw new Error( 'Could not find the module "' + - metadata.id + + metadata[ID] + '" in the React SSR Manifest. ' + 'This is probably a bug in the React Server Components bundler.', ); } - name = metadata.name; + name = metadata[NAME]; + } + if (isAsyncImport(metadata)) { + return [ + resolvedModuleData.id, + resolvedModuleData.chunks, + name, + 1 /* async */, + ]; + } else { + return [resolvedModuleData.id, resolvedModuleData.chunks, name]; } - return { - id: resolvedModuleData.id, - chunks: resolvedModuleData.chunks, - name: name, - async: !!metadata.async, - }; } return metadata; } @@ -98,12 +129,7 @@ export function resolveServerReference( } } // TODO: This needs to return async: true if it's an async module. - return { - id: resolvedModuleData.id, - chunks: resolvedModuleData.chunks, - name: name, - async: false, - }; + return [resolvedModuleData.id, resolvedModuleData.chunks, name]; } // The chunk cache contains all the chunks we've preloaded so far. @@ -147,13 +173,15 @@ function ignoreReject() { export function preloadModule( metadata: ClientReference, ): null | Thenable { - const chunks = metadata.chunks; + const chunks = metadata[CHUNKS]; const promises = []; - for (let i = 0; i < chunks.length; i++) { - const chunkId = chunks[i]; + let i = 0; + while (i < chunks.length) { + const chunkId = chunks[i++]; + const chunkFilename = chunks[i++]; const entry = chunkCache.get(chunkId); if (entry === undefined) { - const thenable = __webpack_chunk_load__(chunkId); + const thenable = loadChunk(chunkId, chunkFilename); promises.push(thenable); // $FlowFixMe[method-unbinding] const resolve = chunkCache.set.bind(chunkCache, chunkId, null); @@ -163,12 +191,12 @@ export function preloadModule( promises.push(entry); } } - if (metadata.async) { + if (isAsyncImport(metadata)) { if (promises.length === 0) { - return requireAsyncModule(metadata.id); + return requireAsyncModule(metadata[ID]); } else { return Promise.all(promises).then(() => { - return requireAsyncModule(metadata.id); + return requireAsyncModule(metadata[ID]); }); } } else if (promises.length > 0) { @@ -181,8 +209,8 @@ export function preloadModule( // Actually require the module or suspend if it's not yet ready. // Increase priority if necessary. export function requireModule(metadata: ClientReference): T { - let moduleExports = __webpack_require__(metadata.id); - if (metadata.async) { + let moduleExports = __webpack_require__(metadata[ID]); + if (isAsyncImport(metadata)) { if (typeof moduleExports.then !== 'function') { // This wasn't a promise after all. } else if (moduleExports.status === 'fulfilled') { @@ -192,15 +220,15 @@ export function requireModule(metadata: ClientReference): T { throw moduleExports.reason; } } - if (metadata.name === '*') { + if (metadata[NAME] === '*') { // This is a placeholder value that represents that the caller imported this // as a CommonJS module as is. return moduleExports; } - if (metadata.name === '') { + if (metadata[NAME] === '') { // This is a placeholder value that represents that the caller accessed the // default property of this if it was an ESM interop module. return moduleExports.__esModule ? moduleExports.default : moduleExports; } - return moduleExports[metadata.name]; + return moduleExports[metadata[NAME]]; } diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackBrowser.js b/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackBrowser.js new file mode 100644 index 0000000000000..48779fb1e65e9 --- /dev/null +++ b/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackBrowser.js @@ -0,0 +1,28 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +const chunkMap: Map = new Map(); + +/** + * We patch the chunk filename function in webpack to insert our own resolution + * of chunks that come from Flight and may not be known to the webpack runtime + */ +const webpackGetChunkFilename = __webpack_require__.u; +__webpack_require__.u = function (chunkId: string) { + const flightChunk = chunkMap.get(chunkId); + if (flightChunk !== undefined) { + return flightChunk; + } + return webpackGetChunkFilename(chunkId); +}; + +export function loadChunk(chunkId: string, filename: string): Promise { + chunkMap.set(chunkId, filename); + return __webpack_chunk_load__(chunkId); +} diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer.js b/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer.js new file mode 100644 index 0000000000000..8eeb39a24a3e1 --- /dev/null +++ b/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export function loadChunk(chunkId: string, filename: string): Promise { + return __webpack_chunk_load__(chunkId); +} diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackBrowser.js b/packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackBrowser.js new file mode 100644 index 0000000000000..60b9e87dbea3e --- /dev/null +++ b/packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackBrowser.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type ModuleLoading = null; + +export function prepareDestinationWithChunks( + moduleLoading: ModuleLoading, + chunks: mixed, + nonce: ?string, +) { + // In the browser we don't need to prepare our destination since the browser is the Destination +} diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer.js b/packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer.js new file mode 100644 index 0000000000000..f5793fdab434d --- /dev/null +++ b/packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {preinitScriptForSSR} from 'react-client/src/ReactFlightClientConfig'; + +export type ModuleLoading = null | { + prefix: string, + crossOrigin?: 'use-credentials' | '', +}; + +export function prepareDestinationWithChunks( + moduleLoading: ModuleLoading, + // Chunks are double-indexed [..., idx, filenamex, idy, filenamey, ...] + chunks: Array, + nonce: ?string, +) { + if (moduleLoading !== null) { + for (let i = 1; i < chunks.length; i += 2) { + preinitScriptForSSR( + moduleLoading.prefix + chunks[i], + nonce, + moduleLoading.crossOrigin, + ); + } + } +} diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js index d91e7d7a755cb..64e6b3886adf7 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js @@ -34,8 +34,10 @@ export type Options = { function createResponseFromOptions(options: void | Options) { return createResponse( + null, null, options && options.callServer ? options.callServer : undefined, + undefined, // nonce ); } diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js index d9ce8f35a5262..3b2f7aeea044e 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js @@ -11,7 +11,15 @@ import type {Thenable} from 'shared/ReactTypes.js'; import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient'; -import type {SSRManifest} from './ReactFlightClientConfigWebpackBundler'; +import type { + SSRModuleMap, + ModuleLoading, +} from 'react-client/src/ReactFlightClientConfig'; + +type SSRManifest = { + moduleMap: SSRModuleMap, + moduleLoading: ModuleLoading, +}; import { createResponse, @@ -39,13 +47,16 @@ export function createServerReference, T>( } export type Options = { - moduleMap?: $NonMaybeType, + ssrManifest: SSRManifest, + nonce?: string, }; -function createResponseFromOptions(options: void | Options) { +function createResponseFromOptions(options: Options) { return createResponse( - options && options.moduleMap ? options.moduleMap : null, + options.ssrManifest.moduleMap, + options.ssrManifest.moduleLoading, noServerCall, + typeof options.nonce === 'string' ? options.nonce : undefined, ); } @@ -78,7 +89,7 @@ function startReadingFromStream( function createFromReadableStream( stream: ReadableStream, - options?: Options, + options: Options, ): Thenable { const response: FlightResponse = createResponseFromOptions(options); startReadingFromStream(response, stream); @@ -87,7 +98,7 @@ function createFromReadableStream( function createFromFetch( promiseForResponse: Promise, - options?: Options, + options: Options, ): Thenable { const response: FlightResponse = createResponseFromOptions(options); promiseForResponse.then( diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js index c6a14fb6b20e7..db6f233d80dc4 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js @@ -11,7 +11,15 @@ import type {Thenable} from 'shared/ReactTypes.js'; import type {Response} from 'react-client/src/ReactFlightClient'; -import type {SSRManifest} from 'react-client/src/ReactFlightClientConfig'; +import type { + SSRModuleMap, + ModuleLoading, +} from 'react-client/src/ReactFlightClientConfig'; + +type SSRManifest = { + moduleMap: SSRModuleMap, + moduleLoading: ModuleLoading, +}; import type {Readable} from 'stream'; @@ -40,11 +48,21 @@ export function createServerReference, T>( return createServerReferenceImpl(id, noServerCall); } +export type Options = { + nonce?: string, +}; + function createFromNodeStream( stream: Readable, - moduleMap: $NonMaybeType, + ssrManifest: SSRManifest, + options?: Options, ): Thenable { - const response: Response = createResponse(moduleMap, noServerCall); + const response: Response = createResponse( + ssrManifest.moduleMap, + ssrManifest.moduleLoading, + noServerCall, + options && typeof options.nonce === 'string' ? options.nonce : undefined, + ); stream.on('data', chunk => { processBinaryChunk(response, chunk); }); diff --git a/packages/react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler.js b/packages/react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler.js index b217ac1ef21fe..49c17b168b96e 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler.js +++ b/packages/react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler.js @@ -8,6 +8,10 @@ */ import type {ReactClientValue} from 'react-server/src/ReactFlightServer'; +import type { + ImportMetadata, + ImportManifestEntry, +} from './shared/ReactFlightImportMetadata'; import type { ClientReference, @@ -17,17 +21,13 @@ import type { export type {ClientReference, ServerReference}; export type ClientManifest = { - [id: string]: ClientReferenceMetadata, + [id: string]: ClientReferenceManifestEntry, }; export type ServerReferenceId = string; -export type ClientReferenceMetadata = { - id: string, - chunks: Array, - name: string, - async: boolean, -}; +export type ClientReferenceMetadata = ImportMetadata; +export opaque type ClientReferenceManifestEntry = ImportManifestEntry; export type ClientReferenceKey = string; @@ -71,12 +71,11 @@ export function resolveClientReferenceMetadata( ); } } - return { - id: resolvedModuleData.id, - chunks: resolvedModuleData.chunks, - name: name, - async: !!clientReference.$$async, - }; + if (clientReference.$$async === true) { + return [resolvedModuleData.id, resolvedModuleData.chunks, name, 1]; + } else { + return [resolvedModuleData.id, resolvedModuleData.chunks, name]; + } } export function getServerReferenceId( diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js index 096f5ce0d1dc4..82b866e419d49 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js +++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js @@ -7,6 +7,8 @@ * @flow */ +import type {ImportManifestEntry} from './shared/ReactFlightImportMetadata'; + import {join} from 'path'; import {pathToFileURL} from 'url'; import asyncLib from 'neo-async'; @@ -221,21 +223,65 @@ export default class ReactFlightWebpackPlugin { return; } + const configuredCrossOriginLoading = + compilation.outputOptions.crossOriginLoading; + const crossOriginMode = + typeof configuredCrossOriginLoading === 'string' + ? configuredCrossOriginLoading === 'use-credentials' + ? configuredCrossOriginLoading + : 'anonymous' + : null; + const resolvedClientFiles = new Set( (resolvedClientReferences || []).map(ref => ref.request), ); const clientManifest: { - [string]: {chunks: $FlowFixMe, id: string, name: string}, + [string]: ImportManifestEntry, } = {}; - const ssrManifest: { + type SSRModuleMap = { [string]: { [string]: {specifier: string, name: string}, }, - } = {}; + }; + const moduleMap: SSRModuleMap = {}; + const ssrBundleConfig: { + moduleLoading: { + prefix: string, + crossOrigin: string | null, + }, + moduleMap: SSRModuleMap, + } = { + moduleLoading: { + prefix: compilation.outputOptions.publicPath || '', + crossOrigin: crossOriginMode, + }, + moduleMap, + }; + + // We figure out which files are always loaded by any initial chunk (entrypoint). + // We use this to filter out chunks that Flight will never need to load + const emptySet: Set = new Set(); + const runtimeChunkFiles: Set = emptySet; + compilation.entrypoints.forEach(entrypoint => { + const runtimeChunk = entrypoint.getRuntimeChunk(); + if (runtimeChunk) { + runtimeChunk.files.forEach(runtimeFile => { + runtimeChunkFiles.add(runtimeFile); + }); + } + }); + compilation.chunkGroups.forEach(function (chunkGroup) { - const chunkIds = chunkGroup.chunks.map(function (c) { - return c.id; + const chunks: Array = []; + chunkGroup.chunks.forEach(function (c) { + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const file of c.files) { + if (!file.endsWith('.js')) return; + if (file.endsWith('.hot-update.js')) return; + chunks.push(c.id, file); + break; + } }); // $FlowFixMe[missing-local-annot] @@ -256,7 +302,7 @@ export default class ReactFlightWebpackPlugin { clientManifest[href] = { id, - chunks: chunkIds, + chunks, name: '*', }; ssrExports['*'] = { @@ -272,7 +318,7 @@ export default class ReactFlightWebpackPlugin { /* clientManifest[href + '#'] = { id, - chunks: chunkIds, + chunks, name: '', }; ssrExports[''] = { @@ -288,7 +334,7 @@ export default class ReactFlightWebpackPlugin { moduleProvidedExports.forEach(function (name) { clientManifest[href + '#' + name] = { id, - chunks: chunkIds, + chunks, name: name, }; ssrExports[name] = { @@ -299,7 +345,7 @@ export default class ReactFlightWebpackPlugin { } */ - ssrManifest[id] = ssrExports; + moduleMap[id] = ssrExports; } } @@ -326,7 +372,7 @@ export default class ReactFlightWebpackPlugin { _this.clientManifestFilename, new sources.RawSource(clientOutput, false), ); - const ssrOutput = JSON.stringify(ssrManifest, null, 2); + const ssrOutput = JSON.stringify(ssrBundleConfig, null, 2); compilation.emitAsset( _this.ssrManifestFilename, new sources.RawSource(ssrOutput, false), diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index f86db1943a3fa..e26db0fdceb45 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -25,8 +25,9 @@ let clientExports; let clientModuleError; let webpackMap; let Stream; +let FlightReact; let React; -let ReactDOM; +let FlightReactDOM; let ReactDOMClient; let ReactServerDOMServer; let ReactServerDOMClient; @@ -37,6 +38,9 @@ let JSDOM; describe('ReactFlightDOM', () => { beforeEach(() => { + // For this first reset we are going to load the dom-node version of react-server-dom-webpack/server + // This can be thought of as essentially being the React Server Components scope with react-server + // condition jest.resetModules(); JSDOM = require('jsdom').JSDOM; @@ -45,23 +49,29 @@ describe('ReactFlightDOM', () => { jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.node.unbundled'), ); + jest.mock('react', () => require('react/react.shared-subset')); - ReactServerDOMClient = require('react-server-dom-webpack/client'); - - act = require('internal-test-utils').act; const WebpackMock = require('./utils/WebpackMock'); clientExports = WebpackMock.clientExports; clientModuleError = WebpackMock.clientModuleError; webpackMap = WebpackMock.webpackMap; + ReactServerDOMServer = require('react-server-dom-webpack/server'); + FlightReact = require('react'); + FlightReactDOM = require('react-dom'); + + // This reset is to load modules for the SSR/Browser scope. + jest.unmock('react-server-dom-webpack/server'); + jest.unmock('react'); + jest.resetModules(); + act = require('internal-test-utils').act; Stream = require('stream'); React = require('react'); - ReactDOM = require('react-dom'); - ReactDOMFizzServer = require('react-dom/server.node'); use = React.use; Suspense = React.Suspense; ReactDOMClient = require('react-dom/client'); - ReactServerDOMServer = require('react-server-dom-webpack/server.node.unbundled'); + ReactDOMFizzServer = require('react-dom/server.node'); + ReactServerDOMClient = require('react-server-dom-webpack/client'); ErrorBoundary = class extends React.Component { state = {hasError: false, error: null}; @@ -485,7 +495,7 @@ describe('ReactFlightDOM', () => { const AsyncModuleRef = clientExports(AsyncModule); function ServerComponent() { - const text = use(AsyncModuleRef); + const text = FlightReact.use(AsyncModuleRef); return

{text}

; } @@ -1205,25 +1215,25 @@ describe('ReactFlightDOM', () => { const ClientComponent = clientExports(Component); async function ServerComponent() { - ReactDOM.prefetchDNS('d before'); - ReactDOM.preconnect('c before'); - ReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'}); - ReactDOM.preload('l before', {as: 'style'}); - ReactDOM.preloadModule('lm before'); - ReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'}); - ReactDOM.preinit('i before', {as: 'script'}); - ReactDOM.preinitModule('m before'); - ReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'}); + FlightReactDOM.prefetchDNS('d before'); + FlightReactDOM.preconnect('c before'); + FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'}); + FlightReactDOM.preload('l before', {as: 'style'}); + FlightReactDOM.preloadModule('lm before'); + FlightReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'}); + FlightReactDOM.preinit('i before', {as: 'script'}); + FlightReactDOM.preinitModule('m before'); + FlightReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'}); await 1; - ReactDOM.prefetchDNS('d after'); - ReactDOM.preconnect('c after'); - ReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'}); - ReactDOM.preload('l after', {as: 'style'}); - ReactDOM.preloadModule('lm after'); - ReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'}); - ReactDOM.preinit('i after', {as: 'script'}); - ReactDOM.preinitModule('m after'); - ReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'}); + FlightReactDOM.prefetchDNS('d after'); + FlightReactDOM.preconnect('c after'); + FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'}); + FlightReactDOM.preload('l after', {as: 'style'}); + FlightReactDOM.preloadModule('lm after'); + FlightReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'}); + FlightReactDOM.preinit('i after', {as: 'script'}); + FlightReactDOM.preinitModule('m after'); + FlightReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'}); return ; } @@ -1298,25 +1308,25 @@ describe('ReactFlightDOM', () => { const ClientComponent = clientExports(Component); async function ServerComponent() { - ReactDOM.prefetchDNS('d before'); - ReactDOM.preconnect('c before'); - ReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'}); - ReactDOM.preload('l before', {as: 'style'}); - ReactDOM.preloadModule('lm before'); - ReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'}); - ReactDOM.preinit('i before', {as: 'script'}); - ReactDOM.preinitModule('m before'); - ReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'}); + FlightReactDOM.prefetchDNS('d before'); + FlightReactDOM.preconnect('c before'); + FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'}); + FlightReactDOM.preload('l before', {as: 'style'}); + FlightReactDOM.preloadModule('lm before'); + FlightReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'}); + FlightReactDOM.preinit('i before', {as: 'script'}); + FlightReactDOM.preinitModule('m before'); + FlightReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'}); await 1; - ReactDOM.prefetchDNS('d after'); - ReactDOM.preconnect('c after'); - ReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'}); - ReactDOM.preload('l after', {as: 'style'}); - ReactDOM.preloadModule('lm after'); - ReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'}); - ReactDOM.preinit('i after', {as: 'script'}); - ReactDOM.preinitModule('m after'); - ReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'}); + FlightReactDOM.prefetchDNS('d after'); + FlightReactDOM.preconnect('c after'); + FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'}); + FlightReactDOM.preload('l after', {as: 'style'}); + FlightReactDOM.preloadModule('lm after'); + FlightReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'}); + FlightReactDOM.preinit('i after', {as: 'script'}); + FlightReactDOM.preinitModule('m after'); + FlightReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'}); return ; } @@ -1406,16 +1416,16 @@ describe('ReactFlightDOM', () => { const ClientComponent = clientExports(Component); async function ServerComponent1() { - ReactDOM.preload('before1', {as: 'style'}); + FlightReactDOM.preload('before1', {as: 'style'}); await 1; - ReactDOM.preload('after1', {as: 'style'}); + FlightReactDOM.preload('after1', {as: 'style'}); return ; } async function ServerComponent2() { - ReactDOM.preload('before2', {as: 'style'}); + FlightReactDOM.preload('before2', {as: 'style'}); await 1; - ReactDOM.preload('after2', {as: 'style'}); + FlightReactDOM.preload('after2', {as: 'style'}); return ; } @@ -1506,21 +1516,21 @@ describe('ReactFlightDOM', () => { const ClientComponent = clientExports(Component); async function ServerComponent() { - ReactDOM.prefetchDNS('dns'); - ReactDOM.preconnect('preconnect'); - ReactDOM.preload('load', {as: 'style'}); - ReactDOM.preinit('init', {as: 'script'}); + FlightReactDOM.prefetchDNS('dns'); + FlightReactDOM.preconnect('preconnect'); + FlightReactDOM.preload('load', {as: 'style'}); + FlightReactDOM.preinit('init', {as: 'script'}); // again but vary preconnect to demonstrate crossOrigin participates in the key - ReactDOM.prefetchDNS('dns'); - ReactDOM.preconnect('preconnect', {crossOrigin: 'anonymous'}); - ReactDOM.preload('load', {as: 'style'}); - ReactDOM.preinit('init', {as: 'script'}); + FlightReactDOM.prefetchDNS('dns'); + FlightReactDOM.preconnect('preconnect', {crossOrigin: 'anonymous'}); + FlightReactDOM.preload('load', {as: 'style'}); + FlightReactDOM.preinit('init', {as: 'script'}); await 1; // after an async point - ReactDOM.prefetchDNS('dns'); - ReactDOM.preconnect('preconnect', {crossOrigin: 'use-credentials'}); - ReactDOM.preload('load', {as: 'style'}); - ReactDOM.preinit('init', {as: 'script'}); + FlightReactDOM.prefetchDNS('dns'); + FlightReactDOM.preconnect('preconnect', {crossOrigin: 'use-credentials'}); + FlightReactDOM.preload('load', {as: 'style'}); + FlightReactDOM.preinit('init', {as: 'script'}); return ; } diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js index 4e0f5780b5b35..22ee0696027b1 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -22,6 +22,7 @@ global.setTimeout = cb => cb(); let clientExports; let webpackMap; let webpackModules; +let webpackModuleLoading; let React; let ReactDOMServer; let ReactServerDOMServer; @@ -33,18 +34,28 @@ describe('ReactFlightDOMEdge', () => { jest.resetModules(); // Simulate the condition resolution + jest.mock('react', () => require('react/react.shared-subset')); jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.edge'), ); + ReactServerDOMServer = require('react-server-dom-webpack/server'); const WebpackMock = require('./utils/WebpackMock'); + clientExports = WebpackMock.clientExports; webpackMap = WebpackMock.webpackMap; webpackModules = WebpackMock.webpackModules; + webpackModuleLoading = WebpackMock.moduleLoading; + + jest.resetModules(); + jest.unmock('react'); + jest.unmock('react-server-dom-webpack/server'); + jest.mock('react-server-dom-webpack/client', () => + require('react-server-dom-webpack/client.edge'), + ); React = require('react'); ReactDOMServer = require('react-dom/server.edge'); - ReactServerDOMServer = require('react-server-dom-webpack/server.edge'); - ReactServerDOMClient = require('react-server-dom-webpack/client.edge'); + ReactServerDOMClient = require('react-server-dom-webpack/client'); use = React.use; }); @@ -122,7 +133,10 @@ describe('ReactFlightDOMEdge', () => { webpackMap, ); const response = ReactServerDOMClient.createFromReadableStream(stream, { - moduleMap: translationMap, + ssrManifest: { + moduleMap: translationMap, + moduleLoading: webpackModuleLoading, + }, }); function ClientRoot() { @@ -154,7 +168,15 @@ describe('ReactFlightDOMEdge', () => { expect(serializedContent).not.toContain('\\"'); expect(serializedContent).toContain('\t'); - const result = await ReactServerDOMClient.createFromReadableStream(stream2); + const result = await ReactServerDOMClient.createFromReadableStream( + stream2, + { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }, + ); // Should still match the result when parsed expect(result.text).toBe(testString); expect(result.text2).toBe(testString2); @@ -183,7 +205,12 @@ describe('ReactFlightDOMEdge', () => { const stream = passThrough( ReactServerDOMServer.renderToReadableStream(buffers), ); - const result = await ReactServerDOMClient.createFromReadableStream(stream); + const result = await ReactServerDOMClient.createFromReadableStream(stream, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }); expect(result).toEqual(buffers); }); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js index 7bcd16b3ecfd7..4de3f5528dce8 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js @@ -140,7 +140,12 @@ describe('ReactFlightDOMForm', () => { ); } const rscStream = ReactServerDOMServer.renderToReadableStream(); - const response = ReactServerDOMClient.createFromReadableStream(rscStream); + const response = ReactServerDOMClient.createFromReadableStream(rscStream, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }); const ssrStream = await ReactDOMServer.renderToReadableStream(response); await readIntoContainer(ssrStream); @@ -203,7 +208,12 @@ describe('ReactFlightDOMForm', () => { ); } const rscStream = ReactServerDOMServer.renderToReadableStream(); - const response = ReactServerDOMClient.createFromReadableStream(rscStream); + const response = ReactServerDOMClient.createFromReadableStream(rscStream, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }); const ssrStream = await ReactDOMServer.renderToReadableStream(response); await readIntoContainer(ssrStream); @@ -239,7 +249,12 @@ describe('ReactFlightDOMForm', () => { ); } const rscStream = ReactServerDOMServer.renderToReadableStream(); - const response = ReactServerDOMClient.createFromReadableStream(rscStream); + const response = ReactServerDOMClient.createFromReadableStream(rscStream, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }); const ssrStream = await ReactDOMServer.renderToReadableStream(response); await readIntoContainer(ssrStream); @@ -307,7 +322,12 @@ describe('ReactFlightDOMForm', () => { , webpackMap, ); - const response = ReactServerDOMClient.createFromReadableStream(rscStream); + const response = ReactServerDOMClient.createFromReadableStream(rscStream, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }); const ssrStream = await ReactDOMServer.renderToReadableStream(response); await readIntoContainer(ssrStream); @@ -349,7 +369,12 @@ describe('ReactFlightDOMForm', () => { , webpackMap, ); - const response = ReactServerDOMClient.createFromReadableStream(rscStream); + const response = ReactServerDOMClient.createFromReadableStream(rscStream, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }); const ssrStream = await ReactDOMServer.renderToReadableStream(response); await readIntoContainer(ssrStream); @@ -392,7 +417,12 @@ describe('ReactFlightDOMForm', () => { , webpackMap, ); - const response = ReactServerDOMClient.createFromReadableStream(rscStream); + const response = ReactServerDOMClient.createFromReadableStream(rscStream, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }); const ssrStream = await ReactDOMServer.renderToReadableStream(response); await readIntoContainer(ssrStream); @@ -410,8 +440,15 @@ describe('ReactFlightDOMForm', () => { , webpackMap, ); - const postbackResponse = - ReactServerDOMClient.createFromReadableStream(postbackRscStream); + const postbackResponse = ReactServerDOMClient.createFromReadableStream( + postbackRscStream, + { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }, + ); const postbackSsrStream = await ReactDOMServer.renderToReadableStream( postbackResponse, {experimental_formState: formState}, @@ -471,7 +508,15 @@ describe('ReactFlightDOMForm', () => { , webpackMap, ); - const response = ReactServerDOMClient.createFromReadableStream(rscStream); + const response = ReactServerDOMClient.createFromReadableStream( + rscStream, + { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }, + ); const ssrStream = await ReactDOMServer.renderToReadableStream(response); await readIntoContainer(ssrStream); @@ -493,8 +538,15 @@ describe('ReactFlightDOMForm', () => { , webpackMap, ); - const postbackResponse = - ReactServerDOMClient.createFromReadableStream(postbackRscStream); + const postbackResponse = ReactServerDOMClient.createFromReadableStream( + postbackRscStream, + { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }, + ); const postbackSsrStream = await ReactDOMServer.renderToReadableStream( postbackResponse, {experimental_formState: formState}, @@ -518,8 +570,15 @@ describe('ReactFlightDOMForm', () => { , webpackMap, ); - const postbackResponse2 = - ReactServerDOMClient.createFromReadableStream(postbackRscStream2); + const postbackResponse2 = ReactServerDOMClient.createFromReadableStream( + postbackRscStream2, + { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }, + ); const postbackSsrStream2 = await ReactDOMServer.renderToReadableStream( postbackResponse2, {experimental_formState: formState2}, @@ -570,7 +629,12 @@ describe('ReactFlightDOMForm', () => { , webpackMap, ); - const response = ReactServerDOMClient.createFromReadableStream(rscStream); + const response = ReactServerDOMClient.createFromReadableStream(rscStream, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }); const ssrStream = await ReactDOMServer.renderToReadableStream(response); await readIntoContainer(ssrStream); @@ -590,8 +654,15 @@ describe('ReactFlightDOMForm', () => { , webpackMap, ); - const postbackResponse = - ReactServerDOMClient.createFromReadableStream(postbackRscStream); + const postbackResponse = ReactServerDOMClient.createFromReadableStream( + postbackRscStream, + { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }, + ); const postbackSsrStream = await ReactDOMServer.renderToReadableStream( postbackResponse, {experimental_formState: formState}, @@ -633,7 +704,12 @@ describe('ReactFlightDOMForm', () => { , webpackMap, ); - const response = ReactServerDOMClient.createFromReadableStream(rscStream); + const response = ReactServerDOMClient.createFromReadableStream(rscStream, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }); const ssrStream = await ReactDOMServer.renderToReadableStream(response); await readIntoContainer(ssrStream); @@ -654,8 +730,15 @@ describe('ReactFlightDOMForm', () => { , webpackMap, ); - const postbackResponse = - ReactServerDOMClient.createFromReadableStream(postbackRscStream); + const postbackResponse = ReactServerDOMClient.createFromReadableStream( + postbackRscStream, + { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }, + ); const postbackSsrStream = await ReactDOMServer.renderToReadableStream( postbackResponse, {experimental_formState: formState}, @@ -675,8 +758,15 @@ describe('ReactFlightDOMForm', () => { , webpackMap, ); - const postbackResponse2 = - ReactServerDOMClient.createFromReadableStream(postbackRscStream2); + const postbackResponse2 = ReactServerDOMClient.createFromReadableStream( + postbackRscStream2, + { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }, + ); const postbackSsrStream2 = await ReactDOMServer.renderToReadableStream( postbackResponse2, {experimental_formState: formState2}, @@ -713,7 +803,12 @@ describe('ReactFlightDOMForm', () => { , webpackMap, ); - const response = ReactServerDOMClient.createFromReadableStream(rscStream); + const response = ReactServerDOMClient.createFromReadableStream(rscStream, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }); const ssrStream = await ReactDOMServer.renderToReadableStream(response); await readIntoContainer(ssrStream); @@ -754,7 +849,12 @@ describe('ReactFlightDOMForm', () => { , webpackMap, ); - const response = ReactServerDOMClient.createFromReadableStream(rscStream); + const response = ReactServerDOMClient.createFromReadableStream(rscStream, { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }); const ssrStream = await ReactDOMServer.renderToReadableStream(response); await readIntoContainer(ssrStream); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js index 5fb6b75071c47..490c321689d17 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -16,6 +16,7 @@ global.setImmediate = cb => cb(); let clientExports; let webpackMap; let webpackModules; +let webpackModuleLoading; let React; let ReactDOMServer; let ReactServerDOMServer; @@ -28,18 +29,28 @@ describe('ReactFlightDOMNode', () => { jest.resetModules(); // Simulate the condition resolution + jest.mock('react', () => require('react/react.shared-subset')); jest.mock('react-server-dom-webpack/server', () => require('react-server-dom-webpack/server.node'), ); + ReactServerDOMServer = require('react-server-dom-webpack/server'); const WebpackMock = require('./utils/WebpackMock'); clientExports = WebpackMock.clientExports; webpackMap = WebpackMock.webpackMap; webpackModules = WebpackMock.webpackModules; + webpackModuleLoading = WebpackMock.moduleLoading; + + jest.resetModules(); + jest.unmock('react'); + jest.unmock('react-server-dom-webpack/server'); + jest.mock('react-server-dom-webpack/client', () => + require('react-server-dom-webpack/client.node'), + ); + React = require('react'); ReactDOMServer = require('react-dom/server.node'); - ReactServerDOMServer = require('react-server-dom-webpack/server.node'); - ReactServerDOMClient = require('react-server-dom-webpack/client.node'); + ReactServerDOMClient = require('react-server-dom-webpack/client'); Stream = require('stream'); use = React.use; }); @@ -68,7 +79,11 @@ describe('ReactFlightDOMNode', () => { } // The Client build may not have the same IDs as the Server bundles for the same // component. - const ClientComponentOnTheClient = clientExports(ClientComponent); + const ClientComponentOnTheClient = clientExports( + ClientComponent, + 123, + 'path/to/chunk.js', + ); const ClientComponentOnTheServer = clientExports(ClientComponent); // In the SSR bundle this module won't exist. We simulate this by deleting it. @@ -83,6 +98,10 @@ describe('ReactFlightDOMNode', () => { '*': ssrMetadata, }, }; + const ssrManifest = { + moduleMap: translationMap, + moduleLoading: webpackModuleLoading, + }; function App() { return ; @@ -93,14 +112,16 @@ describe('ReactFlightDOMNode', () => { webpackMap, ); const readable = new Stream.PassThrough(); - const response = ReactServerDOMClient.createFromNodeStream( - readable, - translationMap, - ); + let response; stream.pipe(readable); function ClientRoot() { + if (response) return use(response); + response = ReactServerDOMClient.createFromNodeStream( + readable, + ssrManifest, + ); return use(response); } @@ -108,7 +129,9 @@ describe('ReactFlightDOMNode', () => { , ); const result = await readResult(ssrStream); - expect(result).toEqual('Client Component'); + expect(result).toEqual( + 'Client Component', + ); }); it('should encode long string in a compact format', async () => { @@ -121,7 +144,10 @@ describe('ReactFlightDOMNode', () => { const readable = new Stream.PassThrough(); const stringResult = readResult(readable); - const parsedResult = ReactServerDOMClient.createFromNodeStream(readable); + const parsedResult = ReactServerDOMClient.createFromNodeStream(readable, { + moduleMap: {}, + moduleLoading: webpackModuleLoading, + }); stream.pipe(readable); @@ -160,9 +186,76 @@ describe('ReactFlightDOMNode', () => { ]; const stream = ReactServerDOMServer.renderToPipeableStream(buffers); const readable = new Stream.PassThrough(); - const promise = ReactServerDOMClient.createFromNodeStream(readable); + const promise = ReactServerDOMClient.createFromNodeStream(readable, { + moduleMap: {}, + moduleLoading: webpackModuleLoading, + }); stream.pipe(readable); const result = await promise; expect(result).toEqual(buffers); }); + + it('should allow accept a nonce option for Flight preinitialized scripts', 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 = clientExports( + ClientComponent, + 123, + 'path/to/chunk.js', + ); + const ClientComponentOnTheServer = clientExports(ClientComponent); + + // In the SSR bundle this module won't exist. We simulate this by deleting it. + const clientId = webpackMap[ClientComponentOnTheClient.$$id].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.$$id]; + const translationMap = { + [clientId]: { + '*': ssrMetadata, + }, + }; + const ssrManifest = { + moduleMap: translationMap, + moduleLoading: webpackModuleLoading, + }; + + function App() { + return ; + } + + const stream = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + const readable = new Stream.PassThrough(); + let response; + + stream.pipe(readable); + + function ClientRoot() { + if (response) return use(response); + response = ReactServerDOMClient.createFromNodeStream( + readable, + ssrManifest, + { + nonce: 'r4nd0m', + }, + ); + return use(response); + } + + const ssrStream = await ReactDOMServer.renderToPipeableStream( + , + ); + const result = await readResult(ssrStream); + expect(result).toEqual( + 'Client Component', + ); + }); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js b/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js index 71b82dbb192eb..7f80eb10b84a8 100644 --- a/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js +++ b/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js @@ -42,6 +42,9 @@ Module.prototype._compile = previousCompile; exports.webpackMap = webpackClientMap; exports.webpackModules = webpackClientModules; exports.webpackServerMap = webpackServerMap; +exports.moduleLoading = { + prefix: '/', +}; exports.clientModuleError = function clientModuleError(moduleError) { const idx = '' + webpackModuleIdx++; @@ -57,20 +60,28 @@ exports.clientModuleError = function clientModuleError(moduleError) { return mod.exports; }; -exports.clientExports = function clientExports(moduleExports) { +exports.clientExports = function clientExports( + moduleExports, + chunkId, + chunkFilename, +) { + const chunks = []; + if (chunkId) { + chunks.push(chunkId, chunkFilename); + } const idx = '' + webpackModuleIdx++; webpackClientModules[idx] = moduleExports; const path = url.pathToFileURL(idx).href; webpackClientMap[path] = { id: idx, - chunks: [], + chunks, name: '*', }; // We only add this if this test is testing ESM compat. if ('__esModule' in moduleExports) { webpackClientMap[path + '#'] = { id: idx, - chunks: [], + chunks, name: '', }; } @@ -80,7 +91,7 @@ exports.clientExports = function clientExports(moduleExports) { for (const name in asyncModuleExports) { webpackClientMap[path + '#' + name] = { id: idx, - chunks: [], + chunks, name: name, }; } @@ -96,7 +107,7 @@ exports.clientExports = function clientExports(moduleExports) { }; webpackClientMap[path + '#split'] = { id: splitIdx, - chunks: [], + chunks, name: 's', }; } diff --git a/packages/react-server-dom-webpack/src/shared/ReactFlightImportMetadata.js b/packages/react-server-dom-webpack/src/shared/ReactFlightImportMetadata.js new file mode 100644 index 0000000000000..08aafaf00c605 --- /dev/null +++ b/packages/react-server-dom-webpack/src/shared/ReactFlightImportMetadata.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type ImportManifestEntry = { + id: string, + // chunks is a double indexed array of chunkId / chunkFilename pairs + chunks: Array, + name: string, +}; + +// This is the parsed shape of the wire format which is why it is +// condensed to only the essentialy information +export type ImportMetadata = + | [ + /* id */ string, + /* chunks id/filename pairs, double indexed */ Array, + /* name */ string, + /* async */ 1, + ] + | [ + /* id */ string, + /* chunks id/filename pairs, double indexed */ Array, + /* name */ string, + ]; + +export const ID = 0; +export const CHUNKS = 1; +export const NAME = 2; +// export const ASYNC = 3; + +// This logic is correct because currently only include the 4th tuple member +// when the module is async. If that changes we will need to actually assert +// the value is true. We don't index into the 4th slot because flow does not +// like the potential out of bounds access +export function isAsyncImport(metadata: ImportMetadata): boolean { + return metadata.length === 4; +} diff --git a/packages/react/src/__tests__/ReactFetch-test.js b/packages/react/src/__tests__/ReactFetch-test.js index 5a8911888bdf8..c778f7e6162d7 100644 --- a/packages/react/src/__tests__/ReactFetch-test.js +++ b/packages/react/src/__tests__/ReactFetch-test.js @@ -46,9 +46,17 @@ describe('ReactFetch', () => { if (gate(flags => !flags.www)) { jest.mock('react', () => require('react/react.shared-subset')); } + jest.mock('react-server-dom-webpack/server', () => + require('react-server-dom-webpack/server.browser'), + ); + require('react-server-dom-webpack/src/__tests__/utils/WebpackMock'); React = require('react'); - ReactServerDOMServer = require('react-server-dom-webpack/server.browser'); + ReactServerDOMServer = require('react-server-dom-webpack/server'); + + jest.resetModules(); + jest.unmock('react'); + jest.unmock('react-server-dom-webpack/server'); ReactServerDOMClient = require('react-server-dom-webpack/client'); use = React.use; cache = React.cache; diff --git a/packages/react/src/__tests__/ReactFetchEdge-test.js b/packages/react/src/__tests__/ReactFetchEdge-test.js index 1eb0635192b64..9f9555251823e 100644 --- a/packages/react/src/__tests__/ReactFetchEdge-test.js +++ b/packages/react/src/__tests__/ReactFetchEdge-test.js @@ -51,9 +51,17 @@ describe('ReactFetch', () => { if (gate(flags => !flags.www)) { jest.mock('react', () => require('react/react.shared-subset')); } + jest.mock('react-server-dom-webpack/server', () => + require('react-server-dom-webpack/server.edge'), + ); + require('react-server-dom-webpack/src/__tests__/utils/WebpackMock'); React = require('react'); - ReactServerDOMServer = require('react-server-dom-webpack/server.edge'); + ReactServerDOMServer = require('react-server-dom-webpack/server'); + + jest.resetModules(); + jest.unmock('react'); + jest.unmock('react-server-dom-webpack/server'); ReactServerDOMClient = require('react-server-dom-webpack/client'); use = React.use; }); diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index 7f3996de04fd8..97db3a53a4318 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -74,7 +74,9 @@ declare module 'EventListener' { } declare function __webpack_chunk_load__(id: string): Promise; -declare function __webpack_require__(id: string): any; +declare var __webpack_require__: ((id: string) => any) & { + u: string => string, +}; declare module 'fs/promises' { declare var access: (path: string, mode?: number) => Promise; diff --git a/scripts/jest/setupHostConfigs.js b/scripts/jest/setupHostConfigs.js index 5ea6eb0f5810d..48eaf0fd5b731 100644 --- a/scripts/jest/setupHostConfigs.js +++ b/scripts/jest/setupHostConfigs.js @@ -125,6 +125,9 @@ function mockAllConfigs(rendererInfo) { fs.statSync(nodePath.join(process.cwd(), 'packages', candidate)); return jest.requireActual(candidate); } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } // try without a part } parts.pop(); diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 8b11189fcd1ff..56d92a94bbe31 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -37,6 +37,8 @@ module.exports = [ 'react-server-dom-webpack/server', 'react-server-dom-webpack/server.node.unbundled', 'react-server-dom-webpack/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node + 'react-server-dom-webpack/src/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node + 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode.js', 'react-devtools', 'react-devtools-core', 'react-devtools-shell', @@ -90,6 +92,8 @@ module.exports = [ 'react-server-dom-webpack/server.browser', 'react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js', // react-server-dom-webpack/client.browser 'react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js', // react-server-dom-webpack/server.browser + 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js', + 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackBrowser.js', 'react-devtools', 'react-devtools-core', 'react-devtools-shell', @@ -146,6 +150,8 @@ module.exports = [ 'react-server-dom-webpack/server.edge', 'react-server-dom-webpack/src/ReactFlightDOMClientEdge.js', // react-server-dom-webpack/client.edge 'react-server-dom-webpack/src/ReactFlightDOMServerEdge.js', // react-server-dom-webpack/server.edge + 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js', + 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer.js', 'react-devtools', 'react-devtools-core', 'react-devtools-shell', @@ -178,6 +184,9 @@ module.exports = [ 'react-server-dom-webpack/server', 'react-server-dom-webpack/server.node', 'react-server-dom-webpack/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node + 'react-server-dom-webpack/src/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node + 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js', + 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer.js', 'react-server-dom-webpack/node-register', 'react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js', 'react-devtools', @@ -234,7 +243,7 @@ module.exports = [ 'react-dom/src/ReactDOMSharedSubset.js', 'react-dom-bindings', 'react-server-dom-webpack', - 'react-dom/src/server/ReactDOMLegacyServerImpl.js', // not an entrypoint, but only usable in *Brower and *Node files + 'react-dom/src/server/ReactDOMLegacyServerImpl.js', // not an entrypoint, but only usable in *Browser and *Node files 'react-dom/src/server/ReactDOMLegacyServerBrowser.js', // react-dom/server.browser 'react-dom/src/server/ReactDOMLegacyServerNode.js', // react-dom/server.node 'react-dom/src/server/ReactDOMLegacyServerNode.classic.fb.js', From 77fed1f8e8fa7d7d02e79d3f7df8558408138d82 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Thu, 10 Aug 2023 09:17:21 -0700 Subject: [PATCH 2/2] ESM implementation of module preinitialization --- fixtures/flight-esm/.nvmrc | 1 + fixtures/flight-esm/server/global.js | 38 ++++++++++++++----- fixtures/flight-esm/src/App.js | 20 +--------- fixtures/flight-esm/yarn.lock | 22 +++++------ ...ReactFlightClientConfig.dom-browser-esm.js | 3 +- .../ReactFlightClientConfig.dom-node-esm.js | 5 ++- .../src/shared/ReactFlightClientConfigDOM.js | 14 +++++++ ...s => ReactFlightClientConfigBundlerESM.js} | 21 +++++++++- ...ReactFlightClientConfigTargetESMBrowser.js | 18 +++++++++ .../ReactFlightClientConfigTargetESMServer.js | 35 +++++++++++++++++ .../src/ReactFlightDOMClientBrowser.js | 2 + .../src/ReactFlightDOMClientNode.js | 14 ++++++- scripts/shared/inlinedHostConfigs.js | 4 +- 13 files changed, 149 insertions(+), 48 deletions(-) create mode 100644 fixtures/flight-esm/.nvmrc rename packages/react-server-dom-esm/src/{ReactFlightClientConfigESMBundler.js => ReactFlightClientConfigBundlerESM.js} (74%) create mode 100644 packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser.js create mode 100644 packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer.js diff --git a/fixtures/flight-esm/.nvmrc b/fixtures/flight-esm/.nvmrc new file mode 100644 index 0000000000000..3f430af82b3df --- /dev/null +++ b/fixtures/flight-esm/.nvmrc @@ -0,0 +1 @@ +v18 diff --git a/fixtures/flight-esm/server/global.js b/fixtures/flight-esm/server/global.js index d6aaf4cc8ca17..1088d42967a2f 100644 --- a/fixtures/flight-esm/server/global.js +++ b/fixtures/flight-esm/server/global.js @@ -10,6 +10,7 @@ const compress = require('compression'); const chalk = require('chalk'); const express = require('express'); const http = require('http'); +const React = require('react'); const {renderToPipeableStream} = require('react-dom/server'); const {createFromNodeStream} = require('react-server-dom-esm/client'); @@ -62,23 +63,39 @@ app.all('/', async function (req, res, next) { if (req.accepts('text/html')) { try { const rscResponse = await promiseForData; - const moduleBaseURL = '/src'; // For HTML, we're a "client" emulator that runs the client code, // so we start by consuming the RSC payload. This needs the local file path // to load the source files from as well as the URL path for preloads. - const root = await createFromNodeStream( - rscResponse, - moduleBasePath, - moduleBaseURL - ); + + let root; + let Root = () => { + if (root) { + return React.use(root); + } + + return React.use( + (root = createFromNodeStream( + rscResponse, + moduleBasePath, + moduleBaseURL + )) + ); + }; // Render it into HTML by resolving the client components res.set('Content-type', 'text/html'); - const {pipe} = renderToPipeableStream(root, { - // TODO: bootstrapModules inserts a preload before the importmap which causes - // the import map to be invalid. We need to fix that in Float somehow. - // bootstrapModules: ['/src/index.js'], + const {pipe} = renderToPipeableStream(React.createElement(Root), { + importMap: { + imports: { + react: 'https://esm.sh/react@experimental?pin=v124&dev', + 'react-dom': 'https://esm.sh/react-dom@experimental?pin=v124&dev', + 'react-dom/': 'https://esm.sh/react-dom@experimental&pin=v124&dev/', + 'react-server-dom-esm/client': + '/node_modules/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js', + }, + }, + bootstrapModules: ['/src/index.js'], }); pipe(res); } catch (e) { @@ -89,6 +106,7 @@ app.all('/', async function (req, res, next) { } else { try { const rscResponse = await promiseForData; + // For other request, we pass-through the RSC payload. res.set('Content-type', 'text/x-component'); rscResponse.on('data', data => { diff --git a/fixtures/flight-esm/src/App.js b/fixtures/flight-esm/src/App.js index 161776eddd616..d5945280469bc 100644 --- a/fixtures/flight-esm/src/App.js +++ b/fixtures/flight-esm/src/App.js @@ -9,16 +9,6 @@ import {getServerState} from './ServerState.js'; const h = React.createElement; -const importMap = { - imports: { - react: 'https://esm.sh/react@experimental?pin=v124&dev', - 'react-dom': 'https://esm.sh/react-dom@experimental?pin=v124&dev', - 'react-dom/': 'https://esm.sh/react-dom@experimental&pin=v124&dev/', - 'react-server-dom-esm/client': - '/node_modules/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js', - }, -}; - export default async function App() { const res = await fetch('http://localhost:3001/todos'); const todos = await res.json(); @@ -42,12 +32,6 @@ export default async function App() { rel: 'stylesheet', href: '/src/style.css', precedence: 'default', - }), - h('script', { - type: 'importmap', - dangerouslySetInnerHTML: { - __html: JSON.stringify(importMap), - }, }) ), h( @@ -84,9 +68,7 @@ export default async function App() { 'Like' ) ) - ), - // TODO: Move this to bootstrapModules. - h('script', {type: 'module', src: '/src/index.js'}) + ) ) ); } diff --git a/fixtures/flight-esm/yarn.lock b/fixtures/flight-esm/yarn.lock index 409a5592ba966..a00d5c244d88d 100644 --- a/fixtures/flight-esm/yarn.lock +++ b/fixtures/flight-esm/yarn.lock @@ -540,17 +540,17 @@ raw-body@2.5.2: unpipe "1.0.0" react-dom@experimental: - version "0.0.0-experimental-018c58c9c-20230601" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-experimental-018c58c9c-20230601.tgz#2cc0ac824b83bab2ac1c6187f241dbd5dcd5201b" - integrity sha512-hwRsyoG1R3Tub0nUa72YvNcqPvU+pTcr9dadOnUCKKfSiYVbBCy7LxmkqLauCD8OjNJMlwtMgG4UAgtidclYGQ== + version "0.0.0-experimental-b9be4537c-20230905" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-experimental-b9be4537c-20230905.tgz#b078d6d06041e0c98ce5a2f5e9ff26a2e308eb41" + integrity sha512-veAFNVj81lUYhYlucYm3kbj2BhakG57XYkWC/QHVEZDk4Hm2qxM9RUk7gn8dWs9Eq7KR6Q+JWiSH3ZbObQTV9g== dependencies: loose-envify "^1.1.0" - scheduler "0.0.0-experimental-018c58c9c-20230601" + scheduler "0.0.0-experimental-b9be4537c-20230905" react@experimental: - version "0.0.0-experimental-018c58c9c-20230601" - resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-experimental-018c58c9c-20230601.tgz#ab04d1243c8f83b0166ed342056fa6b38ab2cd23" - integrity sha512-nSQIBsZ26Ii899pZ9cRt/6uQLbIUEAcDIivvAQyaHp4pWm289aB+7AK7VCWojAJIf4OStCuWs2berZsk4mzLVg== + version "0.0.0-experimental-b9be4537c-20230905" + resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-experimental-b9be4537c-20230905.tgz#3c2352b42b8024544a12dcd96f2700313cebcb6b" + integrity sha512-QNeK74S7AU94j4vCxet2S76HqxpF6CJo1pG3XcgY2NravyXdWYszrRDNHrfu86gGNwAQvSU+YpStYn/i0b9tLA== dependencies: loose-envify "^1.1.0" @@ -588,10 +588,10 @@ safe-buffer@5.1.2: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -scheduler@0.0.0-experimental-018c58c9c-20230601: - version "0.0.0-experimental-018c58c9c-20230601" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-experimental-018c58c9c-20230601.tgz#4f083614f8e857bab63dd90b4b37b03783dafe6b" - integrity sha512-otUM7AAAnCoJ5/0jTQwUQ7NhxjgcPEdrfzW7NfkpocrDoTUbql1kIGIhj9L9POMVFDI/wcZzRNK/oIEWsB4DPw== +scheduler@0.0.0-experimental-b9be4537c-20230905: + version "0.0.0-experimental-b9be4537c-20230905" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-experimental-b9be4537c-20230905.tgz#f0fe5a710ce15a9d637c28e9f019a4100e1f3f34" + integrity sha512-V5P9LOS+c5CG7qaCJu+Qgcz9eh/dP4nBszj3w1MCgZnMtAna6+J8ZuuUnRDMeY86F8KH+cY8Q5beIvAL2noMzA== dependencies: loose-envify "^1.1.0" diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js index 53058d0d18841..ec71cd94382cc 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js @@ -8,6 +8,7 @@ */ export * from 'react-client/src/ReactFlightClientConfigBrowser'; -export * from 'react-server-dom-esm/src/ReactFlightClientConfigESMBundler'; +export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM'; +export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = false; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js index 8390c4c06b439..016ac820d356c 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js @@ -7,7 +7,8 @@ * @flow */ -export * from 'react-client/src/ReactFlightClientConfigBrowser'; -export * from 'react-server-dom-esm/src/ReactFlightClientConfigESMBundler'; +export * from 'react-client/src/ReactFlightClientConfigNode'; +export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM'; +export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = true; diff --git a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js index 48f4b24a362f2..3ef89a47f9593 100644 --- a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js +++ b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js @@ -113,6 +113,20 @@ function refineModel(code: T, model: HintModel): HintModel { return model; } +export function preinitModuleForSSR( + href: string, + nonce: ?string, + crossOrigin: ?string, +) { + const dispatcher = ReactDOMCurrentDispatcher.current; + if (dispatcher) { + dispatcher.preinitModuleScript(href, { + crossOrigin: getCrossOriginString(crossOrigin), + nonce, + }); + } +} + export function preinitScriptForSSR( href: string, nonce: ?string, diff --git a/packages/react-server-dom-esm/src/ReactFlightClientConfigESMBundler.js b/packages/react-server-dom-esm/src/ReactFlightClientConfigBundlerESM.js similarity index 74% rename from packages/react-server-dom-esm/src/ReactFlightClientConfigESMBundler.js rename to packages/react-server-dom-esm/src/ReactFlightClientConfigBundlerESM.js index 55deba3073677..5db99dc8fff05 100644 --- a/packages/react-server-dom-esm/src/ReactFlightClientConfigESMBundler.js +++ b/packages/react-server-dom-esm/src/ReactFlightClientConfigBundlerESM.js @@ -12,13 +12,16 @@ import type { FulfilledThenable, RejectedThenable, } from 'shared/ReactTypes'; +import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig'; -export type SSRManifest = string; // Module root path +export type SSRModuleMap = string; // Module root path export type ServerManifest = string; // Module root path export type ServerReferenceId = string; +import {prepareDestinationForModuleImpl} from 'react-client/src/ReactFlightClientConfig'; + export opaque type ClientReferenceMetadata = [ string, // module path string, // export name @@ -30,8 +33,22 @@ export opaque type ClientReference = { name: string, }; +// The reason this function needs to defined here in this file instead of just +// being exported directly from the WebpackDestination... file is because the +// ClientReferenceMetadata is opaque and we can't unwrap it there. +// This should get inlined and we could also just implement an unwrapping function +// though that risks it getting used in places it shouldn't be. This is unfortunate +// but currently it seems to be the best option we have. +export function prepareDestinationForModule( + moduleLoading: ModuleLoading, + nonce: ?string, + metadata: ClientReferenceMetadata, +) { + prepareDestinationForModuleImpl(moduleLoading, metadata[0], nonce); +} + export function resolveClientReference( - bundlerConfig: SSRManifest, + bundlerConfig: SSRModuleMap, metadata: ClientReferenceMetadata, ): ClientReference { const baseURL = bundlerConfig; diff --git a/packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser.js b/packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser.js new file mode 100644 index 0000000000000..0ba181c4cdfea --- /dev/null +++ b/packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type ModuleLoading = null; + +export function prepareDestinationForModuleImpl( + moduleLoading: ModuleLoading, + chunks: mixed, + nonce: ?string, +) { + // In the browser we don't need to prepare our destination since the browser is the Destination +} diff --git a/packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer.js b/packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer.js new file mode 100644 index 0000000000000..c435e40b0b0f8 --- /dev/null +++ b/packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer.js @@ -0,0 +1,35 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {preinitModuleForSSR} from 'react-client/src/ReactFlightClientConfig'; + +export type ModuleLoading = + | null + | string + | { + prefix: string, + crossOrigin?: string, + }; + +export function prepareDestinationForModuleImpl( + moduleLoading: ModuleLoading, + // Chunks are double-indexed [..., idx, filenamex, idy, filenamey, ...] + mod: string, + nonce: ?string, +) { + if (typeof moduleLoading === 'string') { + preinitModuleForSSR(moduleLoading + mod, nonce, undefined); + } else if (moduleLoading !== null) { + preinitModuleForSSR( + moduleLoading.prefix + mod, + nonce, + moduleLoading.crossOrigin, + ); + } +} diff --git a/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js index e3ebaf3fb1aae..181328e93fda5 100644 --- a/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js @@ -36,7 +36,9 @@ export type Options = { function createResponseFromOptions(options: void | Options) { return createResponse( options && options.moduleBaseURL ? options.moduleBaseURL : '', + null, options && options.callServer ? options.callServer : undefined, + undefined, // nonce ); } diff --git a/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js index 4288d5878a928..dbc9ed8e3d2a0 100644 --- a/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js @@ -38,12 +38,22 @@ export function createServerReference, T>( return createServerReferenceImpl(id, noServerCall); } +export type Options = { + nonce?: string, +}; + function createFromNodeStream( stream: Readable, moduleRootPath: string, - moduleBaseURL: string, // TODO: Used for preloading hints + moduleBaseURL: string, + options?: Options, ): Thenable { - const response: Response = createResponse(moduleRootPath, noServerCall); + const response: Response = createResponse( + moduleRootPath, + moduleBaseURL, + noServerCall, + options && typeof options.nonce === 'string' ? options.nonce : undefined, + ); stream.on('data', chunk => { processBinaryChunk(response, chunk); }); diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 56d92a94bbe31..c958a4a4ccfae 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -116,6 +116,7 @@ module.exports = [ 'react-server-dom-esm', 'react-server-dom-esm/client', 'react-server-dom-esm/client.browser', + 'react-server-dom-esm/src/ReactFlightDOMClientBrowser.js', // react-server-dom-esm/client.browser 'react-devtools', 'react-devtools-core', 'react-devtools-shell', @@ -221,7 +222,8 @@ module.exports = [ 'react-server-dom-esm/client.node', 'react-server-dom-esm/server', 'react-server-dom-esm/server.node', - 'react-server-dom-esm/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node + 'react-server-dom-esm/src/ReactFlightDOMServerNode.js', // react-server-dom-esm/server.node + 'react-server-dom-esm/src/ReactFlightDOMClientNode.js', // react-server-dom-esm/client.node 'react-devtools', 'react-devtools-core', 'react-devtools-shell',