diff --git a/.eslintrc.js b/.eslintrc.js index 35e83b0d7abc..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', @@ -415,9 +416,7 @@ module.exports = { }, }, { - files: [ - 'packages/react-native-renderer/**/*.js', - ], + files: ['packages/react-native-renderer/**/*.js'], globals: { nativeFabricUIManager: 'readonly', }, @@ -426,7 +425,14 @@ module.exports = { files: ['packages/react-server-dom-webpack/**/*.js'], globals: { __webpack_chunk_load__: 'readonly', - __webpack_require__: 'readonly', + __webpack_require__: true, + }, + }, + { + files: ['packages/react-server-dom-turbopack/**/*.js'], + globals: { + __turbopack_load__: 'readonly', + __turbopack_require__: 'readonly', }, }, { 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/.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..195bda468dd4 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,24 @@ app.all('/', async function (req, res, next) { // For HTML, we're a "client" emulator that runs the client code, // so we start by consuming the RSC payload. This needs a module // map that reverse engineers the client-side path to the SSR path. - const root = await createFromNodeStream(rscResponse, moduleMap); + let root; + let Root = () => { + if (root) { + return React.use(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'); - const {pipe} = renderToPipeableStream(root, { + const {pipe} = renderToPipeableStream(React.createElement(Root), { bootstrapScripts: mainJSChunks, }); pipe(res); 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/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..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; @@ -35,6 +36,8 @@ export const resolveServerReference = $$$config.resolveServerReference; export const preloadModule = $$$config.preloadModule; export const requireModule = $$$config.requireModule; export const dispatchHint = $$$config.dispatchHint; +export const prepareDestinationForModule = + $$$config.prepareDestinationForModule; export const usedWithSSR = true; export opaque type Source = mixed; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js index 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-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-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-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-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 212290670bd5..65f3da05934a 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js @@ -8,6 +8,18 @@ */ 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 ModuleLoading = mixed; +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 prepareDestinationForModule: any = null; export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js index 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-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-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/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 ed0905658509..1eaabb948d63 100644 --- a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js +++ b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js @@ -63,3 +63,25 @@ export function dispatchHint(code: string, model: HintModel): void { } } } + +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') { + 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-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-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-webpack/src/ReactFlightClientConfigNodeBundler.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js similarity index 74% rename from packages/react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler.js rename to packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js index 0789a52ffc0e..05605700c400 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler.js +++ b/packages/react-server-dom-turbopack/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-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( + + + + + + + + +