From 24b7fc7ce513895e619255bb181ca4be0f50e48f Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 25 Aug 2023 13:52:55 -0700 Subject: [PATCH 1/5] Option to build additional packages without wiping already built packages --- scripts/rollup/build-all-release-channels.js | 37 ++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/scripts/rollup/build-all-release-channels.js b/scripts/rollup/build-all-release-channels.js index 25f2652ecf56..ebff33567fce 100644 --- a/scripts/rollup/build-all-release-channels.js +++ b/scripts/rollup/build-all-release-channels.js @@ -8,6 +8,7 @@ const fse = require('fs-extra'); const {spawnSync} = require('child_process'); const path = require('path'); const tmp = require('tmp'); +const argv = require('minimist')(process.argv.slice(2)); const { ReactVersion, @@ -69,6 +70,30 @@ if (process.env.CIRCLE_NODE_TOTAL) { processExperimental('./build'); } } else { + const priorDir = tmp.dirSync().name; + + if (argv.inc === true || argv.incremental === true) { + // stash existing build so we can recover prior build packages + // since this is an incremental rebuild + crossDeviceRenameSync('./build', priorDir); + let channels = fs.readdirSync(priorDir); + for (const channel of channels) { + const stat = fs.statSync(path.join(priorDir, channel)); + if (stat.isDirectory()) { + const packages = fs.readdirSync(path.join(priorDir, channel)); + for (const pkg of packages) { + try { + fs.statSync(path.join(priorDir, channel, pkg, 'package.json')); + } catch (error) { + spawnSync('rm', ['-rm', path.join(priorDir, channel, pkg)]); + } + } + } else { + spawnSync('rm', ['-rm', path.join(priorDir, channel)]); + } + } + } + // Running locally, no concurrency. Move each channel's build artifacts into // a temporary directory so that they don't conflict. buildForChannel('stable', '', ''); @@ -86,8 +111,16 @@ if (process.env.CIRCLE_NODE_TOTAL) { // In CI, merging is handled automatically by CircleCI's workspace feature. mergeDirsSync(experimentalDir + '/', stableDir + '/'); - // Now restore the combined directory back to its original name - crossDeviceRenameSync(stableDir, './build'); + if (argv.inc === true || argv.incremental === true) { + mergeDirsSync(stableDir + '/', priorDir + '/'); + // Now restore the combined directory back to its original name + // This will wipe out any previously built packages + crossDeviceRenameSync(priorDir, './build'); + } else { + // Now restore the combined directory back to its original name + // This will wipe out any previously built packages + crossDeviceRenameSync(stableDir, './build'); + } } function buildForChannel(channel, nodeTotal, nodeIndex) { From 08412fe7e21934cdb5c92d41593c4914422ff327 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 25 Aug 2023 11:24:29 -0700 Subject: [PATCH 2/5] dom-legacy does not make sense for Flight. we could still type check the files but it adds maintenance burden in the inlinedHostConfigs whenever things change there. Going to make these configs opaque mixed types to quiet flow since no entrypoints use the flight code --- .../src/forks/ReactFlightClientConfig.dom-legacy.js | 12 +++++++++++- .../src/forks/ReactFlightServerConfig.dom-legacy.js | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js index 212290670bd5..0ad00d57cdac 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js @@ -8,6 +8,16 @@ */ export * from 'react-client/src/ReactFlightClientConfigBrowser'; -export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; + +export type Response = any; +export opaque type SSRManifest = mixed; +export opaque type ServerManifest = mixed; +export opaque type ServerReferenceId = string; +export opaque type ClientReferenceMetadata = mixed; +export opaque type ClientReference = mixed; // eslint-disable-line no-unused-vars +export const resolveClientReference: any = null; +export const resolveServerReference: any = null; +export const preloadModule: any = null; +export const requireModule: any = null; export const usedWithSSR = true; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js index 212bd8c89a66..2e7273d82d69 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js @@ -9,7 +9,7 @@ import type {Request} from 'react-server/src/ReactFlightServer'; -export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler'; +export * from '../ReactFlightServerConfigBundlerCustom'; export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; export const supportsRequestStorage = false; From 11c575639589a01ff69470855504a20921e73eaf Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 14 Aug 2023 11:59:31 -0700 Subject: [PATCH 3/5] During SSR we should preinitialize imports so they can begin to be fetched before bootstrap in the browser --- .eslintrc.js | 6 +- fixtures/flight/.nvmrc | 1 + fixtures/flight/config/webpack.config.js | 6 + fixtures/flight/server/global.js | 23 +++- .../react-client/src/ReactFlightClient.js | 7 + .../forks/ReactFlightClientConfig.custom.js | 2 + .../ReactFlightClientConfig.dom-browser.js | 4 +- .../forks/ReactFlightClientConfig.dom-bun.js | 2 + ...eactFlightClientConfig.dom-edge-webpack.js | 4 +- .../ReactFlightClientConfig.dom-legacy.js | 2 + ...eactFlightClientConfig.dom-node-webpack.js | 4 +- .../forks/ReactFlightClientConfig.dom-node.js | 3 +- .../src/shared/ReactFlightClientConfigDOM.js | 11 ++ .../src/ReactNoopFlightClient.js | 1 + ... => ReactFlightClientConfigBundlerNode.js} | 41 ++++-- ... ReactFlightClientConfigBundlerWebpack.js} | 99 +++++++++----- ...FlightClientConfigBundlerWebpackBrowser.js | 28 ++++ ...tFlightClientConfigBundlerWebpackServer.js | 12 ++ ...tFlightClientConfigTargetWebpackBrowser.js | 17 +++ ...ctFlightClientConfigTargetWebpackServer.js | 30 +++++ .../src/ReactFlightDOMClientBrowser.js | 1 + .../src/ReactFlightDOMClientEdge.js | 7 +- .../src/ReactFlightDOMClientNode.js | 18 ++- .../ReactFlightServerConfigWebpackBundler.js | 25 ++-- .../src/ReactFlightWebpackPlugin.js | 74 ++++++++-- .../src/__tests__/ReactFlightDOM-test.js | 127 +++++++++--------- .../src/shared/ReactFlightClientReference.js | 45 +++++++ .../react/src/__tests__/ReactFetch-test.js | 5 + scripts/flow/environment.js | 4 +- scripts/jest/setupHostConfigs.js | 3 + scripts/shared/inlinedHostConfigs.js | 11 +- 31 files changed, 472 insertions(+), 151 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} (67%) 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/ReactFlightClientReference.js diff --git a/.eslintrc.js b/.eslintrc.js index 35e83b0d7abc..c36dc08f7def 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', }, @@ -426,7 +424,7 @@ module.exports = { files: ['packages/react-server-dom-webpack/**/*.js'], globals: { __webpack_chunk_load__: 'readonly', - __webpack_require__: 'readonly', + __webpack_require__: true, }, }, { diff --git a/fixtures/flight/.nvmrc b/fixtures/flight/.nvmrc new file mode 100644 index 000000000000..3f430af82b3d --- /dev/null +++ b/fixtures/flight/.nvmrc @@ -0,0 +1 @@ +v18 diff --git a/fixtures/flight/config/webpack.config.js b/fixtures/flight/config/webpack.config.js index de6eb9916bbf..b634cf2c4e52 100644 --- a/fixtures/flight/config/webpack.config.js +++ b/fixtures/flight/config/webpack.config.js @@ -248,6 +248,12 @@ module.exports = function (webpackEnv) { tsconfig: [paths.appTsConfig, paths.appJsConfig].filter(f => fs.existsSync(f) ), + react: [ + 'react/', + 'react-dom/', + 'react-server-dom-webpack/', + 'scheduler/', + ], }, }, infrastructureLogging: { diff --git a/fixtures/flight/server/global.js b/fixtures/flight/server/global.js index 16184287b122..372dfd79d5a9 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,9 +127,9 @@ 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'), + path.join(buildPath, 'react-ssr-bundle-config.json'), 'utf8' ) ); @@ -138,10 +144,19 @@ 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; + } + root = createFromNodeStream(rscResponse, ssrBundleConfig.ssrManifest, { + moduleLoading: ssrBundleConfig.moduleLoading, + }); + return root; + }; // 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 0b9d6b9f4801..9aa46f1aefdf 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -15,6 +15,7 @@ import type { ClientReferenceMetadata, SSRManifest, 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'; @@ -176,6 +178,7 @@ Chunk.prototype.then = function ( export type Response = { _bundlerConfig: SSRManifest, + _moduleLoading: ModuleLoading, _callServer: CallServerCallback, _chunks: Map>, _fromJSON: (key: string, value: JSONValue) => any, @@ -704,11 +707,13 @@ function missingCall() { export function createResponse( bundlerConfig: SSRManifest, + 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 b5de594d4d46..86740a1c4e4c 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.custom.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.custom.js @@ -35,6 +35,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 52212d1e0c86..f17151a1a1fa 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 0ad00d57cdac..65f3da05934a 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js @@ -11,6 +11,7 @@ export * from 'react-client/src/ReactFlightClientConfigBrowser'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export type Response = any; +export opaque type ModuleLoading = mixed; export opaque type SSRManifest = mixed; export opaque type ServerManifest = mixed; export opaque type ServerReferenceId = string; @@ -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 212290670bd5..954ca1f2a984 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 0ad00d57cdac..65f3da05934a 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js @@ -11,6 +11,7 @@ export * from 'react-client/src/ReactFlightClientConfigBrowser'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export type Response = any; +export opaque type ModuleLoading = mixed; export opaque type SSRManifest = mixed; export opaque type ServerManifest = mixed; export opaque type ServerReferenceId = string; @@ -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 4df4617caec6..4b4d77ce0cc5 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 bf0ddb29fa43..554ddfdc40a6 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 ed0905658509..9c3d5c750683 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 013c663cb0c4..3bd3d863ac45 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 0789a52ffc0e..05605700c400 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler.js +++ b/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode.js @@ -13,6 +13,17 @@ import type { RejectedThenable, } from 'shared/ReactTypes'; +import type {ClientReferenceMetadata as SharedClientReferenceMetadata} from './shared/ReactFlightClientReference'; +import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig'; + +import { + ID, + CHUNKS, + NAME, + isAsyncClientReference, +} from './shared/ReactFlightClientReference'; +import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig'; + export type SSRManifest = { [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 = SharedClientReferenceMetadata; // 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, 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: isAsyncClientReference(metadata), }; } diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler.js b/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js similarity index 67% rename from packages/react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler.js rename to packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js index ae94267a672c..59543f4ed5a0 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'; +import type { + ClientReferenceMetadata as SharedClientReferenceMetadata, + ClientReferenceManifestEntry as SharedClientReferenceManifestEntry, +} from './shared/ReactFlightClientReference'; +import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig'; + +import { + ID, + CHUNKS, + NAME, + isAsyncClientReference, +} from './shared/ReactFlightClientReference'; + +import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig'; + +import {loadChunk} from 'react-client/src/ReactFlightClientConfig'; + export type SSRManifest = null | { [clientId: string]: { - [clientExportName: string]: ClientReferenceMetadata, + [clientExportName: string]: ClientReferenceManifestEntry, }, }; export type ServerManifest = { - [id: string]: ClientReference, + [id: string]: SharedClientReferenceManifestEntry, }; export type ServerReferenceId = string; -export opaque type ClientReferenceMetadata = { - id: string, - chunks: Array, - name: string, - async: boolean, -}; +export opaque type ClientReferenceManifestEntry = SharedClientReferenceManifestEntry; +export opaque type ClientReferenceMetadata = SharedClientReferenceMetadata; // 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, 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 (isAsyncClientReference(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 (isAsyncClientReference(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 (isAsyncClientReference(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 000000000000..48779fb1e65e --- /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 000000000000..8eeb39a24a3e --- /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 000000000000..61d5f5cdca5e --- /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 000000000000..5115d72f9309 --- /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 d91e7d7a755c..d3c75495ec5a 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 d9ce8f35a526..ce687613e70a 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 { + SSRManifest, + ModuleLoading, +} from 'react-client/src/ReactFlightClientConfig'; import { createResponse, @@ -40,11 +43,13 @@ export function createServerReference, T>( export type Options = { 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 c6a14fb6b20e..83031f1cfef5 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 { + SSRManifest, + 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. ' + @@ -43,8 +50,15 @@ export function createServerReference, T>( function createFromNodeStream( stream: Readable, 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 b217ac1ef21f..5887fd7dcc20 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 { + ClientReferenceMetadata as SharedClientReferenceMetadata, + ClientReferenceManifestEntry as SharedClientReferenceManifestEntry, +} from './shared/ReactFlightClientReference'; 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 = SharedClientReferenceMetadata; +export opaque type ClientReferenceManifestEntry = SharedClientReferenceManifestEntry; 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 096f5ce0d1dc..c0b068a691dd 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 {ClientReferenceManifestEntry} from './shared/ReactFlightClientReference'; + import {join} from 'path'; import {pathToFileURL} from 'url'; import asyncLib from 'neo-async'; @@ -56,7 +58,7 @@ type Options = { clientReferences?: ClientReferencePath | $ReadOnlyArray, chunkName?: string, clientManifestFilename?: string, - ssrManifestFilename?: string, + ssrBundleConfigFilename?: string, }; const PLUGIN_NAME = 'React Server Plugin'; @@ -65,7 +67,7 @@ export default class ReactFlightWebpackPlugin { clientReferences: $ReadOnlyArray; chunkName: string; clientManifestFilename: string; - ssrManifestFilename: string; + ssrBundleConfigFilename: string; constructor(options: Options) { if (!options || typeof options.isServer !== 'boolean') { @@ -103,8 +105,8 @@ export default class ReactFlightWebpackPlugin { } this.clientManifestFilename = options.clientManifestFilename || 'react-client-manifest.json'; - this.ssrManifestFilename = - options.ssrManifestFilename || 'react-ssr-manifest.json'; + this.ssrBundleConfigFilename = + options.ssrBundleConfigFilename || 'react-ssr-bundle-config.json'; } apply(compiler: any) { @@ -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]: ClientReferenceManifestEntry, } = {}; - const ssrManifest: { + type SSRManifest = { [string]: { [string]: {specifier: string, name: string}, }, - } = {}; + }; + const ssrManifest: SSRManifest = {}; + const ssrBundleConfig: { + moduleLoading: { + prefix: string, + crossOrigin: string | null, + }, + ssrManifest: SSRManifest, + } = { + moduleLoading: { + prefix: compilation.outputOptions.publicPath || '', + crossOrigin: crossOriginMode, + }, + ssrManifest, + }; + + // 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] = { @@ -326,9 +372,9 @@ 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, + _this.ssrBundleConfigFilename, 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 acb5724ce500..932fd94b46e7 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; @@ -46,22 +50,25 @@ describe('ReactFlightDOM', () => { require('react-server-dom-webpack/server.node.unbundled'), ); - 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.node.unbundled'); + FlightReact = require('react/react.shared-subset'); + FlightReactDOM = require('react-dom'); + + // This reset is to load modules for the SSR/Browser scope. + 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 +492,7 @@ describe('ReactFlightDOM', () => { const AsyncModuleRef = clientExports(AsyncModule); function ServerComponent() { - const text = use(AsyncModuleRef); + const text = FlightReact.use(AsyncModuleRef); return

{text}

; } @@ -1205,25 +1212,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 +1315,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 +1433,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 +1533,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/ReactFlightClientReference.js b/packages/react-server-dom-webpack/src/shared/ReactFlightClientReference.js new file mode 100644 index 000000000000..93c740268da6 --- /dev/null +++ b/packages/react-server-dom-webpack/src/shared/ReactFlightClientReference.js @@ -0,0 +1,45 @@ +/** + * 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 ClientReferenceManifestEntry = { + 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 ClientReferenceMetadata = + | [ + /* 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 isAsyncClientReference( + metadata: ClientReferenceMetadata, +): boolean { + return metadata.length === 4; +} diff --git a/packages/react/src/__tests__/ReactFetch-test.js b/packages/react/src/__tests__/ReactFetch-test.js index 5a8911888bdf..97a51432e8ee 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 18ba25264138..42812413ddbf 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 5ea6eb0f5810..48eaf0fd5b73 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 14c219ee7ac7..7137503a16be 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', From c8926bb7eaf1402f0c2d21168b0c38cb293605b7 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Thu, 10 Aug 2023 09:17:21 -0700 Subject: [PATCH 4/5] ESM implementation of module preinitialization --- fixtures/flight-esm/.nvmrc | 1 + fixtures/flight-esm/server/global.js | 38 ++++++++++++++----- fixtures/flight-esm/server/region.js | 3 ++ fixtures/flight-esm/src/App.js | 20 +--------- fixtures/flight/server/global.js | 15 +++++--- fixtures/flight/server/region.js | 3 ++ .../forks/ReactFlightClientConfig.custom.js | 1 + ...ReactFlightClientConfig.dom-browser-esm.js | 3 +- .../ReactFlightClientConfig.dom-node-esm.js | 5 ++- .../src/server/ReactFizzConfigDOM.js | 1 + .../src/shared/ReactFlightClientConfigDOM.js | 13 ++++++- ...s => ReactFlightClientConfigBundlerESM.js} | 16 ++++++++ ...ReactFlightClientConfigTargetESMBrowser.js | 17 +++++++++ .../ReactFlightClientConfigTargetESMServer.js | 30 +++++++++++++++ .../src/ReactFlightDOMClientBrowser.js | 1 + .../src/ReactFlightDOMClientNode.js | 8 +++- ...ctFlightClientConfigTargetWebpackServer.js | 4 +- packages/shared/ReactVersion.js | 17 +-------- scripts/shared/inlinedHostConfigs.js | 4 +- 19 files changed, 141 insertions(+), 59 deletions(-) create mode 100644 fixtures/flight-esm/.nvmrc rename packages/react-server-dom-esm/src/{ReactFlightClientConfigESMBundler.js => ReactFlightClientConfigBundlerESM.js} (77%) create mode 100644 packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser.js create mode 100644 packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer.js diff --git a/fixtures/flight-esm/.nvmrc b/fixtures/flight-esm/.nvmrc new file mode 100644 index 000000000000..3f430af82b3d --- /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 d6aaf4cc8ca1..1088d42967a2 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/server/region.js b/fixtures/flight-esm/server/region.js index c7e8d9aad33c..13051de9e4b3 100644 --- a/fixtures/flight-esm/server/region.js +++ b/fixtures/flight-esm/server/region.js @@ -36,6 +36,9 @@ async function renderApp(res, returnValue) { // For client-invoked server actions we refresh the tree and return a return value. const payload = returnValue ? {returnValue, root} : root; const {pipe} = renderToPipeableStream(payload, moduleBasePath); + await new Promise(res => { + setTimeout(res, 1000); + }); pipe(res); } diff --git a/fixtures/flight-esm/src/App.js b/fixtures/flight-esm/src/App.js index 161776eddd61..d5945280469b 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/server/global.js b/fixtures/flight/server/global.js index 372dfd79d5a9..195bda468dd4 100644 --- a/fixtures/flight/server/global.js +++ b/fixtures/flight/server/global.js @@ -147,12 +147,17 @@ app.all('/', async function (req, res, next) { let root; let Root = () => { if (root) { - return root; + return React.use(root); } - root = createFromNodeStream(rscResponse, ssrBundleConfig.ssrManifest, { - moduleLoading: ssrBundleConfig.moduleLoading, - }); - return root; + return React.use( + (root = createFromNodeStream( + rscResponse, + ssrBundleConfig.ssrManifest, + { + moduleLoading: ssrBundleConfig.moduleLoading, + } + )) + ); }; // Render it into HTML by resolving the client components res.set('Content-type', 'text/html'); diff --git a/fixtures/flight/server/region.js b/fixtures/flight/server/region.js index 4beae03ff35e..31b66f92f4d4 100644 --- a/fixtures/flight/server/region.js +++ b/fixtures/flight/server/region.js @@ -95,6 +95,9 @@ async function renderApp(res, returnValue) { // For client-invoked server actions we refresh the tree and return a return value. const payload = returnValue ? {returnValue, root} : root; const {pipe} = renderToPipeableStream(payload, moduleMap); + await new Promise(res => { + setTimeout(res, 1000); + }); pipe(res); } diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.custom.js b/packages/react-client/src/forks/ReactFlightClientConfig.custom.js index 86740a1c4e4c..90e12eed656e 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.custom.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.custom.js @@ -25,6 +25,7 @@ declare var $$$config: any; +export opaque type ModuleLoading = mixed; export opaque type SSRManifest = mixed; export opaque type ServerManifest = mixed; export opaque type ServerReferenceId = string; 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 53058d0d1884..ec71cd94382c 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js @@ -8,6 +8,7 @@ */ export * from 'react-client/src/ReactFlightClientConfigBrowser'; -export * from 'react-server-dom-esm/src/ReactFlightClientConfigESMBundler'; +export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM'; +export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = false; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js index 8390c4c06b43..016ac820d356 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js @@ -7,7 +7,8 @@ * @flow */ -export * from 'react-client/src/ReactFlightClientConfigBrowser'; -export * from 'react-server-dom-esm/src/ReactFlightClientConfigESMBundler'; +export * from 'react-client/src/ReactFlightClientConfigNode'; +export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM'; +export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = true; diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 0eb1c51f7262..2b1634f52469 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -5328,6 +5328,7 @@ function preinit(href: string, options: PreinitOptions): void { } return; } + case 'module': case 'script': { const src = href; const key = getResourceKey(as, src); diff --git a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js index 9c3d5c750683..1eaabb948d63 100644 --- a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js +++ b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js @@ -64,7 +64,18 @@ export function dispatchHint(code: string, model: HintModel): void { } } -export function preinitModulesForSSR(href: string, crossOrigin: ?string) { +export function preinitModuleForSSR(href: string, crossOrigin: ?string) { + const dispatcher = ReactDOMCurrentDispatcher.current; + if (dispatcher) { + if (typeof crossOrigin === 'string') { + dispatcher.preinitModule(href, {crossOrigin}); + } else { + dispatcher.preinitModule(href); + } + } +} + +export function preinitScriptForSSR(href: string, crossOrigin: ?string) { const dispatcher = ReactDOMCurrentDispatcher.current; if (dispatcher) { if (typeof crossOrigin === 'string') { diff --git a/packages/react-server-dom-esm/src/ReactFlightClientConfigESMBundler.js b/packages/react-server-dom-esm/src/ReactFlightClientConfigBundlerESM.js similarity index 77% rename from packages/react-server-dom-esm/src/ReactFlightClientConfigESMBundler.js rename to packages/react-server-dom-esm/src/ReactFlightClientConfigBundlerESM.js index 55deba307367..5779a7100153 100644 --- a/packages/react-server-dom-esm/src/ReactFlightClientConfigESMBundler.js +++ b/packages/react-server-dom-esm/src/ReactFlightClientConfigBundlerESM.js @@ -12,6 +12,7 @@ import type { FulfilledThenable, RejectedThenable, } from 'shared/ReactTypes'; +import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig'; export type SSRManifest = string; // Module root path @@ -19,6 +20,8 @@ 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,6 +33,19 @@ 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, + metadata: ClientReferenceMetadata, +) { + prepareDestinationForModuleImpl(moduleLoading, metadata[0]); +} + export function resolveClientReference( bundlerConfig: SSRManifest, metadata: ClientReferenceMetadata, 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 000000000000..2906fa52758b --- /dev/null +++ b/packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser.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 prepareDestinationForModuleImpl( + 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-esm/src/ReactFlightClientConfigTargetESMServer.js b/packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer.js new file mode 100644 index 000000000000..cc549b581d9b --- /dev/null +++ b/packages/react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer.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 {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, +) { + if (typeof moduleLoading === 'string') { + preinitModuleForSSR(moduleLoading + mod, undefined); + } else if (moduleLoading !== null) { + preinitModuleForSSR(moduleLoading.prefix + mod, moduleLoading.crossOrigin); + } +} diff --git a/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js index e3ebaf3fb1aa..701286d32f34 100644 --- a/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js @@ -36,6 +36,7 @@ export type Options = { function createResponseFromOptions(options: void | Options) { return createResponse( options && options.moduleBaseURL ? options.moduleBaseURL : '', + null, options && options.callServer ? options.callServer : undefined, ); } diff --git a/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js index 4288d5878a92..7a8a77bffa95 100644 --- a/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js @@ -41,9 +41,13 @@ export function createServerReference, T>( function createFromNodeStream( stream: Readable, moduleRootPath: string, - moduleBaseURL: string, // TODO: Used for preloading hints + moduleBaseURL: string, ): Thenable { - const response: Response = createResponse(moduleRootPath, noServerCall); + const response: Response = createResponse( + moduleRootPath, + moduleBaseURL, + noServerCall, + ); stream.on('data', chunk => { processBinaryChunk(response, chunk); }); diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer.js b/packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer.js index 5115d72f9309..55c763bd0127 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer.js +++ b/packages/react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer.js @@ -7,7 +7,7 @@ * @flow */ -import {preinitModulesForSSR} from 'react-client/src/ReactFlightClientConfig'; +import {preinitScriptForSSR} from 'react-client/src/ReactFlightClientConfig'; export type ModuleLoading = null | { prefix: string, @@ -21,7 +21,7 @@ export function prepareDestinationWithChunks( ) { if (moduleLoading !== null) { for (let i = 1; i < chunks.length; i += 2) { - preinitModulesForSSR( + preinitScriptForSSR( moduleLoading.prefix + chunks[i], moduleLoading.crossOrigin, ); diff --git a/packages/shared/ReactVersion.js b/packages/shared/ReactVersion.js index d0a14cfff780..9055167e29cc 100644 --- a/packages/shared/ReactVersion.js +++ b/packages/shared/ReactVersion.js @@ -1,16 +1 @@ -/** - * 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. - */ - -// TODO: this is special because it gets imported during build. -// -// TODO: 18.0.0 has not been released to NPM; -// It exists as a placeholder so that DevTools can support work tag changes between releases. -// When we next publish a release, update the matching TODO in backend/renderer.js -// TODO: This module is used both by the release scripts and to expose a version -// at runtime. We should instead inject the version number as part of the build -// process, and use the ReactVersions.js module as the single source of truth. -export default '18.2.0'; +export default '18.3.0-PLACEHOLDER'; diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 7137503a16be..8073b5a3441a 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -112,6 +112,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', @@ -214,7 +215,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', From db45991c33ab1988952fc6594a8e8b2a33ab808d Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 25 Aug 2023 08:39:13 -0700 Subject: [PATCH 5/5] Implements react-server-dom-turbopack --- .eslintrc.js | 8 + ...lightClientConfig.dom-browser-turbopack.js | 15 + ...ctFlightClientConfig.dom-edge-turbopack.js | 15 + ...ClientConfig.dom-node-turbopack-bundled.js | 15 + ...ctFlightClientConfig.dom-node-turbopack.js | 14 + packages/react-server-dom-turbopack/README.md | 5 + .../client.browser.js | 10 + .../react-server-dom-turbopack/client.edge.js | 10 + packages/react-server-dom-turbopack/client.js | 10 + .../react-server-dom-turbopack/client.node.js | 10 + .../client.node.unbundled.js | 10 + .../esm/package.json | 3 + ...om-turbopack-node-loader.production.min.js | 10 + packages/react-server-dom-turbopack/index.js | 10 + .../node-register.js | 10 + .../npm/client.browser.js | 7 + .../npm/client.edge.js | 7 + .../react-server-dom-turbopack/npm/client.js | 3 + .../npm/client.node.js | 7 + .../npm/client.node.unbundled.js | 7 + .../npm/esm/package.json | 3 + .../react-server-dom-turbopack/npm/index.js | 12 + .../npm/node-register.js | 3 + .../npm/server.browser.js | 7 + .../npm/server.edge.js | 7 + .../react-server-dom-turbopack/npm/server.js | 6 + .../npm/server.node.js | 7 + .../npm/server.node.unbundled.js | 7 + .../react-server-dom-turbopack/package.json | 95 + .../server.browser.js | 10 + .../react-server-dom-turbopack/server.edge.js | 10 + packages/react-server-dom-turbopack/server.js | 13 + .../react-server-dom-turbopack/server.node.js | 10 + .../server.node.unbundled.js | 10 + .../src/ReactFlightClientConfigBundlerNode.js | 161 ++ ...ReactFlightClientConfigBundlerTurbopack.js | 231 +++ ...ightClientConfigBundlerTurbopackBrowser.js | 12 + ...lightClientConfigBundlerTurbopackServer.js | 12 + ...lightClientConfigTargetTurbopackBrowser.js | 17 + ...FlightClientConfigTargetTurbopackServer.js | 30 + .../src/ReactFlightDOMClientBrowser.js | 110 ++ .../src/ReactFlightDOMClientEdge.js | 109 ++ .../src/ReactFlightDOMClientNode.js | 72 + .../src/ReactFlightDOMServerBrowser.js | 100 ++ .../src/ReactFlightDOMServerEdge.js | 100 ++ .../src/ReactFlightDOMServerNode.js | 170 ++ ...ReactFlightServerConfigTurbopackBundler.js | 93 + .../src/ReactFlightTurbopackNodeLoader.js | 483 +++++ .../src/ReactFlightTurbopackNodeRegister.js | 110 ++ .../src/ReactFlightTurbopackReferences.js | 256 +++ .../src/__tests__/ReactFlightDOM-test.js | 1590 +++++++++++++++++ .../__tests__/ReactFlightDOMBrowser-test.js | 1225 +++++++++++++ .../src/__tests__/ReactFlightDOMEdge-test.js | 189 ++ .../src/__tests__/ReactFlightDOMForm-test.js | 239 +++ .../src/__tests__/ReactFlightDOMNode-test.js | 168 ++ .../src/__tests__/ReactFlightDOMReply-test.js | 232 +++ .../src/__tests__/utils/WebpackMock.js | 141 ++ .../src/shared/ReactFlightClientReference.js | 41 + ...lightServerConfig.dom-browser-turbopack.js | 16 + ...ctFlightServerConfig.dom-edge-turbopack.js | 18 + ...ctFlightServerConfig.dom-node-turbopack.js | 19 + scripts/flow/environment.js | 5 + scripts/rollup/bundles.js | 103 ++ scripts/shared/inlinedHostConfigs.js | 176 +- 64 files changed, 6593 insertions(+), 21 deletions(-) create mode 100644 packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js create mode 100644 packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js create mode 100644 packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js create mode 100644 packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js create mode 100644 packages/react-server-dom-turbopack/README.md create mode 100644 packages/react-server-dom-turbopack/client.browser.js create mode 100644 packages/react-server-dom-turbopack/client.edge.js create mode 100644 packages/react-server-dom-turbopack/client.js create mode 100644 packages/react-server-dom-turbopack/client.node.js create mode 100644 packages/react-server-dom-turbopack/client.node.unbundled.js create mode 100644 packages/react-server-dom-turbopack/esm/package.json create mode 100644 packages/react-server-dom-turbopack/esm/react-server-dom-turbopack-node-loader.production.min.js create mode 100644 packages/react-server-dom-turbopack/index.js create mode 100644 packages/react-server-dom-turbopack/node-register.js create mode 100644 packages/react-server-dom-turbopack/npm/client.browser.js create mode 100644 packages/react-server-dom-turbopack/npm/client.edge.js create mode 100644 packages/react-server-dom-turbopack/npm/client.js create mode 100644 packages/react-server-dom-turbopack/npm/client.node.js create mode 100644 packages/react-server-dom-turbopack/npm/client.node.unbundled.js create mode 100644 packages/react-server-dom-turbopack/npm/esm/package.json create mode 100644 packages/react-server-dom-turbopack/npm/index.js create mode 100644 packages/react-server-dom-turbopack/npm/node-register.js create mode 100644 packages/react-server-dom-turbopack/npm/server.browser.js create mode 100644 packages/react-server-dom-turbopack/npm/server.edge.js create mode 100644 packages/react-server-dom-turbopack/npm/server.js create mode 100644 packages/react-server-dom-turbopack/npm/server.node.js create mode 100644 packages/react-server-dom-turbopack/npm/server.node.unbundled.js create mode 100644 packages/react-server-dom-turbopack/package.json create mode 100644 packages/react-server-dom-turbopack/server.browser.js create mode 100644 packages/react-server-dom-turbopack/server.edge.js create mode 100644 packages/react-server-dom-turbopack/server.js create mode 100644 packages/react-server-dom-turbopack/server.node.js create mode 100644 packages/react-server-dom-turbopack/server.node.unbundled.js create mode 100644 packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js create mode 100644 packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js create mode 100644 packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser.js create mode 100644 packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js create mode 100644 packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser.js create mode 100644 packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer.js create mode 100644 packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js create mode 100644 packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js create mode 100644 packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js create mode 100644 packages/react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js create mode 100644 packages/react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js create mode 100644 packages/react-server-dom-turbopack/src/ReactFlightDOMServerNode.js create mode 100644 packages/react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler.js create mode 100644 packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js create mode 100644 packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js create mode 100644 packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js create mode 100644 packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js create mode 100644 packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMBrowser-test.js create mode 100644 packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMEdge-test.js create mode 100644 packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMForm-test.js create mode 100644 packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMNode-test.js create mode 100644 packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMReply-test.js create mode 100644 packages/react-server-dom-turbopack/src/__tests__/utils/WebpackMock.js create mode 100644 packages/react-server-dom-turbopack/src/shared/ReactFlightClientReference.js create mode 100644 packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-turbopack.js create mode 100644 packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-turbopack.js create mode 100644 packages/react-server/src/forks/ReactFlightServerConfig.dom-node-turbopack.js diff --git a/.eslintrc.js b/.eslintrc.js index c36dc08f7def..4fd0c2ac02a3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -326,6 +326,7 @@ module.exports = { 'packages/react-refresh/**/*.js', 'packages/react-server-dom-esm/**/*.js', 'packages/react-server-dom-webpack/**/*.js', + 'packages/react-server-dom-turbopack/**/*.js', 'packages/react-test-renderer/**/*.js', 'packages/react-debug-tools/**/*.js', 'packages/react-devtools-extensions/**/*.js', @@ -427,6 +428,13 @@ module.exports = { __webpack_require__: true, }, }, + { + files: ['packages/react-server-dom-turbopack/**/*.js'], + globals: { + __turbopack_load__: 'readonly', + __turbopack_require__: 'readonly', + }, + }, { files: ['packages/scheduler/**/*.js'], globals: { diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js new file mode 100644 index 000000000000..9cb36f0674d0 --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js @@ -0,0 +1,15 @@ +/** + * 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 * from 'react-client/src/ReactFlightClientConfigBrowser'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser'; +export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; +export const usedWithSSR = false; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js new file mode 100644 index 000000000000..3c6949554e36 --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js @@ -0,0 +1,15 @@ +/** + * 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 * from 'react-client/src/ReactFlightClientConfigBrowser'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer'; +export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; +export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js new file mode 100644 index 000000000000..30e737d14f66 --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js @@ -0,0 +1,15 @@ +/** + * 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 * from 'react-client/src/ReactFlightClientConfigNode'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer'; +export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; +export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js new file mode 100644 index 000000000000..ad8f3608102c --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js @@ -0,0 +1,14 @@ +/** + * 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 * from 'react-client/src/ReactFlightClientConfigNode'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer'; +export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; +export const usedWithSSR = true; diff --git a/packages/react-server-dom-turbopack/README.md b/packages/react-server-dom-turbopack/README.md new file mode 100644 index 000000000000..3d183a8a4a2f --- /dev/null +++ b/packages/react-server-dom-turbopack/README.md @@ -0,0 +1,5 @@ +# react-server-dom-turbopack + +Experimental React Flight bindings for DOM using Turbopack. + +**Use it at your own risk.** diff --git a/packages/react-server-dom-turbopack/client.browser.js b/packages/react-server-dom-turbopack/client.browser.js new file mode 100644 index 000000000000..7d26c2771e50 --- /dev/null +++ b/packages/react-server-dom-turbopack/client.browser.js @@ -0,0 +1,10 @@ +/** + * 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 * from './src/ReactFlightDOMClientBrowser'; diff --git a/packages/react-server-dom-turbopack/client.edge.js b/packages/react-server-dom-turbopack/client.edge.js new file mode 100644 index 000000000000..fadceeaf8443 --- /dev/null +++ b/packages/react-server-dom-turbopack/client.edge.js @@ -0,0 +1,10 @@ +/** + * 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 * from './src/ReactFlightDOMClientEdge'; diff --git a/packages/react-server-dom-turbopack/client.js b/packages/react-server-dom-turbopack/client.js new file mode 100644 index 000000000000..2dad5bb51387 --- /dev/null +++ b/packages/react-server-dom-turbopack/client.js @@ -0,0 +1,10 @@ +/** + * 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 * from './client.browser'; diff --git a/packages/react-server-dom-turbopack/client.node.js b/packages/react-server-dom-turbopack/client.node.js new file mode 100644 index 000000000000..4f435353a20f --- /dev/null +++ b/packages/react-server-dom-turbopack/client.node.js @@ -0,0 +1,10 @@ +/** + * 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 * from './src/ReactFlightDOMClientNode'; diff --git a/packages/react-server-dom-turbopack/client.node.unbundled.js b/packages/react-server-dom-turbopack/client.node.unbundled.js new file mode 100644 index 000000000000..4f435353a20f --- /dev/null +++ b/packages/react-server-dom-turbopack/client.node.unbundled.js @@ -0,0 +1,10 @@ +/** + * 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 * from './src/ReactFlightDOMClientNode'; diff --git a/packages/react-server-dom-turbopack/esm/package.json b/packages/react-server-dom-turbopack/esm/package.json new file mode 100644 index 000000000000..3dbc1ca591c0 --- /dev/null +++ b/packages/react-server-dom-turbopack/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/react-server-dom-turbopack/esm/react-server-dom-turbopack-node-loader.production.min.js b/packages/react-server-dom-turbopack/esm/react-server-dom-turbopack-node-loader.production.min.js new file mode 100644 index 000000000000..ef6486656caf --- /dev/null +++ b/packages/react-server-dom-turbopack/esm/react-server-dom-turbopack-node-loader.production.min.js @@ -0,0 +1,10 @@ +/** + * 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 * from '../src/ReactFlightTurbopackNodeLoader.js'; diff --git a/packages/react-server-dom-turbopack/index.js b/packages/react-server-dom-turbopack/index.js new file mode 100644 index 000000000000..348324f0de86 --- /dev/null +++ b/packages/react-server-dom-turbopack/index.js @@ -0,0 +1,10 @@ +/** + * 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 + */ + +throw new Error('Use react-server-dom-turbopack/client instead.'); diff --git a/packages/react-server-dom-turbopack/node-register.js b/packages/react-server-dom-turbopack/node-register.js new file mode 100644 index 000000000000..0d399f384273 --- /dev/null +++ b/packages/react-server-dom-turbopack/node-register.js @@ -0,0 +1,10 @@ +/** + * 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 + */ + +module.exports = require('./src/ReactFlightTurbopackNodeRegister'); diff --git a/packages/react-server-dom-turbopack/npm/client.browser.js b/packages/react-server-dom-turbopack/npm/client.browser.js new file mode 100644 index 000000000000..5cd0ada18845 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/client.browser.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-turbopack-client.browser.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-turbopack-client.browser.development.js'); +} diff --git a/packages/react-server-dom-turbopack/npm/client.edge.js b/packages/react-server-dom-turbopack/npm/client.edge.js new file mode 100644 index 000000000000..3499ce22c2a3 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/client.edge.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-turbopack-client.edge.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-turbopack-client.edge.development.js'); +} diff --git a/packages/react-server-dom-turbopack/npm/client.js b/packages/react-server-dom-turbopack/npm/client.js new file mode 100644 index 000000000000..89d93a7a7920 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/client.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./client.browser'); diff --git a/packages/react-server-dom-turbopack/npm/client.node.js b/packages/react-server-dom-turbopack/npm/client.node.js new file mode 100644 index 000000000000..c346d351d344 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/client.node.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-turbopack-client.node.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-turbopack-client.node.development.js'); +} diff --git a/packages/react-server-dom-turbopack/npm/client.node.unbundled.js b/packages/react-server-dom-turbopack/npm/client.node.unbundled.js new file mode 100644 index 000000000000..9b15ea16d8a3 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/client.node.unbundled.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-turbopack-client.node.unbundled.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-turbopack-client.node.unbundled.development.js'); +} diff --git a/packages/react-server-dom-turbopack/npm/esm/package.json b/packages/react-server-dom-turbopack/npm/esm/package.json new file mode 100644 index 000000000000..3dbc1ca591c0 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/react-server-dom-turbopack/npm/index.js b/packages/react-server-dom-turbopack/npm/index.js new file mode 100644 index 000000000000..53e8a98128cd --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/index.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 + */ + +'use strict'; + +throw new Error('Use react-server-dom-turbopack/client instead.'); diff --git a/packages/react-server-dom-turbopack/npm/node-register.js b/packages/react-server-dom-turbopack/npm/node-register.js new file mode 100644 index 000000000000..7506743f033f --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/node-register.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./cjs/react-server-dom-turbopack-node-register.js'); diff --git a/packages/react-server-dom-turbopack/npm/server.browser.js b/packages/react-server-dom-turbopack/npm/server.browser.js new file mode 100644 index 000000000000..05c0b03496bd --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/server.browser.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-turbopack-server.browser.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-turbopack-server.browser.development.js'); +} diff --git a/packages/react-server-dom-turbopack/npm/server.edge.js b/packages/react-server-dom-turbopack/npm/server.edge.js new file mode 100644 index 000000000000..b09cb0b82282 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/server.edge.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-turbopack-server.edge.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-turbopack-server.edge.development.js'); +} diff --git a/packages/react-server-dom-turbopack/npm/server.js b/packages/react-server-dom-turbopack/npm/server.js new file mode 100644 index 000000000000..13a632e64117 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/server.js @@ -0,0 +1,6 @@ +'use strict'; + +throw new Error( + 'The React Server Writer cannot be used outside a react-server environment. ' + + 'You must configure Node.js using the `--conditions react-server` flag.' +); diff --git a/packages/react-server-dom-turbopack/npm/server.node.js b/packages/react-server-dom-turbopack/npm/server.node.js new file mode 100644 index 000000000000..59635310eb2d --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/server.node.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-turbopack-server.node.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-turbopack-server.node.development.js'); +} diff --git a/packages/react-server-dom-turbopack/npm/server.node.unbundled.js b/packages/react-server-dom-turbopack/npm/server.node.unbundled.js new file mode 100644 index 000000000000..4f8856e4303a --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/server.node.unbundled.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-turbopack-server.node.unbundled.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-turbopack-server.node.unbundled.development.js'); +} diff --git a/packages/react-server-dom-turbopack/package.json b/packages/react-server-dom-turbopack/package.json new file mode 100644 index 000000000000..36e876561736 --- /dev/null +++ b/packages/react-server-dom-turbopack/package.json @@ -0,0 +1,95 @@ +{ + "name": "react-server-dom-turbopack", + "description": "React Server Components bindings for DOM using Turbopack. This is intended to be integrated into meta-frameworks. It is not intended to be imported directly.", + "version": "18.2.0", + "keywords": [ + "react" + ], + "homepage": "https://reactjs.org/", + "bugs": "https://github.com/facebook/react/issues", + "license": "MIT", + "files": [ + "LICENSE", + "README.md", + "index.js", + "client.js", + "client.browser.js", + "client.edge.js", + "client.node.js", + "client.node.unbundled.js", + "server.js", + "server.browser.js", + "server.edge.js", + "server.node.js", + "server.node.unbundled.js", + "node-register.js", + "cjs/", + "umd/", + "esm/" + ], + "exports": { + ".": "./index.js", + "./client": { + "workerd": "./client.edge.js", + "deno": "./client.edge.js", + "worker": "./client.edge.js", + "node": { + "turbopack": "./client.node.js", + "webpack": "./client.node.js", + "default": "./client.node.unbundled.js" + }, + "edge-light": "./client.edge.js", + "browser": "./client.browser.js", + "default": "./client.browser.js" + }, + "./client.browser": "./client.browser.js", + "./client.edge": "./client.edge.js", + "./client.node": "./client.node.js", + "./client.node.unbundled": "./client.node.unbundled.js", + "./server": { + "react-server": { + "workerd": "./server.edge.js", + "deno": "./server.browser.js", + "node": { + "turbopack": "./server.node.js", + "webpack": "./server.node.js", + "default": "./server.node.unbundled.js" + }, + "edge-light": "./server.edge.js", + "browser": "./server.browser.js" + }, + "default": "./server.js" + }, + "./server.browser": "./server.browser.js", + "./server.edge": "./server.edge.js", + "./server.node": "./server.node.js", + "./server.node.unbundled": "./server.node.unbundled.js", + "./node-loader": "./esm/react-server-dom-turbopack-node-loader.production.min.js", + "./node-register": "./node-register.js", + "./src/*": "./src/*.js", + "./package.json": "./package.json" + }, + "main": "index.js", + "repository": { + "type" : "git", + "url" : "https://github.com/facebook/react.git", + "directory": "packages/react-server-dom-turbopack" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "dependencies": { + "acorn-loose": "^8.3.0", + "neo-async": "^2.6.1", + "loose-envify": "^1.1.0" + }, + "browserify": { + "transform": [ + "loose-envify" + ] + } +} diff --git a/packages/react-server-dom-turbopack/server.browser.js b/packages/react-server-dom-turbopack/server.browser.js new file mode 100644 index 000000000000..41a9fb5c4496 --- /dev/null +++ b/packages/react-server-dom-turbopack/server.browser.js @@ -0,0 +1,10 @@ +/** + * 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 * from './src/ReactFlightDOMServerBrowser'; diff --git a/packages/react-server-dom-turbopack/server.edge.js b/packages/react-server-dom-turbopack/server.edge.js new file mode 100644 index 000000000000..98f975cb4706 --- /dev/null +++ b/packages/react-server-dom-turbopack/server.edge.js @@ -0,0 +1,10 @@ +/** + * 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 * from './src/ReactFlightDOMServerEdge'; diff --git a/packages/react-server-dom-turbopack/server.js b/packages/react-server-dom-turbopack/server.js new file mode 100644 index 000000000000..83d8b8a017ff --- /dev/null +++ b/packages/react-server-dom-turbopack/server.js @@ -0,0 +1,13 @@ +/** + * 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 + */ + +throw new Error( + 'The React Server cannot be used outside a react-server environment. ' + + 'You must configure Node.js using the `--conditions react-server` flag.', +); diff --git a/packages/react-server-dom-turbopack/server.node.js b/packages/react-server-dom-turbopack/server.node.js new file mode 100644 index 000000000000..7726b9bb929d --- /dev/null +++ b/packages/react-server-dom-turbopack/server.node.js @@ -0,0 +1,10 @@ +/** + * 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 * from './src/ReactFlightDOMServerNode'; diff --git a/packages/react-server-dom-turbopack/server.node.unbundled.js b/packages/react-server-dom-turbopack/server.node.unbundled.js new file mode 100644 index 000000000000..7726b9bb929d --- /dev/null +++ b/packages/react-server-dom-turbopack/server.node.unbundled.js @@ -0,0 +1,10 @@ +/** + * 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 * from './src/ReactFlightDOMServerNode'; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js new file mode 100644 index 000000000000..05605700c400 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js @@ -0,0 +1,161 @@ +/** + * 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 type { + Thenable, + FulfilledThenable, + RejectedThenable, +} from 'shared/ReactTypes'; + +import type {ClientReferenceMetadata as SharedClientReferenceMetadata} from './shared/ReactFlightClientReference'; +import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig'; + +import { + ID, + CHUNKS, + NAME, + isAsyncClientReference, +} from './shared/ReactFlightClientReference'; +import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig'; + +export type SSRManifest = { + [clientId: string]: { + [clientExportName: string]: ClientReference, + }, +}; + +export type ServerManifest = void; + +export type ServerReferenceId = string; + +export opaque type ClientReferenceMetadata = SharedClientReferenceMetadata; + +// eslint-disable-next-line no-unused-vars +export opaque type ClientReference = { + specifier: string, + name: string, + 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, + metadata: ClientReferenceMetadata, +): ClientReference { + const moduleExports = bundlerConfig[metadata[ID]]; + let resolvedModuleData = moduleExports[metadata[NAME]]; + let name; + if (resolvedModuleData) { + // The potentially aliased name. + name = resolvedModuleData.name; + } else { + // If we don't have this specific name, we might have the full module. + resolvedModuleData = moduleExports['*']; + if (!resolvedModuleData) { + throw new Error( + 'Could not find the module "' + + metadata[ID] + + '" in the React SSR Manifest. ' + + 'This is probably a bug in the React Server Components bundler.', + ); + } + name = metadata[NAME]; + } + return { + specifier: resolvedModuleData.specifier, + name: name, + async: isAsyncClientReference(metadata), + }; +} + +export function resolveServerReference( + bundlerConfig: ServerManifest, + id: ServerReferenceId, +): ClientReference { + const idx = id.lastIndexOf('#'); + const specifier = id.slice(0, idx); + const name = id.slice(idx + 1); + return {specifier, name}; +} + +const asyncModuleCache: Map> = new Map(); + +export function preloadModule( + metadata: ClientReference, +): null | Thenable { + const existingPromise = asyncModuleCache.get(metadata.specifier); + if (existingPromise) { + if (existingPromise.status === 'fulfilled') { + return null; + } + return existingPromise; + } else { + // $FlowFixMe[unsupported-syntax] + let modulePromise: Promise = import(metadata.specifier); + if (metadata.async) { + // If the module is async, it must have been a CJS module. + // CJS modules are accessed through the default export in + // Node.js so we have to get the default export to get the + // full module exports. + modulePromise = modulePromise.then(function (value) { + return (value: any).default; + }); + } + modulePromise.then( + value => { + const fulfilledThenable: FulfilledThenable = + (modulePromise: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = value; + }, + reason => { + const rejectedThenable: RejectedThenable = (modulePromise: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = reason; + }, + ); + asyncModuleCache.set(metadata.specifier, modulePromise); + return modulePromise; + } +} + +export function requireModule(metadata: ClientReference): T { + let moduleExports; + // We assume that preloadModule has been called before, which + // should have added something to the module cache. + const promise: any = asyncModuleCache.get(metadata.specifier); + if (promise.status === 'fulfilled') { + moduleExports = promise.value; + } else { + throw promise.reason; + } + 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 === '') { + // 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.default; + } + return moduleExports[metadata.name]; +} diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js new file mode 100644 index 000000000000..894753659cfe --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js @@ -0,0 +1,231 @@ +/** + * 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 type { + Thenable, + FulfilledThenable, + RejectedThenable, +} from 'shared/ReactTypes'; + +import type { + ClientReferenceMetadata as SharedClientReferenceMetadata, + ClientReferenceManifestEntry as SharedClientReferenceManifestEntry, +} from './shared/ReactFlightClientReference'; +import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig'; + +import { + ID, + CHUNKS, + NAME, + isAsyncClientReference, +} from './shared/ReactFlightClientReference'; + +import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig'; + +import {loadChunk} from 'react-client/src/ReactFlightClientConfig'; + +export type SSRManifest = null | { + [clientId: string]: { + [clientExportName: string]: ClientReferenceManifestEntry, + }, +}; + +export type ServerManifest = { + [id: string]: SharedClientReferenceManifestEntry, +}; + +export type ServerReferenceId = string; + +export opaque type ClientReferenceManifestEntry = SharedClientReferenceManifestEntry; +export opaque type ClientReferenceMetadata = SharedClientReferenceMetadata; + +// 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 TurbopackDestination... 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, + metadata: ClientReferenceMetadata, +): ClientReference { + if (bundlerConfig) { + const moduleExports = bundlerConfig[metadata[ID]]; + let resolvedModuleData = moduleExports[metadata[NAME]]; + let name; + if (resolvedModuleData) { + // The potentially aliased name. + name = resolvedModuleData.name; + } else { + // If we don't have this specific name, we might have the full module. + resolvedModuleData = moduleExports['*']; + if (!resolvedModuleData) { + throw new Error( + 'Could not find the module "' + + metadata[ID] + + '" in the React SSR Manifest. ' + + 'This is probably a bug in the React Server Components bundler.', + ); + } + name = metadata[NAME]; + } + if (isAsyncClientReference(metadata)) { + return [ + resolvedModuleData.id, + resolvedModuleData.chunks, + name, + 1 /* async */, + ]; + } else { + return [resolvedModuleData.id, resolvedModuleData.chunks, name]; + } + } + return metadata; +} + +export function resolveServerReference( + bundlerConfig: ServerManifest, + id: ServerReferenceId, +): ClientReference { + let name = ''; + let resolvedModuleData = bundlerConfig[id]; + if (resolvedModuleData) { + // The potentially aliased name. + name = resolvedModuleData.name; + } else { + // We didn't find this specific export name but we might have the * export + // which contains this name as well. + // TODO: It's unfortunate that we now have to parse this string. We should + // probably go back to encoding path and name separately on the client reference. + const idx = id.lastIndexOf('#'); + if (idx !== -1) { + name = id.slice(idx + 1); + resolvedModuleData = bundlerConfig[id.slice(0, idx)]; + } + if (!resolvedModuleData) { + throw new Error( + 'Could not find the module "' + + id + + '" in the React Server Manifest. ' + + 'This is probably a bug in the React Server Components bundler.', + ); + } + } + // TODO: This needs to return async: true if it's an async module. + return [resolvedModuleData.id, resolvedModuleData.chunks, name]; +} + +// The chunk cache contains all the chunks we've preloaded so far. +// If they're still pending they're a thenable. This map also exists +// in Turbopack but unfortunately it's not exposed so we have to +// replicate it in user space. null means that it has already loaded. +const chunkCache: Map> = new Map(); + +function requireAsyncModule(id: string): null | Thenable { + // We've already loaded all the chunks. We can require the module. + const promise = __turbopack_require__(id); + if (typeof promise.then !== 'function') { + // This wasn't a promise after all. + return null; + } else if (promise.status === 'fulfilled') { + // This module was already resolved earlier. + return null; + } else { + // Instrument the Promise to stash the result. + promise.then( + value => { + const fulfilledThenable: FulfilledThenable = (promise: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = value; + }, + reason => { + const rejectedThenable: RejectedThenable = (promise: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = reason; + }, + ); + return promise; + } +} + +function ignoreReject() { + // We rely on rejected promises to be handled by another listener. +} +// Start preloading the modules since we might need them soon. +// This function doesn't suspend. +export function preloadModule( + metadata: ClientReference, +): null | Thenable { + const chunks = metadata[CHUNKS]; + const promises = []; + for (let i = 0; i < chunks.length; i++) { + const chunkFilename = chunks[i]; + const entry = chunkCache.get(chunkFilename); + if (entry === undefined) { + const thenable = loadChunk(chunkFilename); + promises.push(thenable); + // $FlowFixMe[method-unbinding] + const resolve = chunkCache.set.bind(chunkCache, chunkFilename, null); + thenable.then(resolve, ignoreReject); + chunkCache.set(chunkFilename, thenable); + } else if (entry !== null) { + promises.push(entry); + } + } + if (isAsyncClientReference(metadata)) { + if (promises.length === 0) { + return requireAsyncModule(metadata[ID]); + } else { + return Promise.all(promises).then(() => { + return requireAsyncModule(metadata[ID]); + }); + } + } else if (promises.length > 0) { + return Promise.all(promises); + } else { + return null; + } +} + +// Actually require the module or suspend if it's not yet ready. +// Increase priority if necessary. +export function requireModule(metadata: ClientReference): T { + let moduleExports = __turbopack_require__(metadata[ID]); + if (isAsyncClientReference(metadata)) { + if (typeof moduleExports.then !== 'function') { + // This wasn't a promise after all. + } else if (moduleExports.status === 'fulfilled') { + // This Promise should've been instrumented by preloadModule. + moduleExports = moduleExports.value; + } else { + throw moduleExports.reason; + } + } + 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] === '') { + // 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]]; +} diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser.js new file mode 100644 index 000000000000..c418f51fa80e --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser.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(filename: string): Promise { + return __turbopack_load__(filename); +} diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js new file mode 100644 index 000000000000..c418f51fa80e --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.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(filename: string): Promise { + return __turbopack_load__(filename); +} diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser.js new file mode 100644 index 000000000000..61d5f5cdca5e --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser.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-turbopack/src/ReactFlightClientConfigTargetTurbopackServer.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer.js new file mode 100644 index 000000000000..b9490dac4e09 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer.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 {preinitScriptForSSR} from 'react-client/src/ReactFlightClientConfig'; + +export type ModuleLoading = null | { + prefix: string, + crossOrigin?: 'use-credentials' | '', +}; + +export function prepareDestinationWithChunks( + moduleLoading: ModuleLoading, + // Chunks are single-indexed filenames + chunks: Array, +) { + if (moduleLoading !== null) { + for (let i = 0; i < chunks.length; i++) { + preinitScriptForSSR( + moduleLoading.prefix + chunks[i], + moduleLoading.crossOrigin, + ); + } + } +} diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js new file mode 100644 index 000000000000..d3c75495ec5a --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js @@ -0,0 +1,110 @@ +/** + * 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 type {Thenable} from 'shared/ReactTypes.js'; + +import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient'; + +import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient'; + +import { + createResponse, + getRoot, + reportGlobalError, + processBinaryChunk, + close, +} from 'react-client/src/ReactFlightClient'; + +import { + processReply, + createServerReference, +} from 'react-client/src/ReactFlightReplyClient'; + +type CallServerCallback = (string, args: A) => Promise; + +export type Options = { + callServer?: CallServerCallback, +}; + +function createResponseFromOptions(options: void | Options) { + return createResponse( + null, + null, + options && options.callServer ? options.callServer : undefined, + ); +} + +function startReadingFromStream( + response: FlightResponse, + stream: ReadableStream, +): void { + const reader = stream.getReader(); + function progress({ + done, + value, + }: { + done: boolean, + value: ?any, + ... + }): void | Promise { + if (done) { + close(response); + return; + } + const buffer: Uint8Array = (value: any); + processBinaryChunk(response, buffer); + return reader.read().then(progress).catch(error); + } + function error(e: any) { + reportGlobalError(response, e); + } + reader.read().then(progress).catch(error); +} + +function createFromReadableStream( + stream: ReadableStream, + options?: Options, +): Thenable { + const response: FlightResponse = createResponseFromOptions(options); + startReadingFromStream(response, stream); + return getRoot(response); +} + +function createFromFetch( + promiseForResponse: Promise, + options?: Options, +): Thenable { + const response: FlightResponse = createResponseFromOptions(options); + promiseForResponse.then( + function (r) { + startReadingFromStream(response, (r.body: any)); + }, + function (e) { + reportGlobalError(response, e); + }, + ); + return getRoot(response); +} + +function encodeReply( + value: ReactServerValue, +): Promise< + string | URLSearchParams | FormData, +> /* We don't use URLSearchParams yet but maybe */ { + return new Promise((resolve, reject) => { + processReply(value, '', resolve, reject); + }); +} + +export { + createFromFetch, + createFromReadableStream, + encodeReply, + createServerReference, +}; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js new file mode 100644 index 000000000000..ce687613e70a --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js @@ -0,0 +1,109 @@ +/** + * 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 type {Thenable} from 'shared/ReactTypes.js'; + +import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient'; + +import type { + SSRManifest, + ModuleLoading, +} from 'react-client/src/ReactFlightClientConfig'; + +import { + createResponse, + getRoot, + reportGlobalError, + processBinaryChunk, + close, +} from 'react-client/src/ReactFlightClient'; + +import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient'; + +function noServerCall() { + throw new Error( + 'Server Functions cannot be called during initial render. ' + + 'This would create a fetch waterfall. Try to use a Server Component ' + + 'to pass data to Client Components instead.', + ); +} + +export function createServerReference, T>( + id: any, + callServer: any, +): (...A) => Promise { + return createServerReferenceImpl(id, noServerCall); +} + +export type Options = { + moduleMap?: $NonMaybeType, + moduleLoading?: $NonMaybeType, +}; + +function createResponseFromOptions(options: void | Options) { + return createResponse( + options && options.moduleMap ? options.moduleMap : null, + options && options.moduleLoading ? options.moduleLoading : null, + noServerCall, + ); +} + +function startReadingFromStream( + response: FlightResponse, + stream: ReadableStream, +): void { + const reader = stream.getReader(); + function progress({ + done, + value, + }: { + done: boolean, + value: ?any, + ... + }): void | Promise { + if (done) { + close(response); + return; + } + const buffer: Uint8Array = (value: any); + processBinaryChunk(response, buffer); + return reader.read().then(progress).catch(error); + } + function error(e: any) { + reportGlobalError(response, e); + } + reader.read().then(progress).catch(error); +} + +function createFromReadableStream( + stream: ReadableStream, + options?: Options, +): Thenable { + const response: FlightResponse = createResponseFromOptions(options); + startReadingFromStream(response, stream); + return getRoot(response); +} + +function createFromFetch( + promiseForResponse: Promise, + options?: Options, +): Thenable { + const response: FlightResponse = createResponseFromOptions(options); + promiseForResponse.then( + function (r) { + startReadingFromStream(response, (r.body: any)); + }, + function (e) { + reportGlobalError(response, e); + }, + ); + return getRoot(response); +} + +export {createFromFetch, createFromReadableStream}; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js new file mode 100644 index 000000000000..83031f1cfef5 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js @@ -0,0 +1,72 @@ +/** + * 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 type {Thenable} from 'shared/ReactTypes.js'; + +import type {Response} from 'react-client/src/ReactFlightClient'; + +import type { + SSRManifest, + ModuleLoading, +} from 'react-client/src/ReactFlightClientConfig'; + +import type {Readable} from 'stream'; + +import { + createResponse, + getRoot, + reportGlobalError, + processBinaryChunk, + close, +} from 'react-client/src/ReactFlightClient'; + +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. ' + + 'This would create a fetch waterfall. Try to use a Server Component ' + + 'to pass data to Client Components instead.', + ); +} + +export function createServerReference, T>( + id: any, + callServer: any, +): (...A) => Promise { + return createServerReferenceImpl(id, noServerCall); +} + +function createFromNodeStream( + stream: Readable, + moduleMap: $NonMaybeType, + options?: Options, +): Thenable { + const moduleLoading = + options && options.moduleLoading ? options.moduleLoading : null; + const response: Response = createResponse( + moduleMap, + moduleLoading, + noServerCall, + ); + stream.on('data', chunk => { + processBinaryChunk(response, chunk); + }); + stream.on('error', error => { + reportGlobalError(response, error); + }); + stream.on('end', () => close(response)); + return getRoot(response); +} + +export {createFromNodeStream}; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js new file mode 100644 index 000000000000..412e697af3ab --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js @@ -0,0 +1,100 @@ +/** + * 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 type {ReactClientValue} from 'react-server/src/ReactFlightServer'; +import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes'; +import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler'; +import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; + +import { + createRequest, + startWork, + startFlowing, + abort, +} from 'react-server/src/ReactFlightServer'; + +import { + createResponse, + close, + getRoot, +} from 'react-server/src/ReactFlightReplyServer'; + +import {decodeAction} from 'react-server/src/ReactFlightActionServer'; + +export { + registerServerReference, + registerClientReference, + createClientModuleProxy, +} from './ReactFlightTurbopackReferences'; + +type Options = { + identifierPrefix?: string, + signal?: AbortSignal, + context?: Array<[string, ServerContextJSONValue]>, + onError?: (error: mixed) => void, + onPostpone?: (reason: string) => void, +}; + +function renderToReadableStream( + model: ReactClientValue, + turbopackMap: ClientManifest, + options?: Options, +): ReadableStream { + const request = createRequest( + model, + turbopackMap, + options ? options.onError : undefined, + options ? options.context : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort(request, (signal: any).reason); + } else { + const listener = () => { + abort(request, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + startWork(request); + }, + pull: (controller): ?Promise => { + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => {}, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + return stream; +} + +function decodeReply( + body: string | FormData, + turbopackMap: ServerManifest, +): Thenable { + if (typeof body === 'string') { + const form = new FormData(); + form.append('0', body); + body = form; + } + const response = createResponse(turbopackMap, '', body); + close(response); + return getRoot(response); +} + +export {renderToReadableStream, decodeReply, decodeAction}; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js new file mode 100644 index 000000000000..412e697af3ab --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js @@ -0,0 +1,100 @@ +/** + * 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 type {ReactClientValue} from 'react-server/src/ReactFlightServer'; +import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes'; +import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler'; +import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; + +import { + createRequest, + startWork, + startFlowing, + abort, +} from 'react-server/src/ReactFlightServer'; + +import { + createResponse, + close, + getRoot, +} from 'react-server/src/ReactFlightReplyServer'; + +import {decodeAction} from 'react-server/src/ReactFlightActionServer'; + +export { + registerServerReference, + registerClientReference, + createClientModuleProxy, +} from './ReactFlightTurbopackReferences'; + +type Options = { + identifierPrefix?: string, + signal?: AbortSignal, + context?: Array<[string, ServerContextJSONValue]>, + onError?: (error: mixed) => void, + onPostpone?: (reason: string) => void, +}; + +function renderToReadableStream( + model: ReactClientValue, + turbopackMap: ClientManifest, + options?: Options, +): ReadableStream { + const request = createRequest( + model, + turbopackMap, + options ? options.onError : undefined, + options ? options.context : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort(request, (signal: any).reason); + } else { + const listener = () => { + abort(request, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + startWork(request); + }, + pull: (controller): ?Promise => { + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => {}, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + return stream; +} + +function decodeReply( + body: string | FormData, + turbopackMap: ServerManifest, +): Thenable { + if (typeof body === 'string') { + const form = new FormData(); + form.append('0', body); + body = form; + } + const response = createResponse(turbopackMap, '', body); + close(response); + return getRoot(response); +} + +export {renderToReadableStream, decodeReply, decodeAction}; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMServerNode.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerNode.js new file mode 100644 index 000000000000..4cfe7a4044d6 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerNode.js @@ -0,0 +1,170 @@ +/** + * 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 type { + Request, + ReactClientValue, +} from 'react-server/src/ReactFlightServer'; +import type {Destination} from 'react-server/src/ReactServerStreamConfigNode'; +import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler'; +import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; +import type {Busboy} from 'busboy'; +import type {Writable} from 'stream'; +import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes'; + +import { + createRequest, + startWork, + startFlowing, + abort, +} from 'react-server/src/ReactFlightServer'; + +import { + createResponse, + reportGlobalError, + close, + resolveField, + resolveFileInfo, + resolveFileChunk, + resolveFileComplete, + getRoot, +} from 'react-server/src/ReactFlightReplyServer'; + +import {decodeAction} from 'react-server/src/ReactFlightActionServer'; + +export { + registerServerReference, + registerClientReference, + createClientModuleProxy, +} from './ReactFlightTurbopackReferences'; + +function createDrainHandler(destination: Destination, request: Request) { + return () => startFlowing(request, destination); +} + +type Options = { + onError?: (error: mixed) => void, + onPostpone?: (reason: string) => void, + context?: Array<[string, ServerContextJSONValue]>, + identifierPrefix?: string, +}; + +type PipeableStream = { + abort(reason: mixed): void, + pipe(destination: T): T, +}; + +function renderToPipeableStream( + model: ReactClientValue, + turbopackMap: ClientManifest, + options?: Options, +): PipeableStream { + const request = createRequest( + model, + turbopackMap, + options ? options.onError : undefined, + options ? options.context : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + ); + let hasStartedFlowing = false; + startWork(request); + return { + pipe(destination: T): T { + if (hasStartedFlowing) { + throw new Error( + 'React currently only supports piping to one writable stream.', + ); + } + hasStartedFlowing = true; + startFlowing(request, destination); + destination.on('drain', createDrainHandler(destination, request)); + return destination; + }, + abort(reason: mixed) { + abort(request, reason); + }, + }; +} + +function decodeReplyFromBusboy( + busboyStream: Busboy, + turbopackMap: ServerManifest, +): Thenable { + const response = createResponse(turbopackMap, ''); + let pendingFiles = 0; + const queuedFields: Array = []; + busboyStream.on('field', (name, value) => { + if (pendingFiles > 0) { + // Because the 'end' event fires two microtasks after the next 'field' + // we would resolve files and fields out of order. To handle this properly + // we queue any fields we receive until the previous file is done. + queuedFields.push(name, value); + } else { + resolveField(response, name, value); + } + }); + busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => { + if (encoding.toLowerCase() === 'base64') { + throw new Error( + "React doesn't accept base64 encoded file uploads because we don't expect " + + "form data passed from a browser to ever encode data that way. If that's " + + 'the wrong assumption, we can easily fix it.', + ); + } + pendingFiles++; + const file = resolveFileInfo(response, name, filename, mimeType); + value.on('data', chunk => { + resolveFileChunk(response, file, chunk); + }); + value.on('end', () => { + resolveFileComplete(response, name, file); + pendingFiles--; + if (pendingFiles === 0) { + // Release any queued fields + for (let i = 0; i < queuedFields.length; i += 2) { + resolveField(response, queuedFields[i], queuedFields[i + 1]); + } + queuedFields.length = 0; + } + }); + }); + busboyStream.on('finish', () => { + close(response); + }); + busboyStream.on('error', err => { + reportGlobalError( + response, + // $FlowFixMe[incompatible-call] types Error and mixed are incompatible + err, + ); + }); + return getRoot(response); +} + +function decodeReply( + body: string | FormData, + turbopackMap: ServerManifest, +): Thenable { + if (typeof body === 'string') { + const form = new FormData(); + form.append('0', body); + body = form; + } + const response = createResponse(turbopackMap, '', body); + close(response); + return getRoot(response); +} + +export { + renderToPipeableStream, + decodeReplyFromBusboy, + decodeReply, + decodeAction, +}; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler.js b/packages/react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler.js new file mode 100644 index 000000000000..b5bed25377b8 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler.js @@ -0,0 +1,93 @@ +/** + * 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 type {ReactClientValue} from 'react-server/src/ReactFlightServer'; +import type { + ClientReferenceMetadata as SharedClientReferenceMetadata, + ClientReferenceManifestEntry as SharedClientReferenceManifestEntry, +} from './shared/ReactFlightClientReference'; + +import type { + ClientReference, + ServerReference, +} from './ReactFlightTurbopackReferences'; + +export type {ClientReference, ServerReference}; + +export type ClientManifest = { + [id: string]: ClientReferenceManifestEntry, +}; + +export type ServerReferenceId = string; + +export type ClientReferenceMetadata = SharedClientReferenceMetadata; +export opaque type ClientReferenceManifestEntry = SharedClientReferenceManifestEntry; + +export type ClientReferenceKey = string; + +export { + isClientReference, + isServerReference, +} from './ReactFlightTurbopackReferences'; + +export function getClientReferenceKey( + reference: ClientReference, +): ClientReferenceKey { + return reference.$$async ? reference.$$id + '#async' : reference.$$id; +} + +export function resolveClientReferenceMetadata( + config: ClientManifest, + clientReference: ClientReference, +): ClientReferenceMetadata { + const modulePath = clientReference.$$id; + let name = ''; + let resolvedModuleData = config[modulePath]; + if (resolvedModuleData) { + // The potentially aliased name. + name = resolvedModuleData.name; + } else { + // We didn't find this specific export name but we might have the * export + // which contains this name as well. + // TODO: It's unfortunate that we now have to parse this string. We should + // probably go back to encoding path and name separately on the client reference. + const idx = modulePath.lastIndexOf('#'); + if (idx !== -1) { + name = modulePath.slice(idx + 1); + resolvedModuleData = config[modulePath.slice(0, idx)]; + } + if (!resolvedModuleData) { + throw new Error( + 'Could not find the module "' + + modulePath + + '" in the React Client Manifest. ' + + 'This is probably a bug in the React Server Components bundler.', + ); + } + } + if (clientReference.$$async === true) { + return [resolvedModuleData.id, resolvedModuleData.chunks, name, 1]; + } else { + return [resolvedModuleData.id, resolvedModuleData.chunks, name]; + } +} + +export function getServerReferenceId( + config: ClientManifest, + serverReference: ServerReference, +): ServerReferenceId { + return serverReference.$$id; +} + +export function getServerReferenceBoundArguments( + config: ClientManifest, + serverReference: ServerReference, +): null | Array { + return serverReference.$$bound; +} diff --git a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js new file mode 100644 index 000000000000..066825857f44 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js @@ -0,0 +1,483 @@ +/** + * 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 * as acorn from 'acorn-loose'; + +type ResolveContext = { + conditions: Array, + parentURL: string | void, +}; + +type ResolveFunction = ( + string, + ResolveContext, + ResolveFunction, +) => {url: string} | Promise<{url: string}>; + +type GetSourceContext = { + format: string, +}; + +type GetSourceFunction = ( + string, + GetSourceContext, + GetSourceFunction, +) => Promise<{source: Source}>; + +type TransformSourceContext = { + format: string, + url: string, +}; + +type TransformSourceFunction = ( + Source, + TransformSourceContext, + TransformSourceFunction, +) => Promise<{source: Source}>; + +type LoadContext = { + conditions: Array, + format: string | null | void, + importAssertions: Object, +}; + +type LoadFunction = ( + string, + LoadContext, + LoadFunction, +) => Promise<{format: string, shortCircuit?: boolean, source: Source}>; + +type Source = string | ArrayBuffer | Uint8Array; + +let warnedAboutConditionsFlag = false; + +let stashedGetSource: null | GetSourceFunction = null; +let stashedResolve: null | ResolveFunction = null; + +export async function resolve( + specifier: string, + context: ResolveContext, + defaultResolve: ResolveFunction, +): Promise<{url: string}> { + // We stash this in case we end up needing to resolve export * statements later. + stashedResolve = defaultResolve; + + if (!context.conditions.includes('react-server')) { + context = { + ...context, + conditions: [...context.conditions, 'react-server'], + }; + if (!warnedAboutConditionsFlag) { + warnedAboutConditionsFlag = true; + // eslint-disable-next-line react-internal/no-production-logging + console.warn( + 'You did not run Node.js with the `--conditions react-server` flag. ' + + 'Any "react-server" override will only work with ESM imports.', + ); + } + } + return await defaultResolve(specifier, context, defaultResolve); +} + +export async function getSource( + url: string, + context: GetSourceContext, + defaultGetSource: GetSourceFunction, +): Promise<{source: Source}> { + // We stash this in case we end up needing to resolve export * statements later. + stashedGetSource = defaultGetSource; + return defaultGetSource(url, context, defaultGetSource); +} + +function addLocalExportedNames(names: Map, node: any) { + switch (node.type) { + case 'Identifier': + names.set(node.name, node.name); + return; + case 'ObjectPattern': + for (let i = 0; i < node.properties.length; i++) + addLocalExportedNames(names, node.properties[i]); + return; + case 'ArrayPattern': + for (let i = 0; i < node.elements.length; i++) { + const element = node.elements[i]; + if (element) addLocalExportedNames(names, element); + } + return; + case 'Property': + addLocalExportedNames(names, node.value); + return; + case 'AssignmentPattern': + addLocalExportedNames(names, node.left); + return; + case 'RestElement': + addLocalExportedNames(names, node.argument); + return; + case 'ParenthesizedExpression': + addLocalExportedNames(names, node.expression); + return; + } +} + +function transformServerModule( + source: string, + body: any, + url: string, + loader: LoadFunction, +): string { + // If the same local name is exported more than once, we only need one of the names. + const localNames: Map = new Map(); + const localTypes: Map = new Map(); + + for (let i = 0; i < body.length; i++) { + const node = body[i]; + switch (node.type) { + case 'ExportAllDeclaration': + // If export * is used, the other file needs to explicitly opt into "use server" too. + break; + case 'ExportDefaultDeclaration': + if (node.declaration.type === 'Identifier') { + localNames.set(node.declaration.name, 'default'); + } else if (node.declaration.type === 'FunctionDeclaration') { + if (node.declaration.id) { + localNames.set(node.declaration.id.name, 'default'); + localTypes.set(node.declaration.id.name, 'function'); + } else { + // TODO: This needs to be rewritten inline because it doesn't have a local name. + } + } + continue; + case 'ExportNamedDeclaration': + if (node.declaration) { + if (node.declaration.type === 'VariableDeclaration') { + const declarations = node.declaration.declarations; + for (let j = 0; j < declarations.length; j++) { + addLocalExportedNames(localNames, declarations[j].id); + } + } else { + const name = node.declaration.id.name; + localNames.set(name, name); + if (node.declaration.type === 'FunctionDeclaration') { + localTypes.set(name, 'function'); + } + } + } + if (node.specifiers) { + const specifiers = node.specifiers; + for (let j = 0; j < specifiers.length; j++) { + const specifier = specifiers[j]; + localNames.set(specifier.local.name, specifier.exported.name); + } + } + continue; + } + } + if (localNames.size === 0) { + return source; + } + let newSrc = source + '\n\n;'; + newSrc += + 'import {registerServerReference} from "react-server-dom-turbopack/server";\n'; + localNames.forEach(function (exported, local) { + if (localTypes.get(local) !== 'function') { + // We first check if the export is a function and if so annotate it. + newSrc += 'if (typeof ' + local + ' === "function") '; + } + newSrc += 'registerServerReference(' + local + ','; + newSrc += JSON.stringify(url) + ','; + newSrc += JSON.stringify(exported) + ');\n'; + }); + return newSrc; +} + +function addExportNames(names: Array, node: any) { + switch (node.type) { + case 'Identifier': + names.push(node.name); + return; + case 'ObjectPattern': + for (let i = 0; i < node.properties.length; i++) + addExportNames(names, node.properties[i]); + return; + case 'ArrayPattern': + for (let i = 0; i < node.elements.length; i++) { + const element = node.elements[i]; + if (element) addExportNames(names, element); + } + return; + case 'Property': + addExportNames(names, node.value); + return; + case 'AssignmentPattern': + addExportNames(names, node.left); + return; + case 'RestElement': + addExportNames(names, node.argument); + return; + case 'ParenthesizedExpression': + addExportNames(names, node.expression); + return; + } +} + +function resolveClientImport( + specifier: string, + parentURL: string, +): {url: string} | Promise<{url: string}> { + // Resolve an import specifier as if it was loaded by the client. This doesn't use + // the overrides that this loader does but instead reverts to the default. + // This resolution algorithm will not necessarily have the same configuration + // as the actual client loader. It should mostly work and if it doesn't you can + // always convert to explicit exported names instead. + const conditions = ['node', 'import']; + if (stashedResolve === null) { + throw new Error( + 'Expected resolve to have been called before transformSource', + ); + } + return stashedResolve(specifier, {conditions, parentURL}, stashedResolve); +} + +async function parseExportNamesInto( + body: any, + names: Array, + parentURL: string, + loader: LoadFunction, +): Promise { + for (let i = 0; i < body.length; i++) { + const node = body[i]; + switch (node.type) { + case 'ExportAllDeclaration': + if (node.exported) { + addExportNames(names, node.exported); + continue; + } else { + const {url} = await resolveClientImport(node.source.value, parentURL); + const {source} = await loader( + url, + {format: 'module', conditions: [], importAssertions: {}}, + loader, + ); + if (typeof source !== 'string') { + throw new Error('Expected the transformed source to be a string.'); + } + let childBody; + try { + childBody = acorn.parse(source, { + ecmaVersion: '2024', + sourceType: 'module', + }).body; + } catch (x) { + // eslint-disable-next-line react-internal/no-production-logging + console.error('Error parsing %s %s', url, x.message); + continue; + } + await parseExportNamesInto(childBody, names, url, loader); + continue; + } + case 'ExportDefaultDeclaration': + names.push('default'); + continue; + case 'ExportNamedDeclaration': + if (node.declaration) { + if (node.declaration.type === 'VariableDeclaration') { + const declarations = node.declaration.declarations; + for (let j = 0; j < declarations.length; j++) { + addExportNames(names, declarations[j].id); + } + } else { + addExportNames(names, node.declaration.id); + } + } + if (node.specifiers) { + const specifiers = node.specifiers; + for (let j = 0; j < specifiers.length; j++) { + addExportNames(names, specifiers[j].exported); + } + } + continue; + } + } +} + +async function transformClientModule( + body: any, + url: string, + loader: LoadFunction, +): Promise { + const names: Array = []; + + await parseExportNamesInto(body, names, url, loader); + + if (names.length === 0) { + return ''; + } + + let newSrc = + 'import {registerClientReference} from "react-server-dom-turbopack/server";\n'; + for (let i = 0; i < names.length; i++) { + const name = names[i]; + if (name === 'default') { + newSrc += 'export default '; + newSrc += 'registerClientReference(function() {'; + newSrc += + 'throw new Error(' + + JSON.stringify( + `Attempted to call the default export of ${url} from the server` + + `but it's on the client. It's not possible to invoke a client function from ` + + `the server, it can only be rendered as a Component or passed to props of a` + + `Client Component.`, + ) + + ');'; + } else { + newSrc += 'export const ' + name + ' = '; + newSrc += 'registerClientReference(function() {'; + newSrc += + 'throw new Error(' + + JSON.stringify( + `Attempted to call ${name}() from the server but ${name} is on the client. ` + + `It's not possible to invoke a client function from the server, it can ` + + `only be rendered as a Component or passed to props of a Client Component.`, + ) + + ');'; + } + newSrc += '},'; + newSrc += JSON.stringify(url) + ','; + newSrc += JSON.stringify(name) + ');\n'; + } + return newSrc; +} + +async function loadClientImport( + url: string, + defaultTransformSource: TransformSourceFunction, +): Promise<{format: string, shortCircuit?: boolean, source: Source}> { + if (stashedGetSource === null) { + throw new Error( + 'Expected getSource to have been called before transformSource', + ); + } + // TODO: Validate that this is another module by calling getFormat. + const {source} = await stashedGetSource( + url, + {format: 'module'}, + stashedGetSource, + ); + const result = await defaultTransformSource( + source, + {format: 'module', url}, + defaultTransformSource, + ); + return {format: 'module', source: result.source}; +} + +async function transformModuleIfNeeded( + source: string, + url: string, + loader: LoadFunction, +): Promise { + // Do a quick check for the exact string. If it doesn't exist, don't + // bother parsing. + if ( + source.indexOf('use client') === -1 && + source.indexOf('use server') === -1 + ) { + return source; + } + + let body; + try { + body = acorn.parse(source, { + ecmaVersion: '2024', + sourceType: 'module', + }).body; + } catch (x) { + // eslint-disable-next-line react-internal/no-production-logging + console.error('Error parsing %s %s', url, x.message); + return source; + } + + let useClient = false; + let useServer = false; + for (let i = 0; i < body.length; i++) { + const node = body[i]; + if (node.type !== 'ExpressionStatement' || !node.directive) { + break; + } + if (node.directive === 'use client') { + useClient = true; + } + if (node.directive === 'use server') { + useServer = true; + } + } + + if (!useClient && !useServer) { + return source; + } + + if (useClient && useServer) { + throw new Error( + 'Cannot have both "use client" and "use server" directives in the same file.', + ); + } + + if (useClient) { + return transformClientModule(body, url, loader); + } + + return transformServerModule(source, body, url, loader); +} + +export async function transformSource( + source: Source, + context: TransformSourceContext, + defaultTransformSource: TransformSourceFunction, +): Promise<{source: Source}> { + const transformed = await defaultTransformSource( + source, + context, + defaultTransformSource, + ); + if (context.format === 'module') { + const transformedSource = transformed.source; + if (typeof transformedSource !== 'string') { + throw new Error('Expected source to have been transformed to a string.'); + } + const newSrc = await transformModuleIfNeeded( + transformedSource, + context.url, + (url: string, ctx: LoadContext, defaultLoad: LoadFunction) => { + return loadClientImport(url, defaultTransformSource); + }, + ); + return {source: newSrc}; + } + return transformed; +} + +export async function load( + url: string, + context: LoadContext, + defaultLoad: LoadFunction, +): Promise<{format: string, shortCircuit?: boolean, source: Source}> { + const result = await defaultLoad(url, context, defaultLoad); + if (result.format === 'module') { + if (typeof result.source !== 'string') { + throw new Error('Expected source to have been loaded into a string.'); + } + const newSrc = await transformModuleIfNeeded( + result.source, + url, + defaultLoad, + ); + return {format: 'module', source: newSrc}; + } + return result; +} diff --git a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js new file mode 100644 index 000000000000..68c692530d6c --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js @@ -0,0 +1,110 @@ +/** + * 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 acorn = require('acorn-loose'); + +const url = require('url'); + +const Module = require('module'); + +module.exports = function register() { + const Server: any = require('react-server-dom-turbopack/server'); + const registerServerReference = Server.registerServerReference; + const createClientModuleProxy = Server.createClientModuleProxy; + + // $FlowFixMe[prop-missing] found when upgrading Flow + const originalCompile = Module.prototype._compile; + + // $FlowFixMe[prop-missing] found when upgrading Flow + Module.prototype._compile = function ( + this: any, + content: string, + filename: string, + ): void { + // Do a quick check for the exact string. If it doesn't exist, don't + // bother parsing. + if ( + content.indexOf('use client') === -1 && + content.indexOf('use server') === -1 + ) { + return originalCompile.apply(this, arguments); + } + + let body; + try { + body = acorn.parse(content, { + ecmaVersion: '2024', + sourceType: 'source', + }).body; + } catch (x) { + // eslint-disable-next-line react-internal/no-production-logging + console.error('Error parsing %s %s', url, x.message); + return originalCompile.apply(this, arguments); + } + + let useClient = false; + let useServer = false; + for (let i = 0; i < body.length; i++) { + const node = body[i]; + if (node.type !== 'ExpressionStatement' || !node.directive) { + break; + } + if (node.directive === 'use client') { + useClient = true; + } + if (node.directive === 'use server') { + useServer = true; + } + } + + if (!useClient && !useServer) { + return originalCompile.apply(this, arguments); + } + + if (useClient && useServer) { + throw new Error( + 'Cannot have both "use client" and "use server" directives in the same file.', + ); + } + + if (useClient) { + const moduleId: string = (url.pathToFileURL(filename).href: any); + this.exports = createClientModuleProxy(moduleId); + } + + if (useServer) { + originalCompile.apply(this, arguments); + + const moduleId: string = (url.pathToFileURL(filename).href: any); + + const exports = this.exports; + + // This module is imported server to server, but opts in to exposing functions by + // reference. If there are any functions in the export. + if (typeof exports === 'function') { + // The module exports a function directly, + registerServerReference( + (exports: any), + moduleId, + // Represents the whole Module object instead of a particular import. + null, + ); + } else { + const keys = Object.keys(exports); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = exports[keys[i]]; + if (typeof value === 'function') { + registerServerReference((value: any), moduleId, key); + } + } + } + } + }; +}; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js new file mode 100644 index 000000000000..9df0e43bd75e --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js @@ -0,0 +1,256 @@ +/** + * 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 type {ReactClientValue} from 'react-server/src/ReactFlightServer'; + +export type ServerReference = T & { + $$typeof: symbol, + $$id: string, + $$bound: null | Array, +}; + +// eslint-disable-next-line no-unused-vars +export type ClientReference = { + $$typeof: symbol, + $$id: string, + $$async: boolean, +}; + +const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference'); +const SERVER_REFERENCE_TAG = Symbol.for('react.server.reference'); + +export function isClientReference(reference: Object): boolean { + return reference.$$typeof === CLIENT_REFERENCE_TAG; +} + +export function isServerReference(reference: Object): boolean { + return reference.$$typeof === SERVER_REFERENCE_TAG; +} + +export function registerClientReference( + proxyImplementation: any, + id: string, + exportName: string, +): ClientReference { + return registerClientReferenceImpl( + proxyImplementation, + id + '#' + exportName, + false, + ); +} + +function registerClientReferenceImpl( + proxyImplementation: any, + id: string, + async: boolean, +): ClientReference { + return Object.defineProperties(proxyImplementation, { + $$typeof: {value: CLIENT_REFERENCE_TAG}, + $$id: {value: id}, + $$async: {value: async}, + }); +} + +// $FlowFixMe[method-unbinding] +const FunctionBind = Function.prototype.bind; +// $FlowFixMe[method-unbinding] +const ArraySlice = Array.prototype.slice; +function bind(this: ServerReference) { + // $FlowFixMe[unsupported-syntax] + const newFn = FunctionBind.apply(this, arguments); + if (this.$$typeof === SERVER_REFERENCE_TAG) { + const args = ArraySlice.call(arguments, 1); + newFn.$$typeof = SERVER_REFERENCE_TAG; + newFn.$$id = this.$$id; + newFn.$$bound = this.$$bound ? this.$$bound.concat(args) : args; + } + return newFn; +} + +export function registerServerReference( + reference: ServerReference, + id: string, + exportName: null | string, +): ServerReference { + return Object.defineProperties((reference: any), { + $$typeof: {value: SERVER_REFERENCE_TAG}, + $$id: {value: exportName === null ? id : id + '#' + exportName}, + $$bound: {value: null}, + bind: {value: bind}, + }); +} + +const PROMISE_PROTOTYPE = Promise.prototype; + +const deepProxyHandlers = { + get: function (target: Function, name: string, receiver: Proxy) { + switch (name) { + // These names are read by the Flight runtime if you end up using the exports object. + case '$$typeof': + // These names are a little too common. We should probably have a way to + // have the Flight runtime extract the inner target instead. + return target.$$typeof; + case '$$id': + return target.$$id; + case '$$async': + return target.$$async; + case 'name': + return target.name; + case 'displayName': + return undefined; + // We need to special case this because createElement reads it if we pass this + // reference. + case 'defaultProps': + return undefined; + // Avoid this attempting to be serialized. + case 'toJSON': + return undefined; + case Symbol.toPrimitive: + // $FlowFixMe[prop-missing] + return Object.prototype[Symbol.toPrimitive]; + case 'Provider': + throw new Error( + `Cannot render a Client Context Provider on the Server. ` + + `Instead, you can export a Client Component wrapper ` + + `that itself renders a Client Context Provider.`, + ); + } + // eslint-disable-next-line react-internal/safe-string-coercion + const expression = String(target.name) + '.' + String(name); + throw new Error( + `Cannot access ${expression} on the server. ` + + 'You cannot dot into a client module from a server component. ' + + 'You can only pass the imported name through.', + ); + }, + set: function () { + throw new Error('Cannot assign to a client module from a server module.'); + }, +}; + +const proxyHandlers = { + get: function ( + target: Function, + name: string, + receiver: Proxy, + ): $FlowFixMe { + switch (name) { + // These names are read by the Flight runtime if you end up using the exports object. + case '$$typeof': + return target.$$typeof; + case '$$id': + return target.$$id; + case '$$async': + return target.$$async; + case 'name': + return target.name; + // We need to special case this because createElement reads it if we pass this + // reference. + case 'defaultProps': + return undefined; + // Avoid this attempting to be serialized. + case 'toJSON': + return undefined; + case Symbol.toPrimitive: + // $FlowFixMe[prop-missing] + return Object.prototype[Symbol.toPrimitive]; + case '__esModule': + // Something is conditionally checking which export to use. We'll pretend to be + // an ESM compat module but then we'll check again on the client. + const moduleId = target.$$id; + target.default = registerClientReferenceImpl( + (function () { + throw new Error( + `Attempted to call the default export of ${moduleId} from the server ` + + `but it's on the client. It's not possible to invoke a client function from ` + + `the server, it can only be rendered as a Component or passed to props of a ` + + `Client Component.`, + ); + }: any), + target.$$id + '#', + target.$$async, + ); + return true; + case 'then': + if (target.then) { + // Use a cached value + return target.then; + } + if (!target.$$async) { + // If this module is expected to return a Promise (such as an AsyncModule) then + // we should resolve that with a client reference that unwraps the Promise on + // the client. + + const clientReference: ClientReference = + registerClientReferenceImpl(({}: any), target.$$id, true); + const proxy = new Proxy(clientReference, proxyHandlers); + + // Treat this as a resolved Promise for React's use() + target.status = 'fulfilled'; + target.value = proxy; + + const then = (target.then = registerClientReferenceImpl( + (function then(resolve, reject: any) { + // Expose to React. + return Promise.resolve(resolve(proxy)); + }: any), + // If this is not used as a Promise but is treated as a reference to a `.then` + // export then we should treat it as a reference to that name. + target.$$id + '#then', + false, + )); + return then; + } else { + // Since typeof .then === 'function' is a feature test we'd continue recursing + // indefinitely if we return a function. Instead, we return an object reference + // if we check further. + return undefined; + } + } + let cachedReference = target[name]; + if (!cachedReference) { + const reference: ClientReference = registerClientReferenceImpl( + (function () { + throw new Error( + // eslint-disable-next-line react-internal/safe-string-coercion + `Attempted to call ${String(name)}() from the server but ${String( + name, + )} is on the client. ` + + `It's not possible to invoke a client function from the server, it can ` + + `only be rendered as a Component or passed to props of a Client Component.`, + ); + }: any), + target.$$id + '#' + name, + target.$$async, + ); + Object.defineProperty((reference: any), 'name', {value: name}); + cachedReference = target[name] = new Proxy(reference, deepProxyHandlers); + } + return cachedReference; + }, + getPrototypeOf(target: Function): Object { + // Pretend to be a Promise in case anyone asks. + return PROMISE_PROTOTYPE; + }, + set: function (): empty { + throw new Error('Cannot assign to a client module from a server module.'); + }, +}; + +export function createClientModuleProxy( + moduleId: string, +): ClientReference { + const clientReference: ClientReference = registerClientReferenceImpl( + ({}: any), + // Represents the whole Module object instead of a particular import. + moduleId, + false, + ); + return new Proxy(clientReference, proxyHandlers); +} diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js new file mode 100644 index 000000000000..932fd94b46e7 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js @@ -0,0 +1,1590 @@ +/** + * 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. + * + * @emails react-core + */ + +'use strict'; + +// Polyfills for test environment +global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; +global.TextEncoder = require('util').TextEncoder; +global.TextDecoder = require('util').TextDecoder; + +// Don't wait before processing work on the server. +// TODO: we can replace this with FlightServer.act(). +global.setImmediate = cb => cb(); + +let act; +let use; +let clientExports; +let clientModuleError; +let webpackMap; +let Stream; +let FlightReact; +let React; +let FlightReactDOM; +let ReactDOMClient; +let ReactServerDOMServer; +let ReactServerDOMClient; +let ReactDOMFizzServer; +let Suspense; +let ErrorBoundary; +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; + + // Simulate the condition resolution + jest.mock('react-server-dom-webpack/server', () => + require('react-server-dom-webpack/server.node.unbundled'), + ); + + const WebpackMock = require('./utils/WebpackMock'); + clientExports = WebpackMock.clientExports; + clientModuleError = WebpackMock.clientModuleError; + webpackMap = WebpackMock.webpackMap; + + ReactServerDOMServer = require('react-server-dom-webpack/server.node.unbundled'); + FlightReact = require('react/react.shared-subset'); + FlightReactDOM = require('react-dom'); + + // This reset is to load modules for the SSR/Browser scope. + jest.resetModules(); + act = require('internal-test-utils').act; + Stream = require('stream'); + React = require('react'); + use = React.use; + Suspense = React.Suspense; + ReactDOMClient = require('react-dom/client'); + ReactDOMFizzServer = require('react-dom/server.node'); + ReactServerDOMClient = require('react-server-dom-webpack/client'); + + ErrorBoundary = class extends React.Component { + state = {hasError: false, error: null}; + static getDerivedStateFromError(error) { + return { + hasError: true, + error, + }; + } + render() { + if (this.state.hasError) { + return this.props.fallback(this.state.error); + } + return this.props.children; + } + }; + }); + + function getTestStream() { + const writable = new Stream.PassThrough(); + const readable = new ReadableStream({ + start(controller) { + writable.on('data', chunk => { + controller.enqueue(chunk); + }); + writable.on('end', () => { + controller.close(); + }); + }, + }); + return { + readable, + writable, + }; + } + + const theInfinitePromise = new Promise(() => {}); + function InfiniteSuspend() { + throw theInfinitePromise; + } + + function getMeaningfulChildren(element) { + const children = []; + let node = element.firstChild; + while (node) { + if (node.nodeType === 1) { + if ( + // some tags are ambiguous and might be hidden because they look like non-meaningful children + // so we have a global override where if this data attribute is included we also include the node + node.hasAttribute('data-meaningful') || + (node.tagName === 'SCRIPT' && + node.hasAttribute('src') && + node.hasAttribute('async')) || + (node.tagName !== 'SCRIPT' && + node.tagName !== 'TEMPLATE' && + node.tagName !== 'template' && + !node.hasAttribute('hidden') && + !node.hasAttribute('aria-hidden')) + ) { + const props = {}; + const attributes = node.attributes; + for (let i = 0; i < attributes.length; i++) { + if ( + attributes[i].name === 'id' && + attributes[i].value.includes(':') + ) { + // We assume this is a React added ID that's a non-visual implementation detail. + continue; + } + props[attributes[i].name] = attributes[i].value; + } + props.children = getMeaningfulChildren(node); + children.push(React.createElement(node.tagName.toLowerCase(), props)); + } + } else if (node.nodeType === 3) { + children.push(node.data); + } + node = node.nextSibling; + } + return children.length === 0 + ? undefined + : children.length === 1 + ? children[0] + : children; + } + + it('should resolve HTML using Node streams', async () => { + function Text({children}) { + return {children}; + } + function HTML() { + return ( +
+ hello + world +
+ ); + } + + function App() { + const model = { + html: , + }; + return model; + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + const model = await response; + expect(model).toEqual({ + html: ( +
+ hello + world +
+ ), + }); + }); + + it('should resolve the root', async () => { + // Model + function Text({children}) { + return {children}; + } + function HTML() { + return ( +
+ hello + world +
+ ); + } + function RootModel() { + return { + html: , + }; + } + + // View + function Message({response}) { + return
{use(response).html}
; + } + function App({response}) { + return ( + Loading...}> + + + ); + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe( + '
helloworld
', + ); + }); + + it('should not get confused by $', async () => { + // Model + function RootModel() { + return {text: '$1'}; + } + + // View + function Message({response}) { + return

{use(response).text}

; + } + function App({response}) { + return ( + Loading...}> + + + ); + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

