From 1108805c24c4a738225509bcbb4e52ff177af40a Mon Sep 17 00:00:00 2001 From: Josh Story Date: Wed, 27 Sep 2023 09:53:31 -0700 Subject: [PATCH] [Flight][Float] Preinitialize module imports during SSR (#27314) Currently when we SSR a Flight response we do not emit any resources for module imports. This means that when the client hydrates it won't have already loaded the necessary scripts to satisfy the Imports defined in the Flight payload which will lead to a delay in hydration completing. This change updates `react-server-dom-webpack` and `react-server-dom-esm` to emit async script tags in the head when we encounter a modules in the flight response. To support this we need some additional server configuration. We need to know the path prefix for chunk loading and whether the chunks will load with CORS or not (and if so with what configuration). --- 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 +-- fixtures/flight/.nvmrc | 1 + fixtures/flight/server/global.js | 41 ++++- .../react-client/src/ReactFlightClient.js | 20 ++- .../forks/ReactFlightClientConfig.custom.js | 5 +- ...ReactFlightClientConfig.dom-browser-esm.js | 3 +- .../ReactFlightClientConfig.dom-browser.js | 4 +- .../forks/ReactFlightClientConfig.dom-bun.js | 4 +- ...eactFlightClientConfig.dom-edge-webpack.js | 4 +- .../ReactFlightClientConfig.dom-legacy.js | 4 +- .../ReactFlightClientConfig.dom-node-esm.js | 5 +- ...eactFlightClientConfig.dom-node-webpack.js | 4 +- .../forks/ReactFlightClientConfig.dom-node.js | 3 +- .../src/shared/ReactFlightClientConfigDOM.js | 30 ++++ .../src/shared/crossOriginStrings.js | 30 ++++ .../react-dom/src/shared/ReactDOMFloat.js | 59 ++++--- .../react-dom/src/shared/ReactDOMTypes.js | 8 +- .../src/ReactNoopFlightClient.js | 1 + ...s => ReactFlightClientConfigBundlerESM.js} | 21 ++- ...ReactFlightClientConfigTargetESMBrowser.js | 18 +++ .../ReactFlightClientConfigTargetESMServer.js | 35 +++++ .../src/ReactFlightDOMClientBrowser.js | 2 + .../src/ReactFlightDOMClientNode.js | 14 +- ... => 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 | 15 +- 48 files changed, 1021 insertions(+), 288 deletions(-) create mode 100644 fixtures/flight-esm/.nvmrc create mode 100644 fixtures/flight/.nvmrc create mode 100644 packages/react-dom-bindings/src/shared/crossOriginStrings.js 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 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-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/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-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-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-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-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..3ef89a47f9593 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,31 @@ export function dispatchHint( 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, + 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-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/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..c958a4a4ccfae 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', @@ -112,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', @@ -146,6 +151,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 +185,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', @@ -212,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', @@ -234,7 +245,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',