From e64e4a5fe34c0ae9e574774bbabc6e4f7fff3845 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 5 Sep 2023 13:43:05 -0700 Subject: [PATCH] During SSR we should preinitialize imports so they can begin to be fetched before bootstrap in the browser --- .eslintrc.js | 4 +- fixtures/flight/.nvmrc | 1 + fixtures/flight/server/global.js | 26 +++- .../react-client/src/ReactFlightClient.js | 13 +- .../forks/ReactFlightClientConfig.custom.js | 5 +- .../ReactFlightClientConfig.dom-browser.js | 4 +- .../forks/ReactFlightClientConfig.dom-bun.js | 4 +- ...eactFlightClientConfig.dom-edge-webpack.js | 4 +- .../ReactFlightClientConfig.dom-legacy.js | 4 +- ...eactFlightClientConfig.dom-node-webpack.js | 4 +- .../forks/ReactFlightClientConfig.dom-node.js | 3 +- .../src/shared/ReactFlightClientConfigDOM.js | 11 ++ .../src/ReactNoopFlightClient.js | 1 + ... => ReactFlightClientConfigBundlerNode.js} | 45 ++++-- ... ReactFlightClientConfigBundlerWebpack.js} | 103 +++++++++----- ...FlightClientConfigBundlerWebpackBrowser.js | 28 ++++ ...tFlightClientConfigBundlerWebpackServer.js | 12 ++ ...tFlightClientConfigTargetWebpackBrowser.js | 17 +++ ...ctFlightClientConfigTargetWebpackServer.js | 30 ++++ .../src/ReactFlightDOMClientBrowser.js | 1 + .../src/ReactFlightDOMClientEdge.js | 9 +- .../src/ReactFlightDOMClientNode.js | 20 ++- .../ReactFlightServerConfigWebpackBundler.js | 25 ++-- .../src/ReactFlightWebpackPlugin.js | 66 +++++++-- .../src/__tests__/ReactFlightDOM-test.js | 130 ++++++++++-------- .../src/shared/ReactFlightImportMetadata.js | 43 ++++++ .../react/src/__tests__/ReactFetch-test.js | 5 + scripts/flow/environment.js | 4 +- scripts/jest/setupHostConfigs.js | 3 + scripts/shared/inlinedHostConfigs.js | 11 +- 30 files changed, 479 insertions(+), 157 deletions(-) create mode 100644 fixtures/flight/.nvmrc rename packages/react-server-dom-webpack/src/{ReactFlightClientConfigNodeBundler.js => ReactFlightClientConfigBundlerNode.js} (74%) 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/.eslintrc.js b/.eslintrc.js index 35e83b0d7abcc..878dd08fda653 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -415,9 +415,7 @@ module.exports = { }, }, { - files: [ - 'packages/react-native-renderer/**/*.js', - ], + files: ['packages/react-native-renderer/**/*.js'], globals: { nativeFabricUIManager: 'readonly', }, 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 16184287b1228..9310c2c14a771 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,7 +127,7 @@ 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 ssrBundleConfig = JSON.parse( await virtualFs.readFile( path.join(buildPath, 'react-ssr-manifest.json'), 'utf8' @@ -138,10 +144,24 @@ 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 = await createFromNodeStream(rscResponse, moduleMap); + let root; + let Root = () => { + if (root) { + return root; + } + return React.use( + (root = createFromNodeStream( + rscResponse, + ssrBundleConfig.ssrModuleMap, + { + moduleLoading: ssrBundleConfig.moduleLoading, + } + )) + ); + }; // 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, }); pipe(res); diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 0b9d6b9f48011..2f2c8b0b42609 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 {HintModel} from 'react-server/src/ReactFlightServerConfig'; @@ -33,6 +34,7 @@ import { readPartialStringChunk, readFinalStringChunk, createStringDecoder, + prepareDestinationForModule, } from './ReactFlightClientConfig'; import {registerServerReference} from './ReactFlightReplyClient'; @@ -175,7 +177,8 @@ Chunk.prototype.then = function ( }; export type Response = { - _bundlerConfig: SSRManifest, + _bundlerConfig: SSRModuleMap, + _moduleLoading: ModuleLoading, _callServer: CallServerCallback, _chunks: Map>, _fromJSON: (key: string, value: JSONValue) => any, @@ -703,12 +706,14 @@ function missingCall() { } export function createResponse( - bundlerConfig: SSRManifest, + bundlerConfig: SSRModuleMap, + moduleLoading: ModuleLoading, callServer: void | CallServerCallback, ): Response { const chunks: Map> = new Map(); const response: Response = { _bundlerConfig: bundlerConfig, + _moduleLoading: moduleLoading, _callServer: callServer !== undefined ? callServer : missingCall, _chunks: chunks, _stringDecoder: createStringDecoder(), @@ -771,6 +776,8 @@ function resolveModule( clientReferenceMetadata, ); + prepareDestinationForModule(response._moduleLoading, clientReferenceMetadata); + // TODO: Add an option to encode modules that are lazy loaded. // For now we preload all modules as early as possible since it's likely // that we'll need them. diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.custom.js b/packages/react-client/src/forks/ReactFlightClientConfig.custom.js index b5de594d4d46f..152bfd8b1d51b 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.custom.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.custom.js @@ -25,7 +25,8 @@ declare var $$$config: any; -export opaque type SSRManifest = mixed; +export opaque type ModuleLoading = mixed; +export opaque type SSRModuleMap = mixed; export opaque type ServerManifest = mixed; export opaque type ServerReferenceId = string; export opaque type ClientReferenceMetadata = mixed; @@ -35,6 +36,8 @@ export const resolveServerReference = $$$config.resolveServerReference; export const preloadModule = $$$config.preloadModule; export const requireModule = $$$config.requireModule; export const dispatchHint = $$$config.dispatchHint; +export const prepareDestinationForModule = + $$$config.prepareDestinationForModule; export const usedWithSSR = true; export opaque type Source = mixed; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js index 52212d1e0c869..f17151a1a1fa6 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js @@ -8,6 +8,8 @@ */ export * from 'react-client/src/ReactFlightClientConfigBrowser'; -export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackBrowser'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackBrowser'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = false; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js index 0ad00d57cdac4..6b72b535dbd7f 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js @@ -11,7 +11,8 @@ export * from 'react-client/src/ReactFlightClientConfigBrowser'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export type Response = any; -export opaque type SSRManifest = mixed; +export opaque type ModuleLoading = mixed; +export opaque type SSRModuleMap = mixed; export opaque type ServerManifest = mixed; export opaque type ServerReferenceId = string; export opaque type ClientReferenceMetadata = mixed; @@ -20,4 +21,5 @@ export const resolveClientReference: any = null; export const resolveServerReference: any = null; export const preloadModule: any = null; export const requireModule: any = null; +export const prepareDestinationForModule: any = null; export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js index 212290670bd57..954ca1f2a9845 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js @@ -8,6 +8,8 @@ */ export * from 'react-client/src/ReactFlightClientConfigBrowser'; -export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js index 0ad00d57cdac4..6b72b535dbd7f 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js @@ -11,7 +11,8 @@ export * from 'react-client/src/ReactFlightClientConfigBrowser'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export type Response = any; -export opaque type SSRManifest = mixed; +export opaque type ModuleLoading = mixed; +export opaque type SSRModuleMap = mixed; export opaque type ServerManifest = mixed; export opaque type ServerReferenceId = string; export opaque type ClientReferenceMetadata = mixed; @@ -20,4 +21,5 @@ export const resolveClientReference: any = null; export const resolveServerReference: any = null; export const preloadModule: any = null; export const requireModule: any = null; +export const prepareDestinationForModule: any = null; export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js index 4df4617caec67..4b4d77ce0cc54 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js @@ -8,6 +8,8 @@ */ export * from 'react-client/src/ReactFlightClientConfigNode'; -export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js index bf0ddb29fa434..554ddfdc40a66 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js @@ -8,6 +8,7 @@ */ export * from 'react-client/src/ReactFlightClientConfigNode'; -export * from 'react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode'; +export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = true; diff --git a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js index ed09056585094..9c3d5c7506832 100644 --- a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js +++ b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js @@ -63,3 +63,14 @@ export function dispatchHint(code: string, model: HintModel): void { } } } + +export function preinitModulesForSSR(href: string, crossOrigin: ?string) { + const dispatcher = ReactDOMCurrentDispatcher.current; + if (dispatcher) { + if (typeof crossOrigin === 'string') { + dispatcher.preinit(href, {as: 'script', crossOrigin}); + } else { + dispatcher.preinit(href, {as: 'script'}); + } + } +} diff --git a/packages/react-noop-renderer/src/ReactNoopFlightClient.js b/packages/react-noop-renderer/src/ReactNoopFlightClient.js index 013c663cb0c4e..3bd3d863ac45e 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightClient.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightClient.js @@ -35,6 +35,7 @@ const {createResponse, processBinaryChunk, getRoot, close} = ReactFlightClient({ resolveClientReference(bundlerConfig: null, idx: string) { return idx; }, + prepareDestinationForModule(moduleLoading: null, metadata: string) {}, preloadModule(idx: string) {}, requireModule(idx: string) { return readModule(idx); diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler.js b/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode.js similarity index 74% rename from packages/react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler.js rename to packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode.js index 0789a52ffc0e1..b8cabb96489a7 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,25 @@ 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, + metadata: ClientReferenceMetadata, +) { + prepareDestinationWithChunks(moduleLoading, metadata[CHUNKS]); +} + 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 +72,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..43b27db427b93 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler.js +++ b/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js @@ -13,35 +13,61 @@ 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, + metadata: ClientReferenceMetadata, +) { + prepareDestinationWithChunks(moduleLoading, metadata[CHUNKS]); +} + 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 +78,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 +128,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 +172,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 +190,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 +208,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 +219,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..61d5f5cdca5ef --- /dev/null +++ b/packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackBrowser.js @@ -0,0 +1,17 @@ +/** + * 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, +) { + // 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..5115d72f93091 --- /dev/null +++ b/packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer.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 + */ + +import {preinitModulesForSSR} 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, +) { + if (moduleLoading !== null) { + for (let i = 1; i < chunks.length; i += 2) { + preinitModulesForSSR( + moduleLoading.prefix + chunks[i], + moduleLoading.crossOrigin, + ); + } + } +} diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js index d91e7d7a755cb..d3c75495ec5af 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js @@ -34,6 +34,7 @@ export type Options = { function createResponseFromOptions(options: void | Options) { return createResponse( + null, null, options && options.callServer ? options.callServer : undefined, ); diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js index d9ce8f35a5262..c0612a87a7bd9 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js @@ -11,7 +11,10 @@ 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'; import { createResponse, @@ -39,12 +42,14 @@ export function createServerReference, T>( } export type Options = { - moduleMap?: $NonMaybeType, + moduleMap?: $NonMaybeType, + moduleLoading?: $NonMaybeType, }; function createResponseFromOptions(options: void | Options) { return createResponse( options && options.moduleMap ? options.moduleMap : null, + options && options.moduleLoading ? options.moduleLoading : null, noServerCall, ); } diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js index c6a14fb6b20e7..e571828a4ffa2 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js @@ -11,7 +11,10 @@ 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'; import type {Readable} from 'stream'; @@ -25,6 +28,10 @@ import { import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient'; +export type Options = { + moduleLoading?: $NonMaybeType, +}; + function noServerCall() { throw new Error( 'Server Functions cannot be called during initial render. ' + @@ -42,9 +49,16 @@ export function createServerReference, T>( function createFromNodeStream( stream: Readable, - moduleMap: $NonMaybeType, + moduleMap: $NonMaybeType, + options?: Options, ): Thenable { - const response: Response = createResponse(moduleMap, noServerCall); + const moduleLoading = + options && options.moduleLoading ? options.moduleLoading : null; + const response: Response = createResponse( + moduleMap, + moduleLoading, + noServerCall, + ); 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..29aaa64857336 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 ssrModuleMap: SSRModuleMap = {}; + const ssrBundleConfig: { + moduleLoading: { + prefix: string, + crossOrigin: string | null, + }, + ssrModuleMap: SSRModuleMap, + } = { + moduleLoading: { + prefix: compilation.outputOptions.publicPath || '', + crossOrigin: crossOriginMode, + }, + ssrModuleMap, + }; + + // 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; + ssrModuleMap[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 acb5724ce5006..7bf296a2237d2 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 ; } @@ -1308,25 +1318,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 ; } @@ -1426,16 +1436,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 ; } @@ -1526,21 +1536,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/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..97a51432e8eed 100644 --- a/packages/react/src/__tests__/ReactFetch-test.js +++ b/packages/react/src/__tests__/ReactFetch-test.js @@ -47,6 +47,11 @@ describe('ReactFetch', () => { jest.mock('react', () => require('react/react.shared-subset')); } + // We need to mock __webpack_require__ for browser builds + global.__webpack_require__ = function (id) { + // We don't actually expect to load any modules in this test + }; + React = require('react'); ReactServerDOMServer = require('react-server-dom-webpack/server.browser'); ReactServerDOMClient = require('react-server-dom-webpack/client'); diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index 18ba25264138e..42812413ddbf4 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 14c219ee7ac7b..7137503a16be2 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -35,6 +35,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', @@ -87,6 +89,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', @@ -141,6 +145,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', @@ -172,6 +178,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', @@ -226,7 +235,7 @@ module.exports = [ 'react-dom', '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',