diff --git a/.eslintrc.js b/.eslintrc.js index dec7cd8f304..a54cc458995 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -330,6 +330,7 @@ module.exports = { 'packages/react-server-dom-webpack/**/*.js', 'packages/react-server-dom-turbopack/**/*.js', 'packages/react-server-dom-parcel/**/*.js', + 'packages/react-server-dom-rspack/**/*.js', 'packages/react-server-dom-fb/**/*.js', 'packages/react-server-dom-unbundled/**/*.js', 'packages/react-test-renderer/**/*.js', @@ -486,6 +487,14 @@ module.exports = { parcelRequire: 'readonly', }, }, + { + files: ['packages/react-server-dom-rspack/**/*.js'], + globals: { + __webpack_chunk_load__: 'readonly', + __webpack_get_script_filename__: 'readonly', + __webpack_require__: 'readonly', + }, + }, { files: ['packages/scheduler/**/*.js'], globals: { diff --git a/ReactVersions.js b/ReactVersions.js index dd8c78afb13..16dfbbde05e 100644 --- a/ReactVersions.js +++ b/ReactVersions.js @@ -41,6 +41,7 @@ const stablePackages = { 'react-server-dom-webpack': ReactVersion, 'react-server-dom-turbopack': ReactVersion, 'react-server-dom-parcel': ReactVersion, + 'react-server-dom-rspack': ReactVersion, 'react-is': ReactVersion, 'react-reconciler': '0.34.0', 'react-refresh': '0.19.0', diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-rspack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-rspack.js new file mode 100644 index 00000000000..4545e17a8fc --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-rspack.js @@ -0,0 +1,19 @@ +/** + * 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 {default as rendererVersion} from 'shared/ReactVersion'; +export const rendererPackageName = 'react-server-dom-rspack'; + +export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; +export * from 'react-client/src/ReactClientConsoleConfigBrowser'; +export * from 'react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspack'; +export * from 'react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspackBrowser'; +export * from 'react-server-dom-rspack/src/client/ReactFlightClientConfigTargetRspackBrowser'; +export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; +export const usedWithSSR = false; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-rspack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-rspack.js new file mode 100644 index 00000000000..83f18f38cd0 --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-rspack.js @@ -0,0 +1,19 @@ +/** + * 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 {default as rendererVersion} from 'shared/ReactVersion'; +export const rendererPackageName = 'react-server-dom-rspack'; + +export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; +export * from 'react-client/src/ReactClientConsoleConfigServer'; +export * from 'react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspack'; +export * from 'react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspackServer'; +export * from 'react-server-dom-rspack/src/client/ReactFlightClientConfigTargetRspackServer'; +export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; +export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-rspack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-rspack.js new file mode 100644 index 00000000000..57dfd63a343 --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-rspack.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ +export {default as rendererVersion} from 'shared/ReactVersion'; +export const rendererPackageName = 'react-server-dom-rspack'; + +export * from 'react-client/src/ReactFlightClientStreamConfigNode'; +export * from 'react-client/src/ReactClientConsoleConfigServer'; +export * from 'react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspack'; +export * from 'react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspackServer'; +export * from 'react-server-dom-rspack/src/client/ReactFlightClientConfigTargetRspackServer'; +export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; +export const usedWithSSR = true; diff --git a/packages/react-server-dom-rspack/README.md b/packages/react-server-dom-rspack/README.md new file mode 100644 index 00000000000..160255abf36 --- /dev/null +++ b/packages/react-server-dom-rspack/README.md @@ -0,0 +1,5 @@ +# react-server-dom-rspack + +Experimental React Flight bindings for DOM using Rspack. + +**Use it at your own risk.** diff --git a/packages/react-server-dom-rspack/client.browser.js b/packages/react-server-dom-rspack/client.browser.js new file mode 100644 index 00000000000..1be0bc04a7e --- /dev/null +++ b/packages/react-server-dom-rspack/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/client/react-flight-dom-client.browser'; diff --git a/packages/react-server-dom-rspack/client.edge.js b/packages/react-server-dom-rspack/client.edge.js new file mode 100644 index 00000000000..ab6a110c112 --- /dev/null +++ b/packages/react-server-dom-rspack/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/client/react-flight-dom-client.edge'; diff --git a/packages/react-server-dom-rspack/client.js b/packages/react-server-dom-rspack/client.js new file mode 100644 index 00000000000..2dad5bb5138 --- /dev/null +++ b/packages/react-server-dom-rspack/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-rspack/client.node.js b/packages/react-server-dom-rspack/client.node.js new file mode 100644 index 00000000000..c3ec7662d6e --- /dev/null +++ b/packages/react-server-dom-rspack/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/client/react-flight-dom-client.node'; diff --git a/packages/react-server-dom-rspack/index.js b/packages/react-server-dom-rspack/index.js new file mode 100644 index 00000000000..d7143b9086a --- /dev/null +++ b/packages/react-server-dom-rspack/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-rspack/client instead.'); diff --git a/packages/react-server-dom-rspack/npm/client.browser.js b/packages/react-server-dom-rspack/npm/client.browser.js new file mode 100644 index 00000000000..d7178511b08 --- /dev/null +++ b/packages/react-server-dom-rspack/npm/client.browser.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-rspack-client.browser.production.js'); +} else { + module.exports = require('./cjs/react-server-dom-rspack-client.browser.development.js'); +} diff --git a/packages/react-server-dom-rspack/npm/client.edge.js b/packages/react-server-dom-rspack/npm/client.edge.js new file mode 100644 index 00000000000..499cf9dc769 --- /dev/null +++ b/packages/react-server-dom-rspack/npm/client.edge.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-rspack-client.edge.production.js'); +} else { + module.exports = require('./cjs/react-server-dom-rspack-client.edge.development.js'); +} diff --git a/packages/react-server-dom-rspack/npm/client.js b/packages/react-server-dom-rspack/npm/client.js new file mode 100644 index 00000000000..89d93a7a792 --- /dev/null +++ b/packages/react-server-dom-rspack/npm/client.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./client.browser'); diff --git a/packages/react-server-dom-rspack/npm/client.node.js b/packages/react-server-dom-rspack/npm/client.node.js new file mode 100644 index 00000000000..632be3e3625 --- /dev/null +++ b/packages/react-server-dom-rspack/npm/client.node.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-rspack-client.node.production.js'); +} else { + module.exports = require('./cjs/react-server-dom-rspack-client.node.development.js'); +} diff --git a/packages/react-server-dom-rspack/npm/client.node.unbundled.js b/packages/react-server-dom-rspack/npm/client.node.unbundled.js new file mode 100644 index 00000000000..f202f21902c --- /dev/null +++ b/packages/react-server-dom-rspack/npm/client.node.unbundled.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-rspack-client.node.unbundled.production.js'); +} else { + module.exports = require('./cjs/react-server-dom-rspack-client.node.unbundled.development.js'); +} diff --git a/packages/react-server-dom-rspack/npm/esm/package.json b/packages/react-server-dom-rspack/npm/esm/package.json new file mode 100644 index 00000000000..3dbc1ca591c --- /dev/null +++ b/packages/react-server-dom-rspack/npm/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/react-server-dom-rspack/npm/index.js b/packages/react-server-dom-rspack/npm/index.js new file mode 100644 index 00000000000..b9ba6cc4c7b --- /dev/null +++ b/packages/react-server-dom-rspack/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-rspack/client instead.'); diff --git a/packages/react-server-dom-rspack/npm/plugin.js b/packages/react-server-dom-rspack/npm/plugin.js new file mode 100644 index 00000000000..b94967ee036 --- /dev/null +++ b/packages/react-server-dom-rspack/npm/plugin.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./cjs/react-server-dom-rspack-plugin.js'); diff --git a/packages/react-server-dom-rspack/npm/server.browser.js b/packages/react-server-dom-rspack/npm/server.browser.js new file mode 100644 index 00000000000..a9bbcfd4588 --- /dev/null +++ b/packages/react-server-dom-rspack/npm/server.browser.js @@ -0,0 +1,24 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-rspack-server.browser.production.js'); +} else { + s = require('./cjs/react-server-dom-rspack-server.browser.development.js'); +} + +exports.renderToReadableStream = s.renderToReadableStream; +exports.decodeReply = s.decodeReply; +exports.decodeAction = s.decodeAction; +exports.decodeFormState = s.decodeFormState; +exports.registerServerReference = s.registerServerReference; +exports.registerClientReference = s.registerClientReference; +exports.createTemporaryReferenceSet = s.createTemporaryReferenceSet; + +exports.setServerActionBoundArgsEncryption = + s.setServerActionBoundArgsEncryption; +exports.encryptServerActionBoundArgs = s.encryptServerActionBoundArgs; +exports.decryptServerActionBoundArgs = s.decryptServerActionBoundArgs; +exports.loadServerAction = s.loadServerAction; +exports.createServerEntry = s.createServerEntry; +exports.ensureServerActions = s.ensureServerActions; diff --git a/packages/react-server-dom-rspack/npm/server.edge.js b/packages/react-server-dom-rspack/npm/server.edge.js new file mode 100644 index 00000000000..25ae4f3bef6 --- /dev/null +++ b/packages/react-server-dom-rspack/npm/server.edge.js @@ -0,0 +1,25 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-rspack-server.edge.production.js'); +} else { + s = require('./cjs/react-server-dom-rspack-server.edge.development.js'); +} + +exports.renderToReadableStream = s.renderToReadableStream; +exports.decodeReply = s.decodeReply; +exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable; +exports.decodeAction = s.decodeAction; +exports.decodeFormState = s.decodeFormState; +exports.registerServerReference = s.registerServerReference; +exports.registerClientReference = s.registerClientReference; +exports.createTemporaryReferenceSet = s.createTemporaryReferenceSet; + +exports.setServerActionBoundArgsEncryption = + s.setServerActionBoundArgsEncryption; +exports.encryptServerActionBoundArgs = s.encryptServerActionBoundArgs; +exports.decryptServerActionBoundArgs = s.decryptServerActionBoundArgs; +exports.loadServerAction = s.loadServerAction; +exports.createServerEntry = s.createServerEntry; +exports.ensureServerActions = s.ensureServerActions; diff --git a/packages/react-server-dom-rspack/npm/server.js b/packages/react-server-dom-rspack/npm/server.js new file mode 100644 index 00000000000..13a632e6411 --- /dev/null +++ b/packages/react-server-dom-rspack/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-rspack/npm/server.node.js b/packages/react-server-dom-rspack/npm/server.node.js new file mode 100644 index 00000000000..db15d642704 --- /dev/null +++ b/packages/react-server-dom-rspack/npm/server.node.js @@ -0,0 +1,27 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-rspack-server.node.production.js'); +} else { + s = require('./cjs/react-server-dom-rspack-server.node.development.js'); +} + +exports.renderToReadableStream = s.renderToReadableStream; +exports.renderToPipeableStream = s.renderToPipeableStream; +exports.decodeReply = s.decodeReply; +exports.decodeReplyFromBusboy = s.decodeReplyFromBusboy; +exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable; +exports.decodeAction = s.decodeAction; +exports.decodeFormState = s.decodeFormState; +exports.registerServerReference = s.registerServerReference; +exports.registerClientReference = s.registerClientReference; +exports.createTemporaryReferenceSet = s.createTemporaryReferenceSet; + +exports.setServerActionBoundArgsEncryption = + s.setServerActionBoundArgsEncryption; +exports.encryptServerActionBoundArgs = s.encryptServerActionBoundArgs; +exports.decryptServerActionBoundArgs = s.decryptServerActionBoundArgs; +exports.loadServerAction = s.loadServerAction; +exports.createServerEntry = s.createServerEntry; +exports.ensureServerActions = s.ensureServerActions; diff --git a/packages/react-server-dom-rspack/npm/server.node.unbundled.js b/packages/react-server-dom-rspack/npm/server.node.unbundled.js new file mode 100644 index 00000000000..e64c4ee8bf9 --- /dev/null +++ b/packages/react-server-dom-rspack/npm/server.node.unbundled.js @@ -0,0 +1,27 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-rspack-server.node.unbundled.production.js'); +} else { + s = require('./cjs/react-server-dom-rspack-server.node.unbundled.development.js'); +} + +exports.renderToReadableStream = s.renderToReadableStream; +exports.renderToPipeableStream = s.renderToPipeableStream; +exports.decodeReply = s.decodeReply; +exports.decodeReplyFromBusboy = s.decodeReplyFromBusboy; +exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable; +exports.decodeAction = s.decodeAction; +exports.decodeFormState = s.decodeFormState; +exports.registerServerReference = s.registerServerReference; +exports.registerClientReference = s.registerClientReference; +exports.createTemporaryReferenceSet = s.createTemporaryReferenceSet; + +exports.setServerActionBoundArgsEncryption = + s.setServerActionBoundArgsEncryption; +exports.encryptServerActionBoundArgs = s.encryptServerActionBoundArgs; +exports.decryptServerActionBoundArgs = s.decryptServerActionBoundArgs; +exports.loadServerAction = s.loadServerAction; +exports.createServerEntry = s.createServerEntry; +exports.ensureServerActions = s.ensureServerActions; diff --git a/packages/react-server-dom-rspack/npm/static.browser.js b/packages/react-server-dom-rspack/npm/static.browser.js new file mode 100644 index 00000000000..448d0ffd498 --- /dev/null +++ b/packages/react-server-dom-rspack/npm/static.browser.js @@ -0,0 +1,10 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-rspack-server.browser.production.js'); +} else { + s = require('./cjs/react-server-dom-rspack-server.browser.development.js'); +} + +exports.prerender = s.prerender; diff --git a/packages/react-server-dom-rspack/npm/static.edge.js b/packages/react-server-dom-rspack/npm/static.edge.js new file mode 100644 index 00000000000..8a050731f7f --- /dev/null +++ b/packages/react-server-dom-rspack/npm/static.edge.js @@ -0,0 +1,10 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-rspack-server.edge.production.js'); +} else { + s = require('./cjs/react-server-dom-rspack-server.edge.development.js'); +} + +exports.prerender = s.prerender; diff --git a/packages/react-server-dom-rspack/npm/static.js b/packages/react-server-dom-rspack/npm/static.js new file mode 100644 index 00000000000..13a632e6411 --- /dev/null +++ b/packages/react-server-dom-rspack/npm/static.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-rspack/npm/static.node.js b/packages/react-server-dom-rspack/npm/static.node.js new file mode 100644 index 00000000000..457d800df70 --- /dev/null +++ b/packages/react-server-dom-rspack/npm/static.node.js @@ -0,0 +1,11 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-rspack-server.node.production.js'); +} else { + s = require('./cjs/react-server-dom-rspack-server.node.development.js'); +} + +exports.prerender = s.prerender; +exports.prerenderToNodeStream = s.prerenderToNodeStream; diff --git a/packages/react-server-dom-rspack/npm/static.node.unbundled.js b/packages/react-server-dom-rspack/npm/static.node.unbundled.js new file mode 100644 index 00000000000..ae89ad7a256 --- /dev/null +++ b/packages/react-server-dom-rspack/npm/static.node.unbundled.js @@ -0,0 +1,10 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-rspack-server.node.unbundled.production.js'); +} else { + s = require('./cjs/react-server-dom-rspack-server.node.unbundled.development.js'); +} + +exports.prerenderToNodeStream = s.prerenderToNodeStream; diff --git a/packages/react-server-dom-rspack/package.json b/packages/react-server-dom-rspack/package.json new file mode 100644 index 00000000000..20ef776a8af --- /dev/null +++ b/packages/react-server-dom-rspack/package.json @@ -0,0 +1,90 @@ +{ + "name": "react-server-dom-rspack", + "description": "React Server Components bindings for DOM using Rspack. This is intended to be integrated into meta-frameworks. It is not intended to be imported directly.", + "version": "19.3.0", + "keywords": [ + "react" + ], + "homepage": "https://react.dev/", + "bugs": "https://github.com/facebook/react/issues", + "license": "MIT", + "files": [ + "LICENSE", + "README.md", + "index.js", + "plugin.js", + "client.js", + "client.browser.js", + "client.edge.js", + "client.node.js", + "server.js", + "server.browser.js", + "server.edge.js", + "server.node.js", + "static.js", + "static.browser.js", + "static.edge.js", + "static.node.js", + "node-register.js", + "cjs/", + "esm/" + ], + "exports": { + ".": "./index.js", + "./plugin": "./plugin.js", + "./client": { + "workerd": "./client.edge.js", + "deno": "./client.edge.js", + "worker": "./client.edge.js", + "node": "./client.node.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", + "./server": { + "react-server": { + "workerd": "./server.edge.js", + "deno": "./server.browser.js", + "node": "./server.node.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", + "./static": { + "react-server": { + "workerd": "./static.edge.js", + "deno": "./static.browser.js", + "node": "./static.node.js", + "edge-light": "./static.edge.js", + "browser": "./static.browser.js" + }, + "default": "./static.js" + }, + "./static.browser": "./static.browser.js", + "./static.edge": "./static.edge.js", + "./static.node": "./static.node.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-rspack" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.3.0", + "react-dom": "^19.3.0", + "rspack": "^2.0.0" + } +} diff --git a/packages/react-server-dom-rspack/server.browser.js b/packages/react-server-dom-rspack/server.browser.js new file mode 100644 index 00000000000..941a6b2b64b --- /dev/null +++ b/packages/react-server-dom-rspack/server.browser.js @@ -0,0 +1,24 @@ +/** + * 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 { + renderToReadableStream, + decodeReply, + decodeAction, + decodeFormState, + registerServerReference, + registerClientReference, + createTemporaryReferenceSet, + setServerActionBoundArgsEncryption, + encryptServerActionBoundArgs, + decryptServerActionBoundArgs, + loadServerAction, + createServerEntry, + ensureServerActions, +} from './src/server/react-flight-dom-server.browser'; diff --git a/packages/react-server-dom-rspack/server.edge.js b/packages/react-server-dom-rspack/server.edge.js new file mode 100644 index 00000000000..563f1d5c898 --- /dev/null +++ b/packages/react-server-dom-rspack/server.edge.js @@ -0,0 +1,25 @@ +/** + * 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 { + renderToReadableStream, + decodeReply, + decodeReplyFromAsyncIterable, + decodeAction, + decodeFormState, + registerServerReference, + registerClientReference, + createTemporaryReferenceSet, + setServerActionBoundArgsEncryption, + encryptServerActionBoundArgs, + decryptServerActionBoundArgs, + loadServerAction, + createServerEntry, + ensureServerActions, +} from './src/server/react-flight-dom-server.edge'; diff --git a/packages/react-server-dom-rspack/server.js b/packages/react-server-dom-rspack/server.js new file mode 100644 index 00000000000..83d8b8a017f --- /dev/null +++ b/packages/react-server-dom-rspack/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-rspack/server.node.js b/packages/react-server-dom-rspack/server.node.js new file mode 100644 index 00000000000..134b1274bff --- /dev/null +++ b/packages/react-server-dom-rspack/server.node.js @@ -0,0 +1,27 @@ +/** + * 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 { + renderToPipeableStream, + renderToReadableStream, + decodeReply, + decodeReplyFromBusboy, + decodeReplyFromAsyncIterable, + decodeAction, + decodeFormState, + registerServerReference, + registerClientReference, + createTemporaryReferenceSet, + setServerActionBoundArgsEncryption, + encryptServerActionBoundArgs, + decryptServerActionBoundArgs, + loadServerAction, + createServerEntry, + ensureServerActions, +} from './src/server/react-flight-dom-server.node'; diff --git a/packages/react-server-dom-rspack/src/ReactFlightRspackReferences.js b/packages/react-server-dom-rspack/src/ReactFlightRspackReferences.js new file mode 100644 index 00000000000..7d089e382b6 --- /dev/null +++ b/packages/react-server-dom-rspack/src/ReactFlightRspackReferences.js @@ -0,0 +1,348 @@ +/** + * 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, + $$location?: Error, +}; + +// 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): any { + // $FlowFixMe[incompatible-call] + const newFn = FunctionBind.apply(this, arguments); + if (this.$$typeof === SERVER_REFERENCE_TAG) { + if (__DEV__) { + const thisBind = arguments[0]; + if (thisBind != null) { + console.error( + 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().', + ); + } + } + const args = ArraySlice.call(arguments, 1); + const $$typeof = {value: SERVER_REFERENCE_TAG}; + const $$id = {value: this.$$id}; + const $$bound = {value: this.$$bound ? this.$$bound.concat(args) : args}; + return Object.defineProperties( + (newFn: any), + (__DEV__ + ? { + $$typeof, + $$id, + $$bound, + $$location: { + value: this.$$location, + configurable: true, + }, + bind: {value: bind, configurable: true}, + } + : { + $$typeof, + $$id, + $$bound, + bind: {value: bind, configurable: true}, + }) as PropertyDescriptorMap, + ); + } + return newFn; +} + +const serverReferenceToString = { + value: () => 'function () { [omitted code] }', + configurable: true, + writable: true, +}; + +export function registerServerReference( + reference: T, + id: string, + exportName: null | string, +): ServerReference { + const $$typeof = {value: SERVER_REFERENCE_TAG}; + const $$id = { + value: exportName === null ? id : id + '#' + exportName, + configurable: true, + }; + const $$bound = {value: null, configurable: true}; + return Object.defineProperties( + (reference: any), + __DEV__ + ? ({ + $$typeof, + $$id, + $$bound, + $$location: { + value: Error('react-stack-top-frame'), + configurable: true, + }, + bind: {value: bind, configurable: true}, + toString: serverReferenceToString, + } as PropertyDescriptorMap) + : ({ + $$typeof, + $$id, + $$bound, + bind: {value: bind, configurable: true}, + toString: serverReferenceToString, + } as PropertyDescriptorMap), + ); +} + +const PROMISE_PROTOTYPE = Promise.prototype; + +const deepProxyHandlers: Proxy$traps = { + get: function ( + target: Function, + name: string | symbol, + 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; + // React looks for debugInfo on thenables. + case '_debugInfo': + return undefined; + // Avoid this attempting to be serialized. + case 'toJSON': + return undefined; + case Symbol.toPrimitive: + // $FlowFixMe[prop-missing] + return Object.prototype[Symbol.toPrimitive]; + case Symbol.toStringTag: + // $FlowFixMe[prop-missing] + return Object.prototype[Symbol.toStringTag]; + 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.`, + ); + case 'then': + throw new Error( + `Cannot await or return from a thenable. ` + + `You cannot await a client module from a server component.`, + ); + } + // 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.'); + }, +}; + +function getReference(target: Function, name: string | symbol): $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; + // React looks for debugInfo on thenables. + case '_debugInfo': + return undefined; + // Avoid this attempting to be serialized. + case 'toJSON': + return undefined; + case Symbol.toPrimitive: + // $FlowFixMe[prop-missing] + return Object.prototype[Symbol.toPrimitive]; + case Symbol.toStringTag: + // $FlowFixMe[prop-missing] + return Object.prototype[Symbol.toStringTag]; + 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; + } + } + if (typeof name === 'symbol') { + throw new Error( + 'Cannot read Symbol exports. Only named exports are supported on a client module ' + + 'imported on the server.', + ); + } + 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; +} + +const proxyHandlers = { + get: function ( + target: Function, + name: string | symbol, + receiver: Proxy, + ): $FlowFixMe { + return getReference(target, name); + }, + getOwnPropertyDescriptor: function ( + target: Function, + name: string | symbol, + ): $FlowFixMe { + let descriptor = Object.getOwnPropertyDescriptor(target, name); + if (!descriptor) { + descriptor = { + value: getReference(target, name), + writable: false, + configurable: false, + enumerable: false, + }; + Object.defineProperty(target, name, descriptor); + } + return descriptor; + }, + 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.'); + }, +}; diff --git a/packages/react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspack.js b/packages/react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspack.js new file mode 100644 index 00000000000..ef0d9e56ddb --- /dev/null +++ b/packages/react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspack.js @@ -0,0 +1,279 @@ +/** + * 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, + ReactDebugInfo, +} from 'shared/ReactTypes'; + +import type { + ImportMetadata, + ImportManifestEntry, +} from '../shared/ReactFlightImportMetadata'; +import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig'; + +import { + ID, + CHUNKS, + NAME, + isAsyncImport, +} from '../shared/ReactFlightImportMetadata'; + +import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig'; + +import { + loadChunk, + addChunkDebugInfo, +} from 'react-client/src/ReactFlightClientConfig'; + +import hasOwnProperty from 'shared/hasOwnProperty'; + +export type ServerConsumerModuleMap = null | { + [clientId: string]: { + [clientExportName: string]: ClientReferenceManifestEntry, + }, +}; + +export type ServerManifest = { + [id: string]: ImportManifestEntry, +}; + +export type ServerReferenceId = string; + +export opaque type ClientReferenceManifestEntry = ImportManifestEntry; +export opaque type ClientReferenceMetadata = ImportMetadata; + +// eslint-disable-next-line no-unused-vars +export opaque type ClientReference = ClientReferenceMetadata; + +// The reason this function needs to defined here in this file instead of just +// being exported directly from the WebpackDestination... file is because the +// ClientReferenceMetadata is opaque and we can't unwrap it there. +// This should get inlined and we could also just implement an unwrapping function +// though that risks it getting used in places it shouldn't be. This is unfortunate +// but currently it seems to be the best option we have. +export function prepareDestinationForModule( + moduleLoading: ModuleLoading, + nonce: ?string, + metadata: ClientReferenceMetadata, +) { + prepareDestinationWithChunks(moduleLoading, metadata[CHUNKS], nonce); +} + +export function resolveClientReference( + bundlerConfig: ServerConsumerModuleMap, + metadata: ClientReferenceMetadata, +): ClientReference { + if (bundlerConfig) { + const moduleExports = bundlerConfig[metadata[ID]]; + let resolvedModuleData = moduleExports && 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 && moduleExports['*']; + if (!resolvedModuleData) { + throw new Error( + 'Could not find the module "' + + metadata[ID] + + '" in the React Server Consumer Manifest. ' + + 'This is probably a bug in the React Server Components bundler.', + ); + } + name = metadata[NAME]; + } + // Note that resolvedModuleData.async may be set if this is an Async Module. + // For Client References we don't actually care because what matters is whether + // the consumer expects an unwrapped async module or just a raw Promise so it + // has to already know which one it wants. + // We could error if this is an Async Import but it's not an Async Module. + // However, we also support plain CJS exporting a top level Promise which is not + // an Async Module according to the bundle graph but is effectively the same. + if (isAsyncImport(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.', + ); + } + } + if (resolvedModuleData.async) { + // If the module is marked as async in a Client Reference, we don't actually care. + // What matters is whether the consumer wants to unwrap it or not. + // For Server References, it is different because the consumer is completely internal + // to the bundler. So instead of passing it to each reference we can mark it in the + // manifest. + return [ + resolvedModuleData.id, + resolvedModuleData.chunks, + name, + 1 /* async */, + ]; + } + 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 Rspack 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 = __webpack_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 = []; + let i = 0; + while (i < chunks.length) { + const chunkId = chunks[i++]; + const chunkFilename = chunks[i++]; + const entry = chunkCache.get(chunkId); + if (entry === undefined) { + const thenable = loadChunk(chunkId, chunkFilename); + promises.push(thenable); + // $FlowFixMe[method-unbinding] + const resolve = chunkCache.set.bind(chunkCache, chunkId, null); + thenable.then(resolve, ignoreReject); + chunkCache.set(chunkId, thenable); + } else if (entry !== null) { + promises.push(entry); + } + } + if (isAsyncImport(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 = __webpack_require__(metadata[ID]); + if (isAsyncImport(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; + } + if (hasOwnProperty.call(moduleExports, metadata[NAME])) { + return moduleExports[metadata[NAME]]; + } + return (undefined: any); +} + +export function getModuleDebugInfo( + metadata: ClientReference, +): null | ReactDebugInfo { + if (!__DEV__) { + return null; + } + const chunks = metadata[CHUNKS]; + const debugInfo: ReactDebugInfo = []; + let i = 0; + while (i < chunks.length) { + const chunkId = chunks[i++]; + const chunkFilename = chunks[i++]; + addChunkDebugInfo(debugInfo, chunkId, chunkFilename); + } + return debugInfo; +} diff --git a/packages/react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspackBrowser.js b/packages/react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspackBrowser.js new file mode 100644 index 00000000000..b2ccd0805e1 --- /dev/null +++ b/packages/react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspackBrowser.js @@ -0,0 +1,129 @@ +/** + * 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 { + ReactDebugInfo, + ReactIOInfo, + ReactAsyncInfo, +} from 'shared/ReactTypes'; + +const chunkMap: Map = new Map(); + +/** + * We patch the chunk filename function in Rspack to insert our own resolution + * of chunks that come from Flight and may not be known to the Rspack runtime + */ +const rspackGetChunkFilename = __webpack_require__.u; +__webpack_require__.u = function (chunkId: string) { + const flightChunk = chunkMap.get(chunkId); + if (flightChunk !== undefined) { + return flightChunk; + } + return rspackGetChunkFilename(chunkId); +}; + +export function loadChunk(chunkId: string, filename: string): Promise { + chunkMap.set(chunkId, filename); + return __webpack_chunk_load__(chunkId); +} + +// We cache ReactIOInfo across requests so that inner refreshes can dedupe with outer. +const chunkIOInfoCache: Map = __DEV__ + ? new Map() + : (null: any); + +export function addChunkDebugInfo( + target: ReactDebugInfo, + chunkId: string, + filename: string, +): void { + if (!__DEV__) { + return; + } + let ioInfo = chunkIOInfoCache.get(chunkId); + if (ioInfo === undefined) { + const scriptFilename = __webpack_get_script_filename__(chunkId); + let href; + try { + // $FlowFixMe + href = new URL(scriptFilename, document.baseURI).href; + } catch (_) { + href = scriptFilename; + } + let start = -1; + let end = -1; + let byteSize = 0; + // $FlowFixMe[method-unbinding] + if (typeof performance.getEntriesByType === 'function') { + // We may be able to collect the start and end time of this resource from Performance Observer. + const resourceEntries = performance.getEntriesByType('resource'); + for (let i = 0; i < resourceEntries.length; i++) { + const resourceEntry = resourceEntries[i]; + if (resourceEntry.name === href) { + start = resourceEntry.startTime; + end = start + resourceEntry.duration; + // $FlowFixMe[prop-missing] + byteSize = (resourceEntry.transferSize: any) || 0; + } + } + } + const value = Promise.resolve(href); + // $FlowFixMe + value.status = 'fulfilled'; + // $FlowFixMe + value.value = { + chunkId: chunkId, + href: href, + // Is there some more useful representation for the chunk? + }; + // Create a fake stack frame that points to the beginning of the chunk. This is + // probably not source mapped so will link to the compiled source rather than + // any individual file that goes into the chunks. + const fakeStack = new Error('react-stack-top-frame'); + if (fakeStack.stack.startsWith('Error: react-stack-top-frame')) { + // Looks like V8 + fakeStack.stack = + 'Error: react-stack-top-frame\n' + + // Add two frames since we always trim one off the top. + ' at Client Component Bundle (' + + href + + ':1:1)\n' + + ' at Client Component Bundle (' + + href + + ':1:1)'; + } else { + // Looks like Firefox or Safari. + // Add two frames since we always trim one off the top. + fakeStack.stack = + 'Client Component Bundle@' + + href + + ':1:1\n' + + 'Client Component Bundle@' + + href + + ':1:1'; + } + ioInfo = ({ + name: 'script', + start: start, + end: end, + value: value, + debugStack: fakeStack, + }: ReactIOInfo); + if (byteSize > 0) { + // $FlowFixMe[cannot-write] + ioInfo.byteSize = byteSize; + } + chunkIOInfoCache.set(chunkId, ioInfo); + } + // We could dedupe the async info too but conceptually each request is its own await. + const asyncInfo: ReactAsyncInfo = { + awaited: ioInfo, + }; + target.push(asyncInfo); +} diff --git a/packages/react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspackServer.js b/packages/react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspackServer.js new file mode 100644 index 00000000000..7dcbdf3fb2b --- /dev/null +++ b/packages/react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspackServer.js @@ -0,0 +1,23 @@ +/** + * 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 {ReactDebugInfo} from 'shared/ReactTypes'; + +export function loadChunk(chunkId: string, filename: string): Promise { + return __webpack_chunk_load__(chunkId); +} + +export function addChunkDebugInfo( + target: ReactDebugInfo, + chunkId: string, + filename: string, +): void { + // We don't emit any debug info on the server since we assume the loading + // of the bundle is insignificant on the server. +} diff --git a/packages/react-server-dom-rspack/src/client/ReactFlightClientConfigTargetRspackBrowser.js b/packages/react-server-dom-rspack/src/client/ReactFlightClientConfigTargetRspackBrowser.js new file mode 100644 index 00000000000..60b9e87dbea --- /dev/null +++ b/packages/react-server-dom-rspack/src/client/ReactFlightClientConfigTargetRspackBrowser.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type ModuleLoading = null; + +export function prepareDestinationWithChunks( + moduleLoading: ModuleLoading, + chunks: mixed, + nonce: ?string, +) { + // In the browser we don't need to prepare our destination since the browser is the Destination +} diff --git a/packages/react-server-dom-rspack/src/client/ReactFlightClientConfigTargetRspackServer.js b/packages/react-server-dom-rspack/src/client/ReactFlightClientConfigTargetRspackServer.js new file mode 100644 index 00000000000..f5793fdab43 --- /dev/null +++ b/packages/react-server-dom-rspack/src/client/ReactFlightClientConfigTargetRspackServer.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {preinitScriptForSSR} from 'react-client/src/ReactFlightClientConfig'; + +export type ModuleLoading = null | { + prefix: string, + crossOrigin?: 'use-credentials' | '', +}; + +export function prepareDestinationWithChunks( + moduleLoading: ModuleLoading, + // Chunks are double-indexed [..., idx, filenamex, idy, filenamey, ...] + chunks: Array, + nonce: ?string, +) { + if (moduleLoading !== null) { + for (let i = 1; i < chunks.length; i += 2) { + preinitScriptForSSR( + moduleLoading.prefix + chunks[i], + nonce, + moduleLoading.crossOrigin, + ); + } + } +} diff --git a/packages/react-server-dom-rspack/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-rspack/src/client/ReactFlightDOMClientBrowser.js new file mode 100644 index 00000000000..7a6a7edd915 --- /dev/null +++ b/packages/react-server-dom-rspack/src/client/ReactFlightDOMClientBrowser.js @@ -0,0 +1,330 @@ +/** + * 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 { + DebugChannel, + DebugChannelCallback, + FindSourceMapURLCallback, + Response as FlightResponse, +} from 'react-client/src/ReactFlightClient'; + +import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient'; + +import { + createResponse, + createStreamState, + getRoot, + reportGlobalError, + processBinaryChunk, + processStringChunk, + close, + injectIntoDevTools, +} from 'react-client/src/ReactFlightClient'; + +import { + processReply, + createServerReference as createServerReferenceImpl, +} from 'react-client/src/ReactFlightReplyClient'; + +import type {ServerReferenceId} from '../client/ReactFlightClientConfigBundlerRspack'; + +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + +import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; + +export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; + +export type {TemporaryReferenceSet}; + +function findSourceMapURL(filename: string, environmentName: string) { + const url = new URL('/__rspack_source_map', window.location.origin); + url.searchParams.set('filename', filename); + url.searchParams.set('environmentName', environmentName); + return url.toString(); +} + +type CallServerCallback = (string, args: A) => Promise; + +let callServer: CallServerCallback | null = null; +export function setServerCallback(fn: CallServerCallback) { + callServer = fn; +} + +function callCurrentServerCallback( + id: ServerReferenceId, + args: A, +): Promise { + if (!callServer) { + throw new Error( + 'No server callback has been registered. Call setServerCallback to register one.', + ); + } + return callServer(id, args); +} + +export function createServerReference, T>( + id: string, + exportName: string, +): (...A) => Promise { + return createServerReferenceImpl( + id + '#' + exportName, + callCurrentServerCallback, + undefined, + findSourceMapURL, + exportName, + ); +} + +export type Options = { + debugChannel?: {writable?: WritableStream, readable?: ReadableStream, ...}, + temporaryReferences?: TemporaryReferenceSet, + findSourceMapURL?: FindSourceMapURLCallback, + replayConsoleLogs?: boolean, + environmentName?: string, + startTime?: number, + endTime?: number, +}; + +function createDebugCallbackFromWritableStream( + debugWritable: WritableStream, +): DebugChannelCallback { + const textEncoder = new TextEncoder(); + const writer = debugWritable.getWriter(); + return message => { + if (message === '') { + writer.close(); + } else { + // Note: It's important that this function doesn't close over the Response object or it can't be GC:ed. + // Therefore, we can't report errors from this write back to the Response object. + if (__DEV__) { + writer.write(textEncoder.encode(message + '\n')).catch(console.error); + } + } + }; +} + +function createResponseFromOptions(options: void | Options) { + const debugChannel: void | DebugChannel = + __DEV__ && options && options.debugChannel !== undefined + ? { + hasReadable: options.debugChannel.readable !== undefined, + callback: + options.debugChannel.writable !== undefined + ? createDebugCallbackFromWritableStream( + options.debugChannel.writable, + ) + : null, + } + : undefined; + + return createResponse( + null, + null, + null, + callCurrentServerCallback, + undefined, // encodeFormAction + undefined, // nonce + options && options.temporaryReferences + ? options.temporaryReferences + : undefined, + __DEV__ && options && options.findSourceMapURL + ? options.findSourceMapURL + : undefined, + __DEV__ ? (options ? options.replayConsoleLogs !== false : true) : false, // defaults to true + __DEV__ && options && options.environmentName + ? options.environmentName + : undefined, + __DEV__ && options && options.startTime != null + ? options.startTime + : undefined, + __DEV__ && options && options.endTime != null ? options.endTime : undefined, + debugChannel, + ); +} + +function startReadingFromUniversalStream( + response: FlightResponse, + stream: ReadableStream, + onDone: () => void, +): void { + // This is the same as startReadingFromStream except this allows WebSocketStreams which + // return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially + // always allow streams with variable chunk types. + const streamState = createStreamState(response, stream); + const reader = stream.getReader(); + function progress({ + done, + value, + }: { + done: boolean, + value: any, + ... + }): void | Promise { + if (done) { + return onDone(); + } + if (value instanceof ArrayBuffer) { + // WebSockets can produce ArrayBuffer values in ReadableStreams. + processBinaryChunk(response, streamState, new Uint8Array(value)); + } else if (typeof value === 'string') { + // WebSockets can produce string values in ReadableStreams. + processStringChunk(response, streamState, value); + } else { + processBinaryChunk(response, streamState, value); + } + return reader.read().then(progress).catch(error); + } + function error(e: any) { + reportGlobalError(response, e); + } + reader.read().then(progress).catch(error); +} + +function startReadingFromStream( + response: FlightResponse, + stream: ReadableStream, + onDone: () => void, + debugValue: mixed, +): void { + const streamState = createStreamState(response, debugValue); + const reader = stream.getReader(); + function progress({ + done, + value, + }: { + done: boolean, + value: ?any, + ... + }): void | Promise { + if (done) { + return onDone(); + } + const buffer: Uint8Array = (value: any); + processBinaryChunk(response, streamState, 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); + if ( + __DEV__ && + options && + options.debugChannel && + options.debugChannel.readable + ) { + let streamDoneCount = 0; + const handleDone = () => { + if (++streamDoneCount === 2) { + close(response); + } + }; + startReadingFromUniversalStream( + response, + options.debugChannel.readable, + handleDone, + ); + startReadingFromStream(response, stream, handleDone, stream); + } else { + startReadingFromStream( + response, + stream, + close.bind(null, response), + stream, + ); + } + return getRoot(response); +} + +function createFromFetch( + promiseForResponse: Promise, + options?: Options, +): Thenable { + const response: FlightResponse = createResponseFromOptions(options); + promiseForResponse.then( + function (r) { + if ( + __DEV__ && + options && + options.debugChannel && + options.debugChannel.readable + ) { + let streamDoneCount = 0; + const handleDone = () => { + if (++streamDoneCount === 2) { + close(response); + } + }; + startReadingFromUniversalStream( + response, + options.debugChannel.readable, + handleDone, + ); + startReadingFromStream(response, (r.body: any), handleDone, r); + } else { + startReadingFromStream( + response, + (r.body: any), + close.bind(null, response), + r, + ); + } + }, + function (e) { + reportGlobalError(response, e); + }, + ); + return getRoot(response); +} + +function encodeReply( + value: ReactServerValue, + options?: {temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal}, +): Promise< + string | URLSearchParams | FormData, +> /* We don't use URLSearchParams yet but maybe */ { + return new Promise((resolve, reject) => { + const abort = processReply( + value, + '', + options && options.temporaryReferences + ? options.temporaryReferences + : undefined, + resolve, + reject, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort((signal: any).reason); + } else { + const listener = () => { + abort((signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + }); +} + +export {createFromFetch, createFromReadableStream, encodeReply}; + +if (__DEV__) { + injectIntoDevTools(); +} diff --git a/packages/react-server-dom-rspack/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-rspack/src/client/ReactFlightDOMClientEdge.js new file mode 100644 index 00000000000..7ce1cd039d1 --- /dev/null +++ b/packages/react-server-dom-rspack/src/client/ReactFlightDOMClientEdge.js @@ -0,0 +1,254 @@ +/** + * 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, ReactCustomFormAction} from 'shared/ReactTypes.js'; + +import type { + DebugChannel, + FindSourceMapURLCallback, + Response as FlightResponse, +} from 'react-client/src/ReactFlightClient'; + +import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient'; + +import type { + ServerConsumerModuleMap, + ModuleLoading, + ServerManifest, +} from 'react-client/src/ReactFlightClientConfig'; + +import { + createResponse, + createStreamState, + getRoot, + reportGlobalError, + processBinaryChunk, + close, +} from 'react-client/src/ReactFlightClient'; + +import { + processReply, + createServerReference as createServerReferenceImpl, +} from 'react-client/src/ReactFlightReplyClient'; + +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + +import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; + +export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; + +export type {TemporaryReferenceSet}; + +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); +} + +type EncodeFormActionCallback = ( + id: any, + args: Promise, +) => ReactCustomFormAction; + +export type Options = { + nonce?: string, + encodeFormAction?: EncodeFormActionCallback, + temporaryReferences?: TemporaryReferenceSet, + findSourceMapURL?: FindSourceMapURLCallback, + replayConsoleLogs?: boolean, + environmentName?: string, + startTime?: number, + endTime?: number, + // For the Edge client we only support a single-direction debug channel. + debugChannel?: {readable?: ReadableStream, ...}, +}; + +declare const __rspack_rsc_manifest__: { + serverConsumerModuleMap: ServerConsumerModuleMap, + moduleLoading: ModuleLoading, + serverManifest: ServerManifest, +}; + +function createResponseFromOptions(options: Options) { + const debugChannel: void | DebugChannel = + __DEV__ && options && options.debugChannel !== undefined + ? { + hasReadable: options.debugChannel.readable !== undefined, + callback: null, + } + : undefined; + + return createResponse( + __rspack_rsc_manifest__.serverConsumerModuleMap, + __rspack_rsc_manifest__.serverManifest, + __rspack_rsc_manifest__.moduleLoading, + noServerCall, + options.encodeFormAction, + typeof options.nonce === 'string' ? options.nonce : undefined, + options && options.temporaryReferences + ? options.temporaryReferences + : undefined, + __DEV__ && options && options.findSourceMapURL + ? options.findSourceMapURL + : undefined, + __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false + __DEV__ && options && options.environmentName + ? options.environmentName + : undefined, + __DEV__ && options && options.startTime != null + ? options.startTime + : undefined, + __DEV__ && options && options.endTime != null ? options.endTime : undefined, + debugChannel, + ); +} + +function startReadingFromStream( + response: FlightResponse, + stream: ReadableStream, + onDone: () => void, + debugValue: mixed, +): void { + const streamState = createStreamState(response, debugValue); + const reader = stream.getReader(); + function progress({ + done, + value, + }: { + done: boolean, + value: ?any, + ... + }): void | Promise { + if (done) { + return onDone(); + } + const buffer: Uint8Array = (value: any); + processBinaryChunk(response, streamState, 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); + + if ( + __DEV__ && + options && + options.debugChannel && + options.debugChannel.readable + ) { + let streamDoneCount = 0; + const handleDone = () => { + if (++streamDoneCount === 2) { + close(response); + } + }; + startReadingFromStream(response, options.debugChannel.readable, handleDone); + startReadingFromStream(response, stream, handleDone, stream); + } else { + startReadingFromStream( + response, + stream, + close.bind(null, response), + stream, + ); + } + + return getRoot(response); +} + +function createFromFetch( + promiseForResponse: Promise, + options?: Options = {}, +): Thenable { + const response: FlightResponse = createResponseFromOptions(options); + promiseForResponse.then( + function (r) { + if ( + __DEV__ && + options && + options.debugChannel && + options.debugChannel.readable + ) { + let streamDoneCount = 0; + const handleDone = () => { + if (++streamDoneCount === 2) { + close(response); + } + }; + startReadingFromStream( + response, + options.debugChannel.readable, + handleDone, + ); + startReadingFromStream(response, (r.body: any), handleDone, r); + } else { + startReadingFromStream( + response, + (r.body: any), + close.bind(null, response), + r, + ); + } + }, + function (e) { + reportGlobalError(response, e); + }, + ); + return getRoot(response); +} + +function encodeReply( + value: ReactServerValue, + options?: {temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal}, +): Promise< + string | URLSearchParams | FormData, +> /* We don't use URLSearchParams yet but maybe */ { + return new Promise((resolve, reject) => { + const abort = processReply( + value, + '', + options && options.temporaryReferences + ? options.temporaryReferences + : undefined, + resolve, + reject, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort((signal: any).reason); + } else { + const listener = () => { + abort((signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + }); +} + +export {createFromFetch, createFromReadableStream, encodeReply}; diff --git a/packages/react-server-dom-rspack/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-rspack/src/client/ReactFlightDOMClientNode.js new file mode 100644 index 00000000000..42741ea6ec0 --- /dev/null +++ b/packages/react-server-dom-rspack/src/client/ReactFlightDOMClientNode.js @@ -0,0 +1,145 @@ +/** + * 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, ReactCustomFormAction} from 'shared/ReactTypes.js'; + +import type { + DebugChannel, + FindSourceMapURLCallback, + Response, +} from 'react-client/src/ReactFlightClient'; + +import type { + ServerConsumerModuleMap, + ModuleLoading, + ServerManifest, +} from 'react-client/src/ReactFlightClientConfig'; + +type ServerConsumerManifest = { + moduleMap: ServerConsumerModuleMap, + moduleLoading: ModuleLoading, + serverModuleMap: null | ServerManifest, +}; + +import type {Readable} from 'stream'; + +import { + createResponse, + createStreamState, + getRoot, + reportGlobalError, + processStringChunk, + processBinaryChunk, + close, +} from 'react-client/src/ReactFlightClient'; + +export * from './ReactFlightDOMClientEdge'; + +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.', + ); +} + +type EncodeFormActionCallback = ( + id: any, + args: Promise, +) => ReactCustomFormAction; + +export type Options = { + nonce?: string, + encodeFormAction?: EncodeFormActionCallback, + findSourceMapURL?: FindSourceMapURLCallback, + replayConsoleLogs?: boolean, + environmentName?: string, + startTime?: number, + endTime?: number, + // For the Node.js client we only support a single-direction debug channel. + debugChannel?: Readable, +}; + +function startReadingFromStream( + response: Response, + stream: Readable, + onEnd: () => void, +): void { + const streamState = createStreamState(response, stream); + + stream.on('data', chunk => { + if (typeof chunk === 'string') { + processStringChunk(response, streamState, chunk); + } else { + processBinaryChunk(response, streamState, chunk); + } + }); + + stream.on('error', error => { + reportGlobalError(response, error); + }); + + stream.on('end', onEnd); +} + +declare const __rspack_rsc_manifest__: { + serverConsumerModuleMap: ServerConsumerModuleMap, + moduleLoading: ModuleLoading, + serverManifest: ServerManifest, +}; + +function createFromNodeStream( + stream: Readable, + serverConsumerManifest: ServerConsumerManifest, + options?: Options, +): Thenable { + const debugChannel: void | DebugChannel = + __DEV__ && options && options.debugChannel !== undefined + ? {hasReadable: true, callback: null} + : undefined; + + const response: Response = createResponse( + __rspack_rsc_manifest__.serverConsumerModuleMap, + __rspack_rsc_manifest__.serverManifest, + __rspack_rsc_manifest__.moduleLoading, + noServerCall, + options ? options.encodeFormAction : undefined, + options && typeof options.nonce === 'string' ? options.nonce : undefined, + undefined, // TODO: If encodeReply is supported, this should support temporaryReferences + __DEV__ && options && options.findSourceMapURL + ? options.findSourceMapURL + : undefined, + __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false + __DEV__ && options && options.environmentName + ? options.environmentName + : undefined, + __DEV__ && options && options.startTime != null + ? options.startTime + : undefined, + __DEV__ && options && options.endTime != null ? options.endTime : undefined, + debugChannel, + ); + + if (__DEV__ && options && options.debugChannel) { + let streamEndedCount = 0; + const handleEnd = () => { + if (++streamEndedCount === 2) { + close(response); + } + }; + startReadingFromStream(response, options.debugChannel, handleEnd); + startReadingFromStream(response, stream, handleEnd); + } else { + startReadingFromStream(response, stream, close.bind(null, response)); + } + + return getRoot(response); +} + +export {createFromNodeStream}; diff --git a/packages/react-server-dom-rspack/src/client/react-flight-dom-client.browser.js b/packages/react-server-dom-rspack/src/client/react-flight-dom-client.browser.js new file mode 100644 index 00000000000..a3f15d0116c --- /dev/null +++ b/packages/react-server-dom-rspack/src/client/react-flight-dom-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 './ReactFlightDOMClientBrowser'; diff --git a/packages/react-server-dom-rspack/src/client/react-flight-dom-client.edge.js b/packages/react-server-dom-rspack/src/client/react-flight-dom-client.edge.js new file mode 100644 index 00000000000..14a8876953b --- /dev/null +++ b/packages/react-server-dom-rspack/src/client/react-flight-dom-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 './ReactFlightDOMClientEdge'; diff --git a/packages/react-server-dom-rspack/src/client/react-flight-dom-client.node.js b/packages/react-server-dom-rspack/src/client/react-flight-dom-client.node.js new file mode 100644 index 00000000000..8eb9daa35b6 --- /dev/null +++ b/packages/react-server-dom-rspack/src/client/react-flight-dom-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 './ReactFlightDOMClientNode'; diff --git a/packages/react-server-dom-rspack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-rspack/src/server/ReactFlightDOMServerBrowser.js new file mode 100644 index 00000000000..13177a0b1f6 --- /dev/null +++ b/packages/react-server-dom-rspack/src/server/ReactFlightDOMServerBrowser.js @@ -0,0 +1,285 @@ +/** + * 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 {Thenable} from 'shared/ReactTypes'; +import { + type ClientManifest, + setServerActionBoundArgsEncryption, + encryptServerActionBoundArgs, + decryptServerActionBoundArgs, + loadServerAction, + createServerEntry, + ensureServerActions, +} from './ReactFlightServerConfigRspackBundler'; +import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; + +import { + createRequest, + createPrerenderRequest, + startWork, + startFlowing, + startFlowingDebug, + stopFlowing, + abort, + resolveDebugMessage, + closeDebugChannel, +} from 'react-server/src/ReactFlightServer'; + +import { + createResponse, + close, + getRoot, +} from 'react-server/src/ReactFlightReplyServer'; + +import { + decodeAction, + decodeFormState, +} from 'react-server/src/ReactFlightActionServer'; + +export { + registerServerReference, + registerClientReference, +} from '../ReactFlightRspackReferences'; + +import { + createStringDecoder, + readPartialStringChunk, + readFinalStringChunk, +} from 'react-client/src/ReactFlightClientStreamConfigWeb'; + +import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences'; + +export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences'; + +export type {TemporaryReferenceSet}; + +type Options = { + debugChannel?: {readable?: ReadableStream, writable?: WritableStream, ...}, + environmentName?: string | (() => string), + filterStackFrame?: (url: string, functionName: string) => boolean, + identifierPrefix?: string, + signal?: AbortSignal, + temporaryReferences?: TemporaryReferenceSet, + onError?: (error: mixed) => void, +}; + +function startReadingFromDebugChannelReadableStream( + request: Request, + stream: ReadableStream, +): void { + const reader = stream.getReader(); + const stringDecoder = createStringDecoder(); + let stringBuffer = ''; + function progress({ + done, + value, + }: { + done: boolean, + value: ?any, + ... + }): void | Promise { + const buffer: Uint8Array = (value: any); + stringBuffer += done + ? readFinalStringChunk(stringDecoder, new Uint8Array(0)) + : readPartialStringChunk(stringDecoder, buffer); + const messages = stringBuffer.split('\n'); + for (let i = 0; i < messages.length - 1; i++) { + resolveDebugMessage(request, messages[i]); + } + stringBuffer = messages[messages.length - 1]; + if (done) { + closeDebugChannel(request); + return; + } + return reader.read().then(progress).catch(error); + } + function error(e: any) { + abort( + request, + new Error('Lost connection to the Debug Channel.', { + cause: e, + }), + ); + } + reader.read().then(progress).catch(error); +} + +declare const __rspack_rsc_manifest__: { + clientManifest: ClientManifest, + serverManifest: ServerManifest, +}; + +function renderToReadableStream( + model: ReactClientValue, + options?: Options, +): ReadableStream { + const debugChannelReadable = + __DEV__ && options && options.debugChannel + ? options.debugChannel.readable + : undefined; + const debugChannelWritable = + __DEV__ && options && options.debugChannel + ? options.debugChannel.writable + : undefined; + const request = createRequest( + model, + __rspack_rsc_manifest__.clientManifest, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + debugChannelReadable !== 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); + } + } + if (debugChannelWritable !== undefined) { + const debugStream = new ReadableStream( + { + type: 'bytes', + pull: (controller): ?Promise => { + startFlowingDebug(request, controller); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + debugStream.pipeTo(debugChannelWritable); + } + if (debugChannelReadable !== undefined) { + startReadingFromDebugChannelReadableStream(request, debugChannelReadable); + } + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + startWork(request); + }, + pull: (controller): ?Promise => { + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => { + stopFlowing(request); + abort(request, reason); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + return stream; +} + +type StaticResult = { + prelude: ReadableStream, +}; + +function prerender( + model: ReactClientValue, + options?: Options, +): Promise { + return new Promise((resolve, reject) => { + const onFatalError = reject; + function onAllReady() { + const stream = new ReadableStream( + { + type: 'bytes', + pull: (controller): ?Promise => { + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => { + stopFlowing(request); + abort(request, reason); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + resolve({prelude: stream}); + } + const request = createPrerenderRequest( + model, + __rspack_rsc_manifest__.clientManifest, + onAllReady, + onFatalError, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + false, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + const reason = (signal: any).reason; + abort(request, reason); + } else { + const listener = () => { + const reason = (signal: any).reason; + abort(request, reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + startWork(request); + }); +} + +function decodeReply( + body: string | FormData, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + if (typeof body === 'string') { + const form = new FormData(); + form.append('0', body); + body = form; + } + const response = createResponse( + __rspack_rsc_manifest__.serverManifest, + '', + options ? options.temporaryReferences : undefined, + body, + ); + const root = getRoot(response); + close(response); + return root; +} + +export { + renderToReadableStream, + prerender, + decodeReply, + decodeAction, + decodeFormState, + + // server action bound args encryption + setServerActionBoundArgsEncryption, + encryptServerActionBoundArgs, + decryptServerActionBoundArgs, + + // Rspack specific + loadServerAction, + createServerEntry, + ensureServerActions, +}; diff --git a/packages/react-server-dom-rspack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-rspack/src/server/ReactFlightDOMServerEdge.js new file mode 100644 index 00000000000..c85ccb5834c --- /dev/null +++ b/packages/react-server-dom-rspack/src/server/ReactFlightDOMServerEdge.js @@ -0,0 +1,335 @@ +/** + * 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 {Thenable} from 'shared/ReactTypes'; +import { + type ClientManifest, + setServerActionBoundArgsEncryption, + encryptServerActionBoundArgs, + decryptServerActionBoundArgs, + loadServerAction, + createServerEntry, + ensureServerActions, +} from './ReactFlightServerConfigRspackBundler'; +import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; + +import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; + +import { + createRequest, + createPrerenderRequest, + startWork, + startFlowing, + startFlowingDebug, + stopFlowing, + abort, + resolveDebugMessage, + closeDebugChannel, +} from 'react-server/src/ReactFlightServer'; + +import { + createResponse, + close, + getRoot, + reportGlobalError, + resolveField, + resolveFile, +} from 'react-server/src/ReactFlightReplyServer'; + +import { + decodeAction, + decodeFormState, +} from 'react-server/src/ReactFlightActionServer'; + +export { + registerServerReference, + registerClientReference, +} from '../ReactFlightRspackReferences'; + +import { + createStringDecoder, + readPartialStringChunk, + readFinalStringChunk, +} from 'react-client/src/ReactFlightClientStreamConfigWeb'; + +import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences'; + +export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences'; + +export type {TemporaryReferenceSet}; + +type Options = { + debugChannel?: {readable?: ReadableStream, writable?: WritableStream, ...}, + environmentName?: string | (() => string), + filterStackFrame?: (url: string, functionName: string) => boolean, + identifierPrefix?: string, + signal?: AbortSignal, + temporaryReferences?: TemporaryReferenceSet, + onError?: (error: mixed) => void, +}; + +function startReadingFromDebugChannelReadableStream( + request: Request, + stream: ReadableStream, +): void { + const reader = stream.getReader(); + const stringDecoder = createStringDecoder(); + let stringBuffer = ''; + function progress({ + done, + value, + }: { + done: boolean, + value: ?any, + ... + }): void | Promise { + const buffer: Uint8Array = (value: any); + stringBuffer += done + ? readFinalStringChunk(stringDecoder, new Uint8Array(0)) + : readPartialStringChunk(stringDecoder, buffer); + const messages = stringBuffer.split('\n'); + for (let i = 0; i < messages.length - 1; i++) { + resolveDebugMessage(request, messages[i]); + } + stringBuffer = messages[messages.length - 1]; + if (done) { + closeDebugChannel(request); + return; + } + return reader.read().then(progress).catch(error); + } + function error(e: any) { + abort( + request, + new Error('Lost connection to the Debug Channel.', { + cause: e, + }), + ); + } + reader.read().then(progress).catch(error); +} + +declare const __rspack_rsc_manifest__: { + clientManifest: ClientManifest, + serverManifest: ServerManifest, +}; + +function renderToReadableStream( + model: ReactClientValue, + options?: Options, +): ReadableStream { + const debugChannelReadable = + __DEV__ && options && options.debugChannel + ? options.debugChannel.readable + : undefined; + const debugChannelWritable = + __DEV__ && options && options.debugChannel + ? options.debugChannel.writable + : undefined; + const request = createRequest( + model, + __rspack_rsc_manifest__.clientManifest, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + debugChannelReadable !== 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); + } + } + if (debugChannelWritable !== undefined) { + const debugStream = new ReadableStream( + { + type: 'bytes', + pull: (controller): ?Promise => { + startFlowingDebug(request, controller); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + debugStream.pipeTo(debugChannelWritable); + } + if (debugChannelReadable !== undefined) { + startReadingFromDebugChannelReadableStream(request, debugChannelReadable); + } + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + startWork(request); + }, + pull: (controller): ?Promise => { + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => { + stopFlowing(request); + abort(request, reason); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + return stream; +} + +type StaticResult = { + prelude: ReadableStream, +}; + +function prerender( + model: ReactClientValue, + options?: Options, +): Promise { + return new Promise((resolve, reject) => { + const onFatalError = reject; + function onAllReady() { + const stream = new ReadableStream( + { + type: 'bytes', + pull: (controller): ?Promise => { + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => { + stopFlowing(request); + abort(request, reason); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + resolve({prelude: stream}); + } + const request = createPrerenderRequest( + model, + __rspack_rsc_manifest__.clientManifest, + onAllReady, + onFatalError, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + false, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + const reason = (signal: any).reason; + abort(request, reason); + } else { + const listener = () => { + const reason = (signal: any).reason; + abort(request, reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + startWork(request); + }); +} + +function decodeReply( + body: string | FormData, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + if (typeof body === 'string') { + const form = new FormData(); + form.append('0', body); + body = form; + } + const response = createResponse( + __rspack_rsc_manifest__.serverManifest, + '', + options ? options.temporaryReferences : undefined, + body, + ); + const root = getRoot(response); + close(response); + return root; +} + +function decodeReplyFromAsyncIterable( + iterable: AsyncIterable<[string, string | File]>, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + const iterator: AsyncIterator<[string, string | File]> = + iterable[ASYNC_ITERATOR](); + + const response = createResponse( + __rspack_rsc_manifest__.serverManifest, + '', + options ? options.temporaryReferences : undefined, + ); + + function progress( + entry: + | {done: false, +value: [string, string | File], ...} + | {done: true, +value: void, ...}, + ) { + if (entry.done) { + close(response); + } else { + const [name, value] = entry.value; + if (typeof value === 'string') { + resolveField(response, name, value); + } else { + resolveFile(response, name, value); + } + iterator.next().then(progress, error); + } + } + function error(reason: Error) { + reportGlobalError(response, reason); + if (typeof (iterator: any).throw === 'function') { + // The iterator protocol doesn't necessarily include this but a generator do. + // $FlowFixMe should be able to pass mixed + iterator.throw(reason).then(error, error); + } + } + + iterator.next().then(progress, error); + + return getRoot(response); +} + +export { + renderToReadableStream, + prerender, + decodeReply, + decodeReplyFromAsyncIterable, + decodeAction, + decodeFormState, + + // server action bound args encryption + setServerActionBoundArgsEncryption, + encryptServerActionBoundArgs, + decryptServerActionBoundArgs, + + // Rspack specific + loadServerAction, + createServerEntry, + ensureServerActions, +}; diff --git a/packages/react-server-dom-rspack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-rspack/src/server/ReactFlightDOMServerNode.js new file mode 100644 index 00000000000..6a64876d981 --- /dev/null +++ b/packages/react-server-dom-rspack/src/server/ReactFlightDOMServerNode.js @@ -0,0 +1,710 @@ +/** + * 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 {ServerManifest} from 'react-client/src/forks/ReactFlightClientConfig.dom-node-rspack'; +import { + type ClientManifest, + setServerActionBoundArgsEncryption, + encryptServerActionBoundArgs, + decryptServerActionBoundArgs, + loadServerAction, + createServerEntry, + ensureServerActions, +} from './ReactFlightServerConfigRspackBundler'; +import type {Busboy} from 'busboy'; +import type {Writable} from 'stream'; +import type {Thenable} from 'shared/ReactTypes'; + +import type {Duplex} from 'stream'; + +import {Readable} from 'stream'; + +import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; + +import { + createRequest, + createPrerenderRequest, + startWork, + startFlowing, + startFlowingDebug, + stopFlowing, + abort, + resolveDebugMessage, + closeDebugChannel, +} from 'react-server/src/ReactFlightServer'; + +import { + createResponse, + reportGlobalError, + close, + resolveField, + resolveFile, + resolveFileInfo, + resolveFileChunk, + resolveFileComplete, + getRoot, +} from 'react-server/src/ReactFlightReplyServer'; + +import { + decodeAction, + decodeFormState, +} from 'react-server/src/ReactFlightActionServer'; + +export { + registerServerReference, + registerClientReference, +} from '../ReactFlightRspackReferences'; + +import { + createStringDecoder, + readPartialStringChunk, + readFinalStringChunk, +} from 'react-client/src/ReactFlightClientStreamConfigNode'; + +import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode'; + +import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences'; + +export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences'; + +export type {TemporaryReferenceSet}; + +function createDrainHandler(destination: Destination, request: Request) { + return () => startFlowing(request, destination); +} + +function createCancelHandler(request: Request, reason: string) { + return () => { + stopFlowing(request); + abort(request, new Error(reason)); + }; +} + +function startReadingFromDebugChannelReadable( + request: Request, + stream: Readable | WebSocket, +): void { + const stringDecoder = createStringDecoder(); + let lastWasPartial = false; + let stringBuffer = ''; + function onData(chunk: string | Uint8Array) { + if (typeof chunk === 'string') { + if (lastWasPartial) { + stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0)); + lastWasPartial = false; + } + stringBuffer += chunk; + } else { + const buffer: Uint8Array = (chunk: any); + stringBuffer += readPartialStringChunk(stringDecoder, buffer); + lastWasPartial = true; + } + const messages = stringBuffer.split('\n'); + for (let i = 0; i < messages.length - 1; i++) { + resolveDebugMessage(request, messages[i]); + } + stringBuffer = messages[messages.length - 1]; + } + function onError(error: mixed) { + abort( + request, + new Error('Lost connection to the Debug Channel.', { + cause: error, + }), + ); + } + function onClose() { + closeDebugChannel(request); + } + if ( + // $FlowFixMe[method-unbinding] + typeof stream.addEventListener === 'function' && + // $FlowFixMe[method-unbinding] + typeof stream.binaryType === 'string' + ) { + const ws: WebSocket = (stream: any); + ws.binaryType = 'arraybuffer'; + ws.addEventListener('message', event => { + // $FlowFixMe + onData(event.data); + }); + ws.addEventListener('error', event => { + // $FlowFixMe + onError(event.error); + }); + ws.addEventListener('close', onClose); + } else { + const readable: Readable = (stream: any); + readable.on('data', onData); + readable.on('error', onError); + readable.on('end', onClose); + } +} + +type Options = { + debugChannel?: Readable | Writable | Duplex | WebSocket, + environmentName?: string | (() => string), + filterStackFrame?: (url: string, functionName: string) => boolean, + onError?: (error: mixed) => void, + identifierPrefix?: string, + temporaryReferences?: TemporaryReferenceSet, +}; + +type PipeableStream = { + abort(reason: mixed): void, + pipe(destination: T): T, +}; + +function renderToPipeableStream( + model: ReactClientValue, + options?: Options, +): PipeableStream { + const debugChannel = __DEV__ && options ? options.debugChannel : undefined; + const debugChannelReadable: void | Readable | WebSocket = + __DEV__ && + debugChannel !== undefined && + // $FlowFixMe[method-unbinding] + (typeof debugChannel.read === 'function' || + typeof debugChannel.readyState === 'number') + ? (debugChannel: any) + : undefined; + const debugChannelWritable: void | Writable = + __DEV__ && debugChannel !== undefined + ? // $FlowFixMe[method-unbinding] + typeof debugChannel.write === 'function' + ? (debugChannel: any) + : // $FlowFixMe[method-unbinding] + typeof debugChannel.send === 'function' + ? createFakeWritableFromWebSocket((debugChannel: any)) + : undefined + : undefined; + const request = createRequest( + model, + __rspack_rsc_manifest__.clientManifest, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + debugChannelReadable !== undefined, + ); + let hasStartedFlowing = false; + startWork(request); + if (debugChannelWritable !== undefined) { + startFlowingDebug(request, debugChannelWritable); + } + if (debugChannelReadable !== undefined) { + startReadingFromDebugChannelReadable(request, debugChannelReadable); + } + 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)); + destination.on( + 'error', + createCancelHandler( + request, + 'The destination stream errored while writing data.', + ), + ); + // We don't close until the debug channel closes. + if (!__DEV__ || debugChannelReadable === undefined) { + destination.on( + 'close', + createCancelHandler(request, 'The destination stream closed early.'), + ); + } + return destination; + }, + abort(reason: mixed) { + abort(request, reason); + }, + }; +} + +function createFakeWritableFromWebSocket(webSocket: WebSocket): Writable { + return ({ + write(chunk: string | Uint8Array) { + webSocket.send((chunk: any)); + return true; + }, + end() { + webSocket.close(); + }, + destroy(reason) { + if (typeof reason === 'object' && reason !== null) { + reason = reason.message; + } + if (typeof reason === 'string') { + webSocket.close(1011, reason); + } else { + webSocket.close(1011); + } + }, + }: any); +} + +function createFakeWritableFromReadableStreamController( + controller: ReadableStreamController, +): Writable { + // The current host config expects a Writable so we create + // a fake writable for now to push into the Readable. + return ({ + write(chunk: string | Uint8Array) { + if (typeof chunk === 'string') { + chunk = textEncoder.encode(chunk); + } + controller.enqueue(chunk); + // in web streams there is no backpressure so we can always write more + return true; + }, + end() { + controller.close(); + }, + destroy(error) { + // $FlowFixMe[method-unbinding] + if (typeof controller.error === 'function') { + // $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types. + controller.error(error); + } else { + controller.close(); + } + }, + }: any); +} + +function startReadingFromDebugChannelReadableStream( + request: Request, + stream: ReadableStream, +): void { + const reader = stream.getReader(); + const stringDecoder = createStringDecoder(); + let stringBuffer = ''; + function progress({ + done, + value, + }: { + done: boolean, + value: ?any, + ... + }): void | Promise { + const buffer: Uint8Array = (value: any); + stringBuffer += done + ? readFinalStringChunk(stringDecoder, new Uint8Array(0)) + : readPartialStringChunk(stringDecoder, buffer); + const messages = stringBuffer.split('\n'); + for (let i = 0; i < messages.length - 1; i++) { + resolveDebugMessage(request, messages[i]); + } + stringBuffer = messages[messages.length - 1]; + if (done) { + closeDebugChannel(request); + return; + } + return reader.read().then(progress).catch(error); + } + function error(e: any) { + abort( + request, + new Error('Lost connection to the Debug Channel.', { + cause: e, + }), + ); + } + reader.read().then(progress).catch(error); +} + +declare const __rspack_rsc_manifest__: { + clientManifest: ClientManifest, + serverManifest: ServerManifest, +}; + +function renderToReadableStream( + model: ReactClientValue, + options?: Omit & { + debugChannel?: {readable?: ReadableStream, writable?: WritableStream, ...}, + signal?: AbortSignal, + }, +): ReadableStream { + const debugChannelReadable = + __DEV__ && options && options.debugChannel + ? options.debugChannel.readable + : undefined; + const debugChannelWritable = + __DEV__ && options && options.debugChannel + ? options.debugChannel.writable + : undefined; + const request = createRequest( + model, + __rspack_rsc_manifest__.clientManifest, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + debugChannelReadable !== 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); + } + } + if (debugChannelWritable !== undefined) { + let debugWritable: Writable; + const debugStream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + debugWritable = + createFakeWritableFromReadableStreamController(controller); + }, + pull: (controller): ?Promise => { + startFlowingDebug(request, debugWritable); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + debugStream.pipeTo(debugChannelWritable); + } + if (debugChannelReadable !== undefined) { + startReadingFromDebugChannelReadableStream(request, debugChannelReadable); + } + let writable: Writable; + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + writable = createFakeWritableFromReadableStreamController(controller); + startWork(request); + }, + pull: (controller): ?Promise => { + startFlowing(request, writable); + }, + cancel: (reason): ?Promise => { + stopFlowing(request); + abort(request, reason); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + return stream; +} + +function createFakeWritableFromNodeReadable(readable: any): Writable { + // The current host config expects a Writable so we create + // a fake writable for now to push into the Readable. + return ({ + write(chunk: string | Uint8Array) { + return readable.push(chunk); + }, + end() { + readable.push(null); + }, + destroy(error) { + readable.destroy(error); + }, + }: any); +} + +type PrerenderOptions = { + environmentName?: string | (() => string), + filterStackFrame?: (url: string, functionName: string) => boolean, + onError?: (error: mixed) => void, + identifierPrefix?: string, + temporaryReferences?: TemporaryReferenceSet, + signal?: AbortSignal, +}; + +type StaticResult = { + prelude: Readable, +}; + +function prerenderToNodeStream( + model: ReactClientValue, + options?: PrerenderOptions, +): Promise { + return new Promise((resolve, reject) => { + const onFatalError = reject; + function onAllReady() { + const readable: Readable = new Readable({ + read() { + startFlowing(request, writable); + }, + }); + const writable = createFakeWritableFromNodeReadable(readable); + resolve({prelude: readable}); + } + + const request = createPrerenderRequest( + model, + __rspack_rsc_manifest__.clientManifest, + onAllReady, + onFatalError, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + false, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + const reason = (signal: any).reason; + abort(request, reason); + } else { + const listener = () => { + const reason = (signal: any).reason; + abort(request, reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + startWork(request); + }); +} + +function prerender( + model: ReactClientValue, + options?: Options & { + signal?: AbortSignal, + }, +): Promise<{ + prelude: ReadableStream, +}> { + return new Promise((resolve, reject) => { + const onFatalError = reject; + function onAllReady() { + let writable: Writable; + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + writable = + createFakeWritableFromReadableStreamController(controller); + }, + pull: (controller): ?Promise => { + startFlowing(request, writable); + }, + cancel: (reason): ?Promise => { + stopFlowing(request); + abort(request, reason); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + resolve({prelude: stream}); + } + const request = createPrerenderRequest( + model, + __rspack_rsc_manifest__.clientManifest, + onAllReady, + onFatalError, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + false, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + const reason = (signal: any).reason; + abort(request, reason); + } else { + const listener = () => { + const reason = (signal: any).reason; + abort(request, reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + startWork(request); + }); +} + +function decodeReplyFromBusboy( + busboyStream: Busboy, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + const response = createResponse( + __rspack_rsc_manifest__.serverManifest, + '', + options ? options.temporaryReferences : undefined, + ); + 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 { + try { + resolveField(response, name, value); + } catch (error) { + busboyStream.destroy(error); + } + } + }); + busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => { + if (encoding.toLowerCase() === 'base64') { + busboyStream.destroy( + 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.', + ), + ); + return; + } + pendingFiles++; + const file = resolveFileInfo(response, name, filename, mimeType); + value.on('data', chunk => { + resolveFileChunk(response, file, chunk); + }); + value.on('end', () => { + try { + 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; + } + } catch (error) { + busboyStream.destroy(error); + } + }); + }); + 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, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + if (typeof body === 'string') { + const form = new FormData(); + form.append('0', body); + body = form; + } + const response = createResponse( + __rspack_rsc_manifest__.serverManifest, + '', + options ? options.temporaryReferences : undefined, + body, + ); + const root = getRoot(response); + close(response); + return root; +} + +function decodeReplyFromAsyncIterable( + iterable: AsyncIterable<[string, string | File]>, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + const iterator: AsyncIterator<[string, string | File]> = + iterable[ASYNC_ITERATOR](); + + const response = createResponse( + __rspack_rsc_manifest__.serverManifest, + '', + options ? options.temporaryReferences : undefined, + ); + + function progress( + entry: + | {done: false, +value: [string, string | File], ...} + | {done: true, +value: void, ...}, + ) { + if (entry.done) { + close(response); + } else { + const [name, value] = entry.value; + if (typeof value === 'string') { + resolveField(response, name, value); + } else { + resolveFile(response, name, value); + } + iterator.next().then(progress, error); + } + } + function error(reason: Error) { + reportGlobalError(response, reason); + if (typeof (iterator: any).throw === 'function') { + // The iterator protocol doesn't necessarily include this but a generator do. + // $FlowFixMe should be able to pass mixed + iterator.throw(reason).then(error, error); + } + } + + iterator.next().then(progress, error); + + return getRoot(response); +} + +export { + renderToReadableStream, + renderToPipeableStream, + prerender, + prerenderToNodeStream, + decodeReply, + decodeReplyFromBusboy, + decodeReplyFromAsyncIterable, + decodeAction, + decodeFormState, + + // server action bound args encryption + setServerActionBoundArgsEncryption, + encryptServerActionBoundArgs, + decryptServerActionBoundArgs, + + // Rspack specific + loadServerAction, + createServerEntry, + ensureServerActions, +}; diff --git a/packages/react-server-dom-rspack/src/server/ReactFlightServerConfigRspackBundler.js b/packages/react-server-dom-rspack/src/server/ReactFlightServerConfigRspackBundler.js new file mode 100644 index 00000000000..ed0e88ff762 --- /dev/null +++ b/packages/react-server-dom-rspack/src/server/ReactFlightServerConfigRspackBundler.js @@ -0,0 +1,208 @@ +/** + * 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 { + ImportMetadata, + ImportManifestEntry, +} from '../shared/ReactFlightImportMetadata'; + +import type { + ClientReference, + ServerReference, +} from '../ReactFlightRspackReferences'; +import type {ServerManifest} from 'react-client/src/forks/ReactFlightClientConfig.dom-edge-rspack'; + +export type {ClientReference, ServerReference}; + +export type ClientManifest = { + [id: string]: ClientReferenceManifestEntry, +}; + +export type ServerReferenceId = string; + +export type ClientReferenceMetadata = ImportMetadata; +export opaque type ClientReferenceManifestEntry = ImportManifestEntry; + +export type ClientReferenceKey = string; + +export type BoundArgsEncryption = { + encrypt: (actionId: string, ...args: Array) => Promise, + decrypt: ( + actionId: string, + payloadPromise: Promise, + ) => Promise>, +}; + +export type ServerEntry = { + ...T, + entryJsFiles: ?Array, + entryCssFiles: ?Array, + ... +}; + +export { + isClientReference, + isServerReference, +} from '../ReactFlightRspackReferences'; + +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 (resolvedModuleData.async === true && clientReference.$$async === true) { + throw new Error( + 'The module "' + + modulePath + + '" is marked as an async ESM module but was loaded as a CJS proxy. ' + + 'This is probably a bug in the React Server Components bundler.', + ); + } + if (resolvedModuleData.async === true || 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; +} + +export function getServerReferenceLocation( + config: ClientManifest, + serverReference: ServerReference, +): void | Error { + return serverReference.$$location; +} + +const defaultStrategy: BoundArgsEncryption = { + encrypt: (_actionId: string, ...args: any[]) => Promise.resolve(args), + decrypt: (_actionId: string, payloadPromise: Promise) => payloadPromise, +}; + +let currentStrategy = defaultStrategy; + +export function setServerActionBoundArgsEncryption( + strategy: BoundArgsEncryption, +) { + currentStrategy = strategy; +} + +export function encryptServerActionBoundArgs( + actionId: string, + ...args: any[] +): Promise<> { + return currentStrategy.encrypt(actionId, ...args); +} + +export function decryptServerActionBoundArgs( + actionId: string, + encryptedPromise: Promise, +): Promise { + return currentStrategy.decrypt(actionId, encryptedPromise); +} + +declare const __rspack_rsc_manifest__: { + entryJsFiles: Array, + entryCssFiles: {[resourceId: string]: Array, ...}, + serverManifest: ServerManifest, +}; + +export function loadServerAction(actionId: string): Function { + const actionModId = __rspack_rsc_manifest__.serverManifest[actionId].id; + + if (!actionModId) { + throw new Error( + `Failed to find Server Action "${actionId}". This request might be from an older or newer deployment.`, + ); + } + + const moduleExports = __webpack_require__(actionModId); + const fn = moduleExports[actionId]; + if (typeof fn !== 'function') { + throw new Error('Server actions must be functions'); + } + return fn; +} + +export function createServerEntry( + value: T, + resourceId: string, +): ServerEntry { + const entryJsFiles = __rspack_rsc_manifest__.entryJsFiles || []; + const entryCssFiles = __rspack_rsc_manifest__.entryCssFiles[resourceId] || []; + if ( + typeof value === 'function' || + (typeof value === 'object' && value !== null) + ) { + // $FlowFixMe: We're dynamically adding properties to create ServerEntry + Object.assign(value, { + entryJsFiles, + entryCssFiles, + }); + } + // $FlowFixMe: After Object.assign, value conforms to ServerEntry + return (value: ServerEntry); +} + +// This function ensures that all the exported values are valid server actions, +// during the runtime. By definition all actions are required to be async +// functions, but here we can only check that they are functions. +export function ensureServerActions(actions: any[]) { + for (let i = 0; i < actions.length; i++) { + const action = actions[i]; + if (typeof action !== 'function') { + throw new Error( + `A "use server" file can only export async functions, found ${typeof action}.`, + ); + } + } +} diff --git a/packages/react-server-dom-rspack/src/server/react-flight-dom-server.browser.js b/packages/react-server-dom-rspack/src/server/react-flight-dom-server.browser.js new file mode 100644 index 00000000000..574827f341d --- /dev/null +++ b/packages/react-server-dom-rspack/src/server/react-flight-dom-server.browser.js @@ -0,0 +1,24 @@ +/** + * 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 { + renderToReadableStream, + prerender, + decodeReply, + decodeAction, + decodeFormState, + registerServerReference, + registerClientReference, + createTemporaryReferenceSet, + setServerActionBoundArgsEncryption, + encryptServerActionBoundArgs, + decryptServerActionBoundArgs, + createServerEntry, + ensureServerActions, +} from './ReactFlightDOMServerBrowser'; diff --git a/packages/react-server-dom-rspack/src/server/react-flight-dom-server.edge.js b/packages/react-server-dom-rspack/src/server/react-flight-dom-server.edge.js new file mode 100644 index 00000000000..4c8215436aa --- /dev/null +++ b/packages/react-server-dom-rspack/src/server/react-flight-dom-server.edge.js @@ -0,0 +1,26 @@ +/** + * 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 { + renderToReadableStream, + prerender, + decodeReply, + decodeReplyFromAsyncIterable, + decodeAction, + decodeFormState, + registerServerReference, + registerClientReference, + createTemporaryReferenceSet, + setServerActionBoundArgsEncryption, + encryptServerActionBoundArgs, + decryptServerActionBoundArgs, + loadServerAction, + createServerEntry, + ensureServerActions, +} from './ReactFlightDOMServerEdge'; diff --git a/packages/react-server-dom-rspack/src/server/react-flight-dom-server.node.js b/packages/react-server-dom-rspack/src/server/react-flight-dom-server.node.js new file mode 100644 index 00000000000..500775a6d9e --- /dev/null +++ b/packages/react-server-dom-rspack/src/server/react-flight-dom-server.node.js @@ -0,0 +1,29 @@ +/** + * 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 { + renderToReadableStream, + renderToPipeableStream, + prerender, + prerenderToNodeStream, + decodeReply, + decodeReplyFromBusboy, + decodeReplyFromAsyncIterable, + decodeAction, + decodeFormState, + registerServerReference, + registerClientReference, + createTemporaryReferenceSet, + setServerActionBoundArgsEncryption, + encryptServerActionBoundArgs, + decryptServerActionBoundArgs, + loadServerAction, + createServerEntry, + ensureServerActions, +} from './ReactFlightDOMServerNode'; diff --git a/packages/react-server-dom-rspack/src/shared/ReactFlightImportMetadata.js b/packages/react-server-dom-rspack/src/shared/ReactFlightImportMetadata.js new file mode 100644 index 00000000000..29b012f6052 --- /dev/null +++ b/packages/react-server-dom-rspack/src/shared/ReactFlightImportMetadata.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type ImportManifestEntry = { + id: string, + // chunks is a double indexed array of chunkId / chunkFilename pairs + chunks: Array, + name: string, + async?: boolean, +}; + +// This is the parsed shape of the wire format which is why it is +// condensed to only the essentialy information +export type ImportMetadata = + | [ + /* id */ string, + /* chunks id/filename pairs, double indexed */ Array, + /* name */ string, + /* async */ 1, + ] + | [ + /* id */ string, + /* chunks id/filename pairs, double indexed */ Array, + /* name */ string, + ]; + +export const ID = 0; +export const CHUNKS = 1; +export const NAME = 2; +// export const ASYNC = 3; + +// This logic is correct because currently only include the 4th tuple member +// when the module is async. If that changes we will need to actually assert +// the value is true. We don't index into the 4th slot because flow does not +// like the potential out of bounds access +export function isAsyncImport(metadata: ImportMetadata): boolean { + return metadata.length === 4; +} diff --git a/packages/react-server-dom-rspack/static.browser.js b/packages/react-server-dom-rspack/static.browser.js new file mode 100644 index 00000000000..25897891632 --- /dev/null +++ b/packages/react-server-dom-rspack/static.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 {prerender} from './src/server/react-flight-dom-server.browser'; diff --git a/packages/react-server-dom-rspack/static.edge.js b/packages/react-server-dom-rspack/static.edge.js new file mode 100644 index 00000000000..a39d54c73f5 --- /dev/null +++ b/packages/react-server-dom-rspack/static.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 {prerender} from './src/server/react-flight-dom-server.edge'; diff --git a/packages/react-server-dom-rspack/static.js b/packages/react-server-dom-rspack/static.js new file mode 100644 index 00000000000..83d8b8a017f --- /dev/null +++ b/packages/react-server-dom-rspack/static.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-rspack/static.node.js b/packages/react-server-dom-rspack/static.node.js new file mode 100644 index 00000000000..78e70a1cf4c --- /dev/null +++ b/packages/react-server-dom-rspack/static.node.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 + */ + +export { + prerender, + prerenderToNodeStream, +} from './src/server/react-flight-dom-server.node'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-rspack.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-rspack.js new file mode 100644 index 00000000000..f2dc1eb7530 --- /dev/null +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-rspack.js @@ -0,0 +1,26 @@ +/** + * 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} from 'react-server/src/ReactFlightServer'; +import type {ReactComponentInfo} from 'shared/ReactTypes'; + +export * from 'react-server-dom-rspack/src/server/ReactFlightServerConfigRspackBundler'; +export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); + +export const supportsComponentStorage = false; +export const componentStorage: AsyncLocalStorage = + (null: any); + +export * from '../ReactFlightServerConfigDebugNoop'; + +export * from '../ReactFlightStackConfigV8'; +export * from '../ReactServerConsoleConfigBrowser'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-rspack.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-rspack.js new file mode 100644 index 00000000000..467c28749dc --- /dev/null +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-rspack.js @@ -0,0 +1,28 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ +import type {Request} from 'react-server/src/ReactFlightServer'; +import type {ReactComponentInfo} from 'shared/ReactTypes'; + +export * from 'react-server-dom-rspack/src/server/ReactFlightServerConfigRspackBundler'; +export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +// For now, we get this from the global scope, but this will likely move to a module. +export const supportsRequestStorage = typeof AsyncLocalStorage === 'function'; +export const requestStorage: AsyncLocalStorage = + supportsRequestStorage ? new AsyncLocalStorage() : (null: any); + +export const supportsComponentStorage: boolean = + __DEV__ && supportsRequestStorage; +export const componentStorage: AsyncLocalStorage = + supportsComponentStorage ? new AsyncLocalStorage() : (null: any); + +export * from '../ReactFlightServerConfigDebugNoop'; + +export * from '../ReactFlightStackConfigV8'; +export * from '../ReactServerConsoleConfigServer'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-rspack.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-rspack.js new file mode 100644 index 00000000000..81e9e189ce6 --- /dev/null +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-rspack.js @@ -0,0 +1,29 @@ +/** + * 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 {AsyncLocalStorage} from 'async_hooks'; + +import type {Request} from 'react-server/src/ReactFlightServer'; +import type {ReactComponentInfo} from 'shared/ReactTypes'; + +export * from 'react-server-dom-rspack/src/server/ReactFlightServerConfigRspackBundler'; +export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = true; +export const requestStorage: AsyncLocalStorage = + new AsyncLocalStorage(); + +export const supportsComponentStorage = __DEV__; +export const componentStorage: AsyncLocalStorage = + supportsComponentStorage ? new AsyncLocalStorage() : (null: any); + +export * from '../ReactFlightServerConfigDebugNode'; + +export * from '../ReactFlightStackConfigV8'; +export * from '../ReactServerConsoleConfigServer'; diff --git a/packages/shared/ReactVersion.js b/packages/shared/ReactVersion.js index bd5fa23ca26..c7ea17f42b0 100644 --- a/packages/shared/ReactVersion.js +++ b/packages/shared/ReactVersion.js @@ -1,15 +1 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -// TODO: this is special because it gets imported during build. -// -// It exists as a placeholder so that DevTools can support work tag changes between releases. -// When we next publish a release, update the matching TODO in backend/renderer.js -// TODO: This module is used both by the release scripts and to expose a version -// at runtime. We should instead inject the version number as part of the build -// process, and use the ReactVersions.js module as the single source of truth. -export default '19.3.0'; +export default '19.3.0-canary-0d593e90-20260109'; diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 8a551d2a154..9f8b1a2da43 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -707,6 +707,80 @@ const bundles = [ externals: ['react', 'react-dom'], }, + /******* React Server DOM Rspack Server *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-rspack/src/server/react-flight-dom-server.browser', + name: 'react-server-dom-rspack-server.browser', + condition: 'react-server', + global: 'ReactServerDOMServer', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom'], + }, + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-rspack/src/server/react-flight-dom-server.node', + name: 'react-server-dom-rspack-server.node', + condition: 'react-server', + global: 'ReactServerDOMServer', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: [ + 'react', + 'react-dom', + 'async_hooks', + 'crypto', + 'stream', + 'util', + ], + }, + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-rspack/src/server/react-flight-dom-server.edge', + name: 'react-server-dom-rspack-server.edge', + condition: 'react-server', + global: 'ReactServerDOMServer', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom'], + }, + + /******* React Server DOM Rspack Client *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-rspack/src/client/react-flight-dom-client.browser', + name: 'react-server-dom-rspack-client.browser', + global: 'ReactServerDOMClient', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom'], + }, + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-rspack/src/client/react-flight-dom-client.node', + name: 'react-server-dom-rspack-client.node', + global: 'ReactServerDOMClient', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom', 'util'], + }, + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-rspack/src/client/react-flight-dom-client.edge', + name: 'react-server-dom-rspack-client.edge', + global: 'ReactServerDOMClient', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom'], + }, + /******* React Server DOM ESM Server *******/ { bundleTypes: [NODE_DEV, NODE_PROD], diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 0bb9da231d9..9efb5aec650 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -233,6 +233,52 @@ module.exports = [ isFlowTyped: true, isServerSupported: true, }, + { + shortName: 'dom-node-rspack', + entryPoints: [ + 'react-server-dom-rspack/src/client/react-flight-dom-client.node', + 'react-server-dom-rspack/src/server/react-flight-dom-server.node', + ], + paths: [ + 'react-dom', + 'react-dom-bindings', + 'react-dom/client', + 'react-dom/profiling', + 'react-dom/server', + 'react-dom/server.node', + 'react-dom/static', + 'react-dom/static.node', + 'react-dom/src/server/react-dom-server.node', + 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node + 'react-dom/src/server/ReactDOMFizzStaticNode.js', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', + 'react-server-dom-rspack', + 'react-server-dom-rspack/client.node', + 'react-server-dom-rspack/server', + 'react-server-dom-rspack/server.node', + 'react-server-dom-rspack/static', + 'react-server-dom-rspack/static.node', + 'react-server-dom-rspack/src/client/ReactFlightDOMClientEdge.js', // react-server-dom-rspack/client.node + 'react-server-dom-rspack/src/client/ReactFlightDOMClientNode.js', // react-server-dom-rspack/client.node + 'react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspack.js', + 'react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspackServer.js', + 'react-server-dom-rspack/src/client/react-flight-dom-client.node', + 'react-server-dom-rspack/src/server/react-flight-dom-server.node', + 'react-server-dom-rspack/src/server/ReactFlightDOMServerNode.js', // react-server-dom-rspack/src/server/react-flight-dom-server.node + 'react-server-dom-rspack/node-register', + 'react-server-dom-rspack/src/ReactFlightRspackNodeRegister.js', + 'react-devtools', + 'react-devtools-core', + 'react-devtools-shell', + 'react-devtools-shared', + 'shared/ReactDOMSharedInternals', + 'react-server/src/ReactFlightServerConfigDebugNode.js', + ], + isFlowTyped: true, + isServerSupported: true, + }, { shortName: 'dom-node-unbundled', entryPoints: [ @@ -399,6 +445,55 @@ module.exports = [ isFlowTyped: true, isServerSupported: true, }, + { + shortName: 'dom-browser-rspack', + entryPoints: [ + 'react-dom', + 'react-dom/client', + 'react-dom/profiling', + 'react-dom/unstable_testing', + 'react-dom/src/server/react-dom-server.browser.js', + 'react-dom/static.browser', + 'react-dom/unstable_server-external-runtime', + 'react-server-dom-rspack/src/client/react-flight-dom-client.browser', + 'react-server-dom-rspack/src/server/react-flight-dom-server.browser', + ], + paths: [ + 'react-dom', + 'react-dom/src/ReactDOMReactServer.js', + 'react-dom-bindings', + 'react-dom/client', + 'react-dom/profiling', + 'react-dom/server.browser', + 'react-dom/static.browser', + 'react-dom/unstable_testing', + 'react-dom/src/server/react-dom-server.browser', + 'react-dom/src/server/ReactDOMFizzServerBrowser.js', // react-dom/server.browser + 'react-dom/src/server/ReactDOMFizzStaticBrowser.js', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', + 'react-server-dom-rspack', + 'react-server-dom-rspack/client', + 'react-server-dom-rspack/client.browser', + 'react-server-dom-rspack/server.browser', + 'react-server-dom-rspack/static.browser', + 'react-server-dom-rspack/src/client/ReactFlightDOMClientBrowser.js', // react-server-dom-rspack/client.browser + 'react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspack.js', + 'react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspackBrowser.js', + 'react-server-dom-rspack/src/client/react-flight-dom-client.browser', + 'react-server-dom-rspack/src/server/react-flight-dom-server.browser', + 'react-server-dom-rspack/src/server/ReactFlightDOMServerBrowser.js', // react-server-dom-rspack/src/server/react-flight-dom-server.browser + 'react-devtools', + 'react-devtools-core', + 'react-devtools-shell', + 'react-devtools-shared', + 'shared/ReactDOMSharedInternals', + 'react-server/src/ReactFlightServerConfigDebugNoop.js', + ], + isFlowTyped: true, + isServerSupported: true, + }, { shortName: 'dom-edge-webpack', entryPoints: [ @@ -523,6 +618,49 @@ module.exports = [ isFlowTyped: true, isServerSupported: true, }, + { + shortName: 'dom-edge-rspack', + entryPoints: [ + 'react-dom/src/server/react-dom-server.edge.js', + 'react-dom/static.edge', + 'react-server-dom-rspack/src/client/react-flight-dom-client.edge', + 'react-server-dom-rspack/src/server/react-flight-dom-server.edge', + ], + paths: [ + 'react-dom', + 'react-dom/src/ReactDOMReactServer.js', + 'react-dom-bindings', + 'react-dom/client', + 'react-dom/profiling', + 'react-dom/server.edge', + 'react-dom/static.edge', + 'react-dom/unstable_testing', + 'react-dom/src/server/react-dom-server.edge', + 'react-dom/src/server/ReactDOMFizzServerEdge.js', // react-dom/server.edge + 'react-dom/src/server/ReactDOMFizzStaticEdge.js', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', + 'react-server-dom-rspack', + 'react-server-dom-rspack/client.edge', + 'react-server-dom-rspack/server.edge', + 'react-server-dom-rspack/static.edge', + 'react-server-dom-rspack/src/client/ReactFlightDOMClientEdge.js', // react-server-dom-rspack/client.edge + 'react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspack.js', + 'react-server-dom-rspack/src/client/ReactFlightClientConfigBundlerRspackServer.js', + 'react-server-dom-rspack/src/client/react-flight-dom-client.edge', + 'react-server-dom-rspack/src/server/react-flight-dom-server.edge', + 'react-server-dom-rspack/src/server/ReactFlightDOMServerEdge.js', // react-server-dom-rspack/src/server/react-flight-dom-server.edge + 'react-devtools', + 'react-devtools-core', + 'react-devtools-shell', + 'react-devtools-shared', + 'shared/ReactDOMSharedInternals', + 'react-server/src/ReactFlightServerConfigDebugNoop.js', + ], + isFlowTyped: true, + isServerSupported: true, + }, { shortName: 'dom-node-esm', entryPoints: [