$1

'); + }); + + it('should not get confused by @', async () => { + // Model + function RootModel() { + return {text: '@div'}; + } + + // View + function Message({response}) { + return

{use(response).text}

; + } + function App({response}) { + return ( + Loading...}> + + + ); + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

@div

'); + }); + + it('should be able to esm compat test module references', async () => { + const ESMCompatModule = { + __esModule: true, + default: function ({greeting}) { + return greeting + ' World'; + }, + hi: 'Hello', + }; + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + function interopWebpack(obj) { + // Basically what Webpack's ESM interop feature testing does. + if (typeof obj === 'object' && obj.__esModule) { + return obj; + } + return Object.assign({default: obj}, obj); + } + + const {default: Component, hi} = interopWebpack( + clientExports(ESMCompatModule), + ); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

Hello World

'); + }); + + it('should be able to render a named component export', async () => { + const Module = { + Component: function ({greeting}) { + return greeting + ' World'; + }, + }; + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const {Component} = clientExports(Module); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

Hello World

'); + }); + + it('should be able to render a module split named component export', async () => { + const Module = { + // This gets split into a separate module from the original one. + split: function ({greeting}) { + return greeting + ' World'; + }, + }; + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const {split: Component} = clientExports(Module); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

Hello World

'); + }); + + it('should unwrap async module references', async () => { + const AsyncModule = Promise.resolve(function AsyncModule({text}) { + return 'Async: ' + text; + }); + + const AsyncModule2 = Promise.resolve({ + exportName: 'Module', + }); + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const AsyncModuleRef = await clientExports(AsyncModule); + const AsyncModuleRef2 = await clientExports(AsyncModule2); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

Async: Module

'); + }); + + it('should unwrap async module references using use', async () => { + const AsyncModule = Promise.resolve('Async Text'); + + function Print({response}) { + return use(response); + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const AsyncModuleRef = clientExports(AsyncModule); + + function ServerComponent() { + const text = FlightReact.use(AsyncModuleRef); + return

{text}

; + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

Async Text

'); + }); + + it('should be able to import a name called "then"', async () => { + const thenExports = { + then: function then() { + return 'and then'; + }, + }; + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const ThenRef = clientExports(thenExports).then; + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

and then

'); + }); + + it('throws when accessing a member below the client exports', () => { + const ClientModule = clientExports({ + Component: {deep: 'thing'}, + }); + function dotting() { + return ClientModule.Component.deep; + } + expect(dotting).toThrowError( + 'Cannot access Component.deep on the server. ' + + 'You cannot dot into a client module from a server component. ' + + 'You can only pass the imported name through.', + ); + }); + + it('does not throw when React inspects any deep props', () => { + const ClientModule = clientExports({ + Component: function () {}, + }); + ; + }); + + it('throws when accessing a Context.Provider below the client exports', () => { + const Context = React.createContext(); + const ClientModule = clientExports({ + Context, + }); + function dotting() { + return ClientModule.Context.Provider; + } + expect(dotting).toThrowError( + `Cannot render a Client Context Provider on the Server. ` + + `Instead, you can export a Client Component wrapper ` + + `that itself renders a Client Context Provider.`, + ); + }); + + it('should progressively reveal server components', async () => { + let reportedErrors = []; + + // Client Components + + function MyErrorBoundary({children}) { + return ( + ( +

+ {__DEV__ ? e.message + ' + ' : null} + {e.digest} +

+ )}> + {children} +
+ ); + } + + // Model + function Text({children}) { + return children; + } + + function makeDelayedText() { + let _resolve, _reject; + let promise = new Promise((resolve, reject) => { + _resolve = () => { + promise = null; + resolve(); + }; + _reject = e => { + promise = null; + reject(e); + }; + }); + async function DelayedText({children}) { + await promise; + return {children}; + } + return [DelayedText, _resolve, _reject]; + } + + const [Friends, resolveFriends] = makeDelayedText(); + const [Name, resolveName] = makeDelayedText(); + const [Posts, resolvePosts] = makeDelayedText(); + const [Photos, resolvePhotos] = makeDelayedText(); + const [Games, , rejectGames] = makeDelayedText(); + + // View + function ProfileDetails({avatar}) { + return ( +
+ :name: + {avatar} +
+ ); + } + function ProfileSidebar({friends}) { + return ( +
+ :photos: + {friends} +
+ ); + } + function ProfilePosts({posts}) { + return
{posts}
; + } + function ProfileGames({games}) { + return
{games}
; + } + + const MyErrorBoundaryClient = clientExports(MyErrorBoundary); + + function ProfileContent() { + return ( + <> + :avatar:} /> + (loading sidebar)

}> + :friends:} /> +
+ (loading posts)

}> + :posts:} /> +
+ + (loading games)

}> + :games:} /> +
+
+ + ); + } + + const model = { + rootContent: , + }; + + function ProfilePage({response}) { + return use(response).rootContent; + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + model, + webpackMap, + { + onError(x) { + reportedErrors.push(x); + return __DEV__ ? 'a dev digest' : `digest("${x.message}")`; + }, + }, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + (loading)

}> + +
, + ); + }); + expect(container.innerHTML).toBe('

(loading)

'); + + // This isn't enough to show anything. + await act(() => { + resolveFriends(); + }); + expect(container.innerHTML).toBe('

(loading)

'); + + // We can now show the details. Sidebar and posts are still loading. + await act(() => { + resolveName(); + }); + // Advance time enough to trigger a nested fallback. + await act(() => { + jest.advanceTimersByTime(500); + }); + expect(container.innerHTML).toBe( + '
:name::avatar:
' + + '

(loading sidebar)

' + + '

(loading posts)

' + + '

(loading games)

', + ); + + expect(reportedErrors).toEqual([]); + + const theError = new Error('Game over'); + // Let's *fail* loading games. + await act(async () => { + await rejectGames(theError); + await 'the inner async function'; + }); + const expectedGamesValue = __DEV__ + ? '

Game over + a dev digest

' + : '

digest("Game over")

'; + expect(container.innerHTML).toBe( + '
:name::avatar:
' + + '

(loading sidebar)

' + + '

(loading posts)

' + + expectedGamesValue, + ); + + expect(reportedErrors).toEqual([theError]); + reportedErrors = []; + + // We can now show the sidebar. + await act(async () => { + await resolvePhotos(); + await 'the inner async function'; + }); + expect(container.innerHTML).toBe( + '
:name::avatar:
' + + '
:photos::friends:
' + + '

(loading posts)

' + + expectedGamesValue, + ); + + // Show everything. + await act(async () => { + await resolvePosts(); + await 'the inner async function'; + }); + expect(container.innerHTML).toBe( + '
:name::avatar:
' + + '
:photos::friends:
' + + '
:posts:
' + + expectedGamesValue, + ); + + expect(reportedErrors).toEqual([]); + }); + + it('should preserve state of client components on refetch', async () => { + // Client + + function Page({response}) { + return use(response); + } + + function Input() { + return ; + } + + const InputClient = clientExports(Input); + + // Server + + function App({color}) { + // Verify both DOM and Client children. + return ( +
+ + +
+ ); + } + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + const stream1 = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(stream1.writable); + const response1 = ReactServerDOMClient.createFromReadableStream( + stream1.readable, + ); + await act(() => { + root.render( + (loading)

}> + +
, + ); + }); + expect(container.children.length).toBe(1); + expect(container.children[0].tagName).toBe('DIV'); + expect(container.children[0].style.color).toBe('red'); + + // Change the DOM state for both inputs. + const inputA = container.children[0].children[0]; + expect(inputA.tagName).toBe('INPUT'); + inputA.value = 'hello'; + const inputB = container.children[0].children[1]; + expect(inputB.tagName).toBe('INPUT'); + inputB.value = 'goodbye'; + + const stream2 = getTestStream(); + const {pipe: pipe2} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe2(stream2.writable); + const response2 = ReactServerDOMClient.createFromReadableStream( + stream2.readable, + ); + await act(() => { + root.render( + (loading)

}> + +
, + ); + }); + expect(container.children.length).toBe(1); + expect(container.children[0].tagName).toBe('DIV'); + expect(container.children[0].style.color).toBe('blue'); + + // Verify we didn't destroy the DOM for either input. + expect(inputA === container.children[0].children[0]).toBe(true); + expect(inputA.tagName).toBe('INPUT'); + expect(inputA.value).toBe('hello'); + expect(inputB === container.children[0].children[1]).toBe(true); + expect(inputB.tagName).toBe('INPUT'); + expect(inputB.value).toBe('goodbye'); + }); + + it('should be able to complete after aborting and throw the reason client-side', async () => { + const reportedErrors = []; + + const {writable, readable} = getTestStream(); + const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream( +
+ +
, + webpackMap, + { + onError(x) { + reportedErrors.push(x); + const message = typeof x === 'string' ? x : x.message; + return __DEV__ ? 'a dev digest' : `digest("${message}")`; + }, + }, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + function App({res}) { + return use(res); + } + + await act(() => { + root.render( + ( +

+ {__DEV__ ? e.message + ' + ' : null} + {e.digest} +

+ )}> + (loading)

}> + +
+
, + ); + }); + expect(container.innerHTML).toBe('

(loading)

'); + + await act(() => { + abort('for reasons'); + }); + if (__DEV__) { + expect(container.innerHTML).toBe( + '

Error: for reasons + a dev digest

', + ); + } else { + expect(container.innerHTML).toBe('

digest("for reasons")

'); + } + + expect(reportedErrors).toEqual(['for reasons']); + }); + + it('should be able to recover from a direct reference erroring client-side', async () => { + const reportedErrors = []; + + const ClientComponent = clientExports(function ({prop}) { + return 'This should never render'; + }); + + const ClientReference = clientModuleError(new Error('module init error')); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( +
+ +
, + webpackMap, + { + onError(x) { + reportedErrors.push(x); + }, + }, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + function App({res}) { + return use(res); + } + + await act(() => { + root.render( +

{e.message}

}> + (loading)

}> + +
+
, + ); + }); + expect(container.innerHTML).toBe('

module init error

'); + + expect(reportedErrors).toEqual([]); + }); + + it('should be able to recover from a direct reference erroring client-side async', async () => { + const reportedErrors = []; + + const ClientComponent = clientExports(function ({prop}) { + return 'This should never render'; + }); + + let rejectPromise; + const ClientReference = await clientExports( + new Promise((resolve, reject) => { + rejectPromise = reject; + }), + ); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( +
+ +
, + webpackMap, + { + onError(x) { + reportedErrors.push(x); + }, + }, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + function App({res}) { + return use(res); + } + + await act(() => { + root.render( +

{e.message}

}> + (loading)

}> + +
+
, + ); + }); + + expect(container.innerHTML).toBe('

(loading)

'); + + await act(() => { + rejectPromise(new Error('async module init error')); + }); + + expect(container.innerHTML).toBe('

async module init error

'); + + expect(reportedErrors).toEqual([]); + }); + + it('should be able to recover from a direct reference erroring server-side', async () => { + const reportedErrors = []; + + const ClientComponent = clientExports(function ({prop}) { + return 'This should never render'; + }); + + // We simulate a bug in the Webpack bundler which causes an error on the server. + for (const id in webpackMap) { + Object.defineProperty(webpackMap, id, { + get: () => { + throw new Error('bug in the bundler'); + }, + }); + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( +
+ +
, + webpackMap, + { + onError(x) { + reportedErrors.push(x.message); + return __DEV__ ? 'a dev digest' : `digest("${x.message}")`; + }, + }, + ); + pipe(writable); + + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + function App({res}) { + return use(res); + } + + await act(() => { + root.render( + ( +

+ {__DEV__ ? e.message + ' + ' : null} + {e.digest} +

+ )}> + (loading)

}> + +
+
, + ); + }); + if (__DEV__) { + expect(container.innerHTML).toBe( + '

bug in the bundler + a dev digest

', + ); + } else { + expect(container.innerHTML).toBe('

digest("bug in the bundler")

'); + } + + expect(reportedErrors).toEqual(['bug in the bundler']); + }); + + it('should pass a Promise through props and be able use() it on the client', async () => { + async function getData() { + return 'async hello'; + } + + function Component({data}) { + const text = use(data); + return

{text}

; + } + + const ClientComponent = clientExports(Component); + + function ServerComponent() { + const data = getData(); // no await here + return ; + } + + function Print({response}) { + return use(response); + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

async hello

'); + }); + + it('should throw on the client if a passed promise eventually rejects', async () => { + const reportedErrors = []; + const theError = new Error('Server throw'); + + async function getData() { + throw theError; + } + + function Component({data}) { + const text = use(data); + return

{text}

; + } + + const ClientComponent = clientExports(Component); + + function ServerComponent() { + const data = getData(); // no await here + return ; + } + + function Await({response}) { + return use(response); + } + + function App({response}) { + return ( + Loading...}> + ( +

+ {__DEV__ ? e.message + ' + ' : null} + {e.digest} +

+ )}> + +
+
+ ); + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + { + onError(x) { + reportedErrors.push(x); + return __DEV__ ? 'a dev digest' : `digest("${x.message}")`; + }, + }, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe( + __DEV__ + ? '

Server throw + a dev digest

' + : '

digest("Server throw")

', + ); + expect(reportedErrors).toEqual([theError]); + }); + + it('should support float methods when rendering in Fiber', async () => { + function Component() { + return

hello world

; + } + + const ClientComponent = clientExports(Component); + + async function ServerComponent() { + 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; + 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 ; + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + + let response = null; + function getResponse() { + if (response === null) { + response = ReactServerDOMClient.createFromReadableStream(readable); + } + return response; + } + + function App() { + return getResponse(); + } + + // We pause to allow the float call after the await point to process before the + // HostDispatcher gets set for Fiber by createRoot. This is only needed in testing + // because the module graphs are not different and the HostDispatcher is shared. + // In a real environment the Fiber and Flight code would each have their own independent + // dispatcher. + // @TODO consider what happens when Server-Components-On-The-Client exist. we probably + // want to use the Fiber HostDispatcher there too since it is more about the host than the runtime + // but we need to make sure that actually makes sense + await 1; + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + + +