diff --git a/.eslintrc.js b/.eslintrc.js
index 878dd08fda653..4d53738e281ae 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -326,6 +326,7 @@ module.exports = {
'packages/react-refresh/**/*.js',
'packages/react-server-dom-esm/**/*.js',
'packages/react-server-dom-webpack/**/*.js',
+ 'packages/react-server-dom-turbopack/**/*.js',
'packages/react-test-renderer/**/*.js',
'packages/react-debug-tools/**/*.js',
'packages/react-devtools-extensions/**/*.js',
@@ -427,6 +428,13 @@ module.exports = {
__webpack_require__: 'readonly',
},
},
+ {
+ files: ['packages/react-server-dom-turbopack/**/*.js'],
+ globals: {
+ __turbopack_load__: 'readonly',
+ __turbopack_require__: 'readonly',
+ },
+ },
{
files: ['packages/scheduler/**/*.js'],
globals: {
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js
new file mode 100644
index 0000000000000..9cb36f0674d02
--- /dev/null
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from 'react-client/src/ReactFlightClientConfigBrowser';
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack';
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser';
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser';
+export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
+export const usedWithSSR = false;
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js
new file mode 100644
index 0000000000000..3c6949554e361
--- /dev/null
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from 'react-client/src/ReactFlightClientConfigBrowser';
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack';
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer';
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer';
+export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
+export const usedWithSSR = true;
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js
new file mode 100644
index 0000000000000..30e737d14f668
--- /dev/null
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from 'react-client/src/ReactFlightClientConfigNode';
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack';
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer';
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer';
+export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
+export const usedWithSSR = true;
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js
new file mode 100644
index 0000000000000..ad8f3608102cf
--- /dev/null
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from 'react-client/src/ReactFlightClientConfigNode';
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode';
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer';
+export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
+export const usedWithSSR = true;
diff --git a/packages/react-server-dom-turbopack/README.md b/packages/react-server-dom-turbopack/README.md
new file mode 100644
index 0000000000000..3d183a8a4a2fd
--- /dev/null
+++ b/packages/react-server-dom-turbopack/README.md
@@ -0,0 +1,5 @@
+# react-server-dom-turbopack
+
+Experimental React Flight bindings for DOM using Turbopack.
+
+**Use it at your own risk.**
diff --git a/packages/react-server-dom-turbopack/client.browser.js b/packages/react-server-dom-turbopack/client.browser.js
new file mode 100644
index 0000000000000..7d26c2771e50a
--- /dev/null
+++ b/packages/react-server-dom-turbopack/client.browser.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from './src/ReactFlightDOMClientBrowser';
diff --git a/packages/react-server-dom-turbopack/client.edge.js b/packages/react-server-dom-turbopack/client.edge.js
new file mode 100644
index 0000000000000..fadceeaf8443a
--- /dev/null
+++ b/packages/react-server-dom-turbopack/client.edge.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from './src/ReactFlightDOMClientEdge';
diff --git a/packages/react-server-dom-turbopack/client.js b/packages/react-server-dom-turbopack/client.js
new file mode 100644
index 0000000000000..2dad5bb513872
--- /dev/null
+++ b/packages/react-server-dom-turbopack/client.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from './client.browser';
diff --git a/packages/react-server-dom-turbopack/client.node.js b/packages/react-server-dom-turbopack/client.node.js
new file mode 100644
index 0000000000000..4f435353a20f0
--- /dev/null
+++ b/packages/react-server-dom-turbopack/client.node.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from './src/ReactFlightDOMClientNode';
diff --git a/packages/react-server-dom-turbopack/client.node.unbundled.js b/packages/react-server-dom-turbopack/client.node.unbundled.js
new file mode 100644
index 0000000000000..4f435353a20f0
--- /dev/null
+++ b/packages/react-server-dom-turbopack/client.node.unbundled.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from './src/ReactFlightDOMClientNode';
diff --git a/packages/react-server-dom-turbopack/esm/package.json b/packages/react-server-dom-turbopack/esm/package.json
new file mode 100644
index 0000000000000..3dbc1ca591c05
--- /dev/null
+++ b/packages/react-server-dom-turbopack/esm/package.json
@@ -0,0 +1,3 @@
+{
+ "type": "module"
+}
diff --git a/packages/react-server-dom-turbopack/esm/react-server-dom-turbopack-node-loader.production.min.js b/packages/react-server-dom-turbopack/esm/react-server-dom-turbopack-node-loader.production.min.js
new file mode 100644
index 0000000000000..ef6486656cafe
--- /dev/null
+++ b/packages/react-server-dom-turbopack/esm/react-server-dom-turbopack-node-loader.production.min.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from '../src/ReactFlightTurbopackNodeLoader.js';
diff --git a/packages/react-server-dom-turbopack/index.js b/packages/react-server-dom-turbopack/index.js
new file mode 100644
index 0000000000000..348324f0de86d
--- /dev/null
+++ b/packages/react-server-dom-turbopack/index.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+throw new Error('Use react-server-dom-turbopack/client instead.');
diff --git a/packages/react-server-dom-turbopack/node-register.js b/packages/react-server-dom-turbopack/node-register.js
new file mode 100644
index 0000000000000..0d399f3842731
--- /dev/null
+++ b/packages/react-server-dom-turbopack/node-register.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+module.exports = require('./src/ReactFlightTurbopackNodeRegister');
diff --git a/packages/react-server-dom-turbopack/npm/client.browser.js b/packages/react-server-dom-turbopack/npm/client.browser.js
new file mode 100644
index 0000000000000..5cd0ada188456
--- /dev/null
+++ b/packages/react-server-dom-turbopack/npm/client.browser.js
@@ -0,0 +1,7 @@
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./cjs/react-server-dom-turbopack-client.browser.production.min.js');
+} else {
+ module.exports = require('./cjs/react-server-dom-turbopack-client.browser.development.js');
+}
diff --git a/packages/react-server-dom-turbopack/npm/client.edge.js b/packages/react-server-dom-turbopack/npm/client.edge.js
new file mode 100644
index 0000000000000..3499ce22c2a39
--- /dev/null
+++ b/packages/react-server-dom-turbopack/npm/client.edge.js
@@ -0,0 +1,7 @@
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./cjs/react-server-dom-turbopack-client.edge.production.min.js');
+} else {
+ module.exports = require('./cjs/react-server-dom-turbopack-client.edge.development.js');
+}
diff --git a/packages/react-server-dom-turbopack/npm/client.js b/packages/react-server-dom-turbopack/npm/client.js
new file mode 100644
index 0000000000000..89d93a7a7920f
--- /dev/null
+++ b/packages/react-server-dom-turbopack/npm/client.js
@@ -0,0 +1,3 @@
+'use strict';
+
+module.exports = require('./client.browser');
diff --git a/packages/react-server-dom-turbopack/npm/client.node.js b/packages/react-server-dom-turbopack/npm/client.node.js
new file mode 100644
index 0000000000000..c346d351d344e
--- /dev/null
+++ b/packages/react-server-dom-turbopack/npm/client.node.js
@@ -0,0 +1,7 @@
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./cjs/react-server-dom-turbopack-client.node.production.min.js');
+} else {
+ module.exports = require('./cjs/react-server-dom-turbopack-client.node.development.js');
+}
diff --git a/packages/react-server-dom-turbopack/npm/client.node.unbundled.js b/packages/react-server-dom-turbopack/npm/client.node.unbundled.js
new file mode 100644
index 0000000000000..9b15ea16d8a33
--- /dev/null
+++ b/packages/react-server-dom-turbopack/npm/client.node.unbundled.js
@@ -0,0 +1,7 @@
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./cjs/react-server-dom-turbopack-client.node.unbundled.production.min.js');
+} else {
+ module.exports = require('./cjs/react-server-dom-turbopack-client.node.unbundled.development.js');
+}
diff --git a/packages/react-server-dom-turbopack/npm/esm/package.json b/packages/react-server-dom-turbopack/npm/esm/package.json
new file mode 100644
index 0000000000000..3dbc1ca591c05
--- /dev/null
+++ b/packages/react-server-dom-turbopack/npm/esm/package.json
@@ -0,0 +1,3 @@
+{
+ "type": "module"
+}
diff --git a/packages/react-server-dom-turbopack/npm/index.js b/packages/react-server-dom-turbopack/npm/index.js
new file mode 100644
index 0000000000000..53e8a98128cdb
--- /dev/null
+++ b/packages/react-server-dom-turbopack/npm/index.js
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+'use strict';
+
+throw new Error('Use react-server-dom-turbopack/client instead.');
diff --git a/packages/react-server-dom-turbopack/npm/node-register.js b/packages/react-server-dom-turbopack/npm/node-register.js
new file mode 100644
index 0000000000000..7506743f033fa
--- /dev/null
+++ b/packages/react-server-dom-turbopack/npm/node-register.js
@@ -0,0 +1,3 @@
+'use strict';
+
+module.exports = require('./cjs/react-server-dom-turbopack-node-register.js');
diff --git a/packages/react-server-dom-turbopack/npm/server.browser.js b/packages/react-server-dom-turbopack/npm/server.browser.js
new file mode 100644
index 0000000000000..05c0b03496bda
--- /dev/null
+++ b/packages/react-server-dom-turbopack/npm/server.browser.js
@@ -0,0 +1,7 @@
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./cjs/react-server-dom-turbopack-server.browser.production.min.js');
+} else {
+ module.exports = require('./cjs/react-server-dom-turbopack-server.browser.development.js');
+}
diff --git a/packages/react-server-dom-turbopack/npm/server.edge.js b/packages/react-server-dom-turbopack/npm/server.edge.js
new file mode 100644
index 0000000000000..b09cb0b82282d
--- /dev/null
+++ b/packages/react-server-dom-turbopack/npm/server.edge.js
@@ -0,0 +1,7 @@
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./cjs/react-server-dom-turbopack-server.edge.production.min.js');
+} else {
+ module.exports = require('./cjs/react-server-dom-turbopack-server.edge.development.js');
+}
diff --git a/packages/react-server-dom-turbopack/npm/server.js b/packages/react-server-dom-turbopack/npm/server.js
new file mode 100644
index 0000000000000..13a632e641179
--- /dev/null
+++ b/packages/react-server-dom-turbopack/npm/server.js
@@ -0,0 +1,6 @@
+'use strict';
+
+throw new Error(
+ 'The React Server Writer cannot be used outside a react-server environment. ' +
+ 'You must configure Node.js using the `--conditions react-server` flag.'
+);
diff --git a/packages/react-server-dom-turbopack/npm/server.node.js b/packages/react-server-dom-turbopack/npm/server.node.js
new file mode 100644
index 0000000000000..59635310eb2d1
--- /dev/null
+++ b/packages/react-server-dom-turbopack/npm/server.node.js
@@ -0,0 +1,7 @@
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./cjs/react-server-dom-turbopack-server.node.production.min.js');
+} else {
+ module.exports = require('./cjs/react-server-dom-turbopack-server.node.development.js');
+}
diff --git a/packages/react-server-dom-turbopack/npm/server.node.unbundled.js b/packages/react-server-dom-turbopack/npm/server.node.unbundled.js
new file mode 100644
index 0000000000000..4f8856e4303a8
--- /dev/null
+++ b/packages/react-server-dom-turbopack/npm/server.node.unbundled.js
@@ -0,0 +1,7 @@
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./cjs/react-server-dom-turbopack-server.node.unbundled.production.min.js');
+} else {
+ module.exports = require('./cjs/react-server-dom-turbopack-server.node.unbundled.development.js');
+}
diff --git a/packages/react-server-dom-turbopack/package.json b/packages/react-server-dom-turbopack/package.json
new file mode 100644
index 0000000000000..36e8765617368
--- /dev/null
+++ b/packages/react-server-dom-turbopack/package.json
@@ -0,0 +1,95 @@
+{
+ "name": "react-server-dom-turbopack",
+ "description": "React Server Components bindings for DOM using Turbopack. This is intended to be integrated into meta-frameworks. It is not intended to be imported directly.",
+ "version": "18.2.0",
+ "keywords": [
+ "react"
+ ],
+ "homepage": "https://reactjs.org/",
+ "bugs": "https://github.com/facebook/react/issues",
+ "license": "MIT",
+ "files": [
+ "LICENSE",
+ "README.md",
+ "index.js",
+ "client.js",
+ "client.browser.js",
+ "client.edge.js",
+ "client.node.js",
+ "client.node.unbundled.js",
+ "server.js",
+ "server.browser.js",
+ "server.edge.js",
+ "server.node.js",
+ "server.node.unbundled.js",
+ "node-register.js",
+ "cjs/",
+ "umd/",
+ "esm/"
+ ],
+ "exports": {
+ ".": "./index.js",
+ "./client": {
+ "workerd": "./client.edge.js",
+ "deno": "./client.edge.js",
+ "worker": "./client.edge.js",
+ "node": {
+ "turbopack": "./client.node.js",
+ "webpack": "./client.node.js",
+ "default": "./client.node.unbundled.js"
+ },
+ "edge-light": "./client.edge.js",
+ "browser": "./client.browser.js",
+ "default": "./client.browser.js"
+ },
+ "./client.browser": "./client.browser.js",
+ "./client.edge": "./client.edge.js",
+ "./client.node": "./client.node.js",
+ "./client.node.unbundled": "./client.node.unbundled.js",
+ "./server": {
+ "react-server": {
+ "workerd": "./server.edge.js",
+ "deno": "./server.browser.js",
+ "node": {
+ "turbopack": "./server.node.js",
+ "webpack": "./server.node.js",
+ "default": "./server.node.unbundled.js"
+ },
+ "edge-light": "./server.edge.js",
+ "browser": "./server.browser.js"
+ },
+ "default": "./server.js"
+ },
+ "./server.browser": "./server.browser.js",
+ "./server.edge": "./server.edge.js",
+ "./server.node": "./server.node.js",
+ "./server.node.unbundled": "./server.node.unbundled.js",
+ "./node-loader": "./esm/react-server-dom-turbopack-node-loader.production.min.js",
+ "./node-register": "./node-register.js",
+ "./src/*": "./src/*.js",
+ "./package.json": "./package.json"
+ },
+ "main": "index.js",
+ "repository": {
+ "type" : "git",
+ "url" : "https://github.com/facebook/react.git",
+ "directory": "packages/react-server-dom-turbopack"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "peerDependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "dependencies": {
+ "acorn-loose": "^8.3.0",
+ "neo-async": "^2.6.1",
+ "loose-envify": "^1.1.0"
+ },
+ "browserify": {
+ "transform": [
+ "loose-envify"
+ ]
+ }
+}
diff --git a/packages/react-server-dom-turbopack/server.browser.js b/packages/react-server-dom-turbopack/server.browser.js
new file mode 100644
index 0000000000000..41a9fb5c44968
--- /dev/null
+++ b/packages/react-server-dom-turbopack/server.browser.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from './src/ReactFlightDOMServerBrowser';
diff --git a/packages/react-server-dom-turbopack/server.edge.js b/packages/react-server-dom-turbopack/server.edge.js
new file mode 100644
index 0000000000000..98f975cb4706f
--- /dev/null
+++ b/packages/react-server-dom-turbopack/server.edge.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from './src/ReactFlightDOMServerEdge';
diff --git a/packages/react-server-dom-turbopack/server.js b/packages/react-server-dom-turbopack/server.js
new file mode 100644
index 0000000000000..83d8b8a017ff2
--- /dev/null
+++ b/packages/react-server-dom-turbopack/server.js
@@ -0,0 +1,13 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+throw new Error(
+ 'The React Server cannot be used outside a react-server environment. ' +
+ 'You must configure Node.js using the `--conditions react-server` flag.',
+);
diff --git a/packages/react-server-dom-turbopack/server.node.js b/packages/react-server-dom-turbopack/server.node.js
new file mode 100644
index 0000000000000..7726b9bb929d4
--- /dev/null
+++ b/packages/react-server-dom-turbopack/server.node.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from './src/ReactFlightDOMServerNode';
diff --git a/packages/react-server-dom-turbopack/server.node.unbundled.js b/packages/react-server-dom-turbopack/server.node.unbundled.js
new file mode 100644
index 0000000000000..7726b9bb929d4
--- /dev/null
+++ b/packages/react-server-dom-turbopack/server.node.unbundled.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from './src/ReactFlightDOMServerNode';
diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js
new file mode 100644
index 0000000000000..b2bf9b8ae0b53
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js
@@ -0,0 +1,162 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {
+ Thenable,
+ FulfilledThenable,
+ RejectedThenable,
+} from 'shared/ReactTypes';
+
+import type {ImportMetadata} from './shared/ReactFlightImportMetadata';
+import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig';
+
+import {
+ ID,
+ CHUNKS,
+ NAME,
+ isAsyncImport,
+} from './shared/ReactFlightImportMetadata';
+import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig';
+
+export type SSRModuleMap = {
+ [clientId: string]: {
+ [clientExportName: string]: ClientReference,
+ },
+};
+
+export type ServerManifest = void;
+
+export type ServerReferenceId = string;
+
+export opaque type ClientReferenceMetadata = ImportMetadata;
+
+// eslint-disable-next-line no-unused-vars
+export opaque type ClientReference = {
+ specifier: string,
+ name: string,
+ async?: boolean,
+};
+
+// The reason this function needs to defined here in this file instead of just
+// being exported directly from the WebpackDestination... file is because the
+// ClientReferenceMetadata is opaque and we can't unwrap it there.
+// This should get inlined and we could also just implement an unwrapping function
+// though that risks it getting used in places it shouldn't be. This is unfortunate
+// but currently it seems to be the best option we have.
+export function prepareDestinationForModule(
+ moduleLoading: ModuleLoading,
+ nonce: ?string,
+ metadata: ClientReferenceMetadata,
+) {
+ prepareDestinationWithChunks(moduleLoading, metadata[CHUNKS], nonce);
+}
+
+export function resolveClientReference(
+ bundlerConfig: SSRModuleMap,
+ metadata: ClientReferenceMetadata,
+): ClientReference {
+ const moduleExports = bundlerConfig[metadata[ID]];
+ let resolvedModuleData = moduleExports[metadata[NAME]];
+ let name;
+ if (resolvedModuleData) {
+ // The potentially aliased name.
+ name = resolvedModuleData.name;
+ } else {
+ // If we don't have this specific name, we might have the full module.
+ resolvedModuleData = moduleExports['*'];
+ if (!resolvedModuleData) {
+ throw new Error(
+ 'Could not find the module "' +
+ metadata[ID] +
+ '" in the React SSR Manifest. ' +
+ 'This is probably a bug in the React Server Components bundler.',
+ );
+ }
+ name = metadata[NAME];
+ }
+ return {
+ specifier: resolvedModuleData.specifier,
+ name: name,
+ async: isAsyncImport(metadata),
+ };
+}
+
+export function resolveServerReference(
+ bundlerConfig: ServerManifest,
+ id: ServerReferenceId,
+): ClientReference {
+ const idx = id.lastIndexOf('#');
+ const specifier = id.slice(0, idx);
+ const name = id.slice(idx + 1);
+ return {specifier, name};
+}
+
+const asyncModuleCache: Map> = new Map();
+
+export function preloadModule(
+ metadata: ClientReference,
+): null | Thenable {
+ const existingPromise = asyncModuleCache.get(metadata.specifier);
+ if (existingPromise) {
+ if (existingPromise.status === 'fulfilled') {
+ return null;
+ }
+ return existingPromise;
+ } else {
+ // $FlowFixMe[unsupported-syntax]
+ let modulePromise: Promise = import(metadata.specifier);
+ if (metadata.async) {
+ // If the module is async, it must have been a CJS module.
+ // CJS modules are accessed through the default export in
+ // Node.js so we have to get the default export to get the
+ // full module exports.
+ modulePromise = modulePromise.then(function (value) {
+ return (value: any).default;
+ });
+ }
+ modulePromise.then(
+ value => {
+ const fulfilledThenable: FulfilledThenable =
+ (modulePromise: any);
+ fulfilledThenable.status = 'fulfilled';
+ fulfilledThenable.value = value;
+ },
+ reason => {
+ const rejectedThenable: RejectedThenable = (modulePromise: any);
+ rejectedThenable.status = 'rejected';
+ rejectedThenable.reason = reason;
+ },
+ );
+ asyncModuleCache.set(metadata.specifier, modulePromise);
+ return modulePromise;
+ }
+}
+
+export function requireModule(metadata: ClientReference): T {
+ let moduleExports;
+ // We assume that preloadModule has been called before, which
+ // should have added something to the module cache.
+ const promise: any = asyncModuleCache.get(metadata.specifier);
+ if (promise.status === 'fulfilled') {
+ moduleExports = promise.value;
+ } else {
+ throw promise.reason;
+ }
+ if (metadata.name === '*') {
+ // This is a placeholder value that represents that the caller imported this
+ // as a CommonJS module as is.
+ return moduleExports;
+ }
+ if (metadata.name === '') {
+ // This is a placeholder value that represents that the caller accessed the
+ // default property of this if it was an ESM interop module.
+ return moduleExports.default;
+ }
+ return moduleExports[metadata.name];
+}
diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js
new file mode 100644
index 0000000000000..b20ad69a408d6
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js
@@ -0,0 +1,232 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {
+ Thenable,
+ FulfilledThenable,
+ RejectedThenable,
+} from 'shared/ReactTypes';
+
+import type {
+ ImportMetadata,
+ ImportManifestEntry,
+} from './shared/ReactFlightImportMetadata';
+import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig';
+
+import {
+ ID,
+ CHUNKS,
+ NAME,
+ isAsyncImport,
+} from './shared/ReactFlightImportMetadata';
+
+import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig';
+
+import {loadChunk} from 'react-client/src/ReactFlightClientConfig';
+
+export type SSRModuleMap = null | {
+ [clientId: string]: {
+ [clientExportName: string]: 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 TurbopackDestination... file is because the
+// ClientReferenceMetadata is opaque and we can't unwrap it there.
+// This should get inlined and we could also just implement an unwrapping function
+// though that risks it getting used in places it shouldn't be. This is unfortunate
+// but currently it seems to be the best option we have.
+export function prepareDestinationForModule(
+ moduleLoading: ModuleLoading,
+ nonce: ?string,
+ metadata: ClientReferenceMetadata,
+) {
+ prepareDestinationWithChunks(moduleLoading, metadata[CHUNKS], nonce);
+}
+
+export function resolveClientReference(
+ bundlerConfig: SSRModuleMap,
+ metadata: ClientReferenceMetadata,
+): ClientReference {
+ if (bundlerConfig) {
+ const moduleExports = bundlerConfig[metadata[ID]];
+ let resolvedModuleData = moduleExports[metadata[NAME]];
+ let name;
+ if (resolvedModuleData) {
+ // The potentially aliased name.
+ name = resolvedModuleData.name;
+ } else {
+ // If we don't have this specific name, we might have the full module.
+ resolvedModuleData = moduleExports['*'];
+ if (!resolvedModuleData) {
+ throw new Error(
+ 'Could not find the module "' +
+ metadata[ID] +
+ '" in the React SSR Manifest. ' +
+ 'This is probably a bug in the React Server Components bundler.',
+ );
+ }
+ name = metadata[NAME];
+ }
+ if (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.',
+ );
+ }
+ }
+ // TODO: This needs to return async: true if it's an async module.
+ return [resolvedModuleData.id, resolvedModuleData.chunks, name];
+}
+
+// The chunk cache contains all the chunks we've preloaded so far.
+// If they're still pending they're a thenable. This map also exists
+// in Turbopack but unfortunately it's not exposed so we have to
+// replicate it in user space. null means that it has already loaded.
+const chunkCache: Map> = new Map();
+
+function requireAsyncModule(id: string): null | Thenable {
+ // We've already loaded all the chunks. We can require the module.
+ const promise = __turbopack_require__(id);
+ if (typeof promise.then !== 'function') {
+ // This wasn't a promise after all.
+ return null;
+ } else if (promise.status === 'fulfilled') {
+ // This module was already resolved earlier.
+ return null;
+ } else {
+ // Instrument the Promise to stash the result.
+ promise.then(
+ value => {
+ const fulfilledThenable: FulfilledThenable = (promise: any);
+ fulfilledThenable.status = 'fulfilled';
+ fulfilledThenable.value = value;
+ },
+ reason => {
+ const rejectedThenable: RejectedThenable = (promise: any);
+ rejectedThenable.status = 'rejected';
+ rejectedThenable.reason = reason;
+ },
+ );
+ return promise;
+ }
+}
+
+function ignoreReject() {
+ // We rely on rejected promises to be handled by another listener.
+}
+// Start preloading the modules since we might need them soon.
+// This function doesn't suspend.
+export function preloadModule(
+ metadata: ClientReference,
+): null | Thenable {
+ const chunks = metadata[CHUNKS];
+ const promises = [];
+ for (let i = 0; i < chunks.length; i++) {
+ const chunkFilename = chunks[i];
+ const entry = chunkCache.get(chunkFilename);
+ if (entry === undefined) {
+ const thenable = loadChunk(chunkFilename);
+ promises.push(thenable);
+ // $FlowFixMe[method-unbinding]
+ const resolve = chunkCache.set.bind(chunkCache, chunkFilename, null);
+ thenable.then(resolve, ignoreReject);
+ chunkCache.set(chunkFilename, thenable);
+ } else if (entry !== null) {
+ promises.push(entry);
+ }
+ }
+ if (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 = __turbopack_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;
+ }
+ return moduleExports[metadata[NAME]];
+}
diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser.js
new file mode 100644
index 0000000000000..c418f51fa80ee
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser.js
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export function loadChunk(filename: string): Promise {
+ return __turbopack_load__(filename);
+}
diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js
new file mode 100644
index 0000000000000..c418f51fa80ee
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export function loadChunk(filename: string): Promise {
+ return __turbopack_load__(filename);
+}
diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser.js
new file mode 100644
index 0000000000000..60b9e87dbea3e
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser.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-turbopack/src/ReactFlightClientConfigTargetTurbopackServer.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer.js
new file mode 100644
index 0000000000000..d68f0802aa5f4
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer.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 single-indexed filenames
+ chunks: Array,
+ nonce: ?string,
+) {
+ if (moduleLoading !== null) {
+ for (let i = 0; i < chunks.length; i++) {
+ preinitScriptForSSR(
+ moduleLoading.prefix + chunks[i],
+ nonce,
+ moduleLoading.crossOrigin,
+ );
+ }
+ }
+}
diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js
new file mode 100644
index 0000000000000..64e6b3886adf7
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js
@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {Thenable} from 'shared/ReactTypes.js';
+
+import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
+
+import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
+
+import {
+ createResponse,
+ getRoot,
+ reportGlobalError,
+ processBinaryChunk,
+ close,
+} from 'react-client/src/ReactFlightClient';
+
+import {
+ processReply,
+ createServerReference,
+} from 'react-client/src/ReactFlightReplyClient';
+
+type CallServerCallback = (string, args: A) => Promise;
+
+export type Options = {
+ callServer?: CallServerCallback,
+};
+
+function createResponseFromOptions(options: void | Options) {
+ return createResponse(
+ null,
+ null,
+ options && options.callServer ? options.callServer : undefined,
+ undefined, // nonce
+ );
+}
+
+function startReadingFromStream(
+ response: FlightResponse,
+ stream: ReadableStream,
+): void {
+ const reader = stream.getReader();
+ function progress({
+ done,
+ value,
+ }: {
+ done: boolean,
+ value: ?any,
+ ...
+ }): void | Promise {
+ if (done) {
+ close(response);
+ return;
+ }
+ const buffer: Uint8Array = (value: any);
+ processBinaryChunk(response, buffer);
+ return reader.read().then(progress).catch(error);
+ }
+ function error(e: any) {
+ reportGlobalError(response, e);
+ }
+ reader.read().then(progress).catch(error);
+}
+
+function createFromReadableStream(
+ stream: ReadableStream,
+ options?: Options,
+): Thenable {
+ const response: FlightResponse = createResponseFromOptions(options);
+ startReadingFromStream(response, stream);
+ return getRoot(response);
+}
+
+function createFromFetch(
+ promiseForResponse: Promise,
+ options?: Options,
+): Thenable {
+ const response: FlightResponse = createResponseFromOptions(options);
+ promiseForResponse.then(
+ function (r) {
+ startReadingFromStream(response, (r.body: any));
+ },
+ function (e) {
+ reportGlobalError(response, e);
+ },
+ );
+ return getRoot(response);
+}
+
+function encodeReply(
+ value: ReactServerValue,
+): Promise<
+ string | URLSearchParams | FormData,
+> /* We don't use URLSearchParams yet but maybe */ {
+ return new Promise((resolve, reject) => {
+ processReply(value, '', resolve, reject);
+ });
+}
+
+export {
+ createFromFetch,
+ createFromReadableStream,
+ encodeReply,
+ createServerReference,
+};
diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js
new file mode 100644
index 0000000000000..3b2f7aeea044e
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {Thenable} from 'shared/ReactTypes.js';
+
+import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
+
+import type {
+ SSRModuleMap,
+ ModuleLoading,
+} from 'react-client/src/ReactFlightClientConfig';
+
+type SSRManifest = {
+ moduleMap: SSRModuleMap,
+ moduleLoading: ModuleLoading,
+};
+
+import {
+ createResponse,
+ getRoot,
+ reportGlobalError,
+ processBinaryChunk,
+ close,
+} from 'react-client/src/ReactFlightClient';
+
+import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient';
+
+function noServerCall() {
+ throw new Error(
+ 'Server Functions cannot be called during initial render. ' +
+ 'This would create a fetch waterfall. Try to use a Server Component ' +
+ 'to pass data to Client Components instead.',
+ );
+}
+
+export function createServerReference, T>(
+ id: any,
+ callServer: any,
+): (...A) => Promise {
+ return createServerReferenceImpl(id, noServerCall);
+}
+
+export type Options = {
+ ssrManifest: SSRManifest,
+ nonce?: string,
+};
+
+function createResponseFromOptions(options: Options) {
+ return createResponse(
+ options.ssrManifest.moduleMap,
+ options.ssrManifest.moduleLoading,
+ noServerCall,
+ typeof options.nonce === 'string' ? options.nonce : undefined,
+ );
+}
+
+function startReadingFromStream(
+ response: FlightResponse,
+ stream: ReadableStream,
+): void {
+ const reader = stream.getReader();
+ function progress({
+ done,
+ value,
+ }: {
+ done: boolean,
+ value: ?any,
+ ...
+ }): void | Promise {
+ if (done) {
+ close(response);
+ return;
+ }
+ const buffer: Uint8Array = (value: any);
+ processBinaryChunk(response, buffer);
+ return reader.read().then(progress).catch(error);
+ }
+ function error(e: any) {
+ reportGlobalError(response, e);
+ }
+ reader.read().then(progress).catch(error);
+}
+
+function createFromReadableStream(
+ stream: ReadableStream,
+ options: Options,
+): Thenable {
+ const response: FlightResponse = createResponseFromOptions(options);
+ startReadingFromStream(response, stream);
+ return getRoot(response);
+}
+
+function createFromFetch(
+ promiseForResponse: Promise,
+ options: Options,
+): Thenable {
+ const response: FlightResponse = createResponseFromOptions(options);
+ promiseForResponse.then(
+ function (r) {
+ startReadingFromStream(response, (r.body: any));
+ },
+ function (e) {
+ reportGlobalError(response, e);
+ },
+ );
+ return getRoot(response);
+}
+
+export {createFromFetch, createFromReadableStream};
diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js
new file mode 100644
index 0000000000000..730bc8d61354b
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {Thenable} from 'shared/ReactTypes.js';
+
+import type {Response} from 'react-client/src/ReactFlightClient';
+
+import type {
+ SSRModuleMap,
+ ModuleLoading,
+} from 'react-client/src/ReactFlightClientConfig';
+
+type SSRManifest = {
+ moduleMap: SSRModuleMap,
+ moduleLoading: ModuleLoading,
+};
+
+import type {Readable} from 'stream';
+
+import {
+ createResponse,
+ getRoot,
+ reportGlobalError,
+ processBinaryChunk,
+ close,
+} from 'react-client/src/ReactFlightClient';
+
+import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient';
+
+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 type Options = {
+ nonce?: string,
+};
+
+export function createServerReference, T>(
+ id: any,
+ callServer: any,
+): (...A) => Promise {
+ return createServerReferenceImpl(id, noServerCall);
+}
+
+function createFromNodeStream(
+ stream: Readable,
+ ssrManifest: SSRManifest,
+ options?: Options,
+): Thenable {
+ const response: Response = createResponse(
+ ssrManifest.moduleMap,
+ ssrManifest.moduleLoading,
+ noServerCall,
+ options && typeof options.nonce === 'string' ? options.nonce : undefined,
+ );
+ stream.on('data', chunk => {
+ processBinaryChunk(response, chunk);
+ });
+ stream.on('error', error => {
+ reportGlobalError(response, error);
+ });
+ stream.on('end', () => close(response));
+ return getRoot(response);
+}
+
+export {createFromNodeStream};
diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js
new file mode 100644
index 0000000000000..412e697af3ab9
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
+import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes';
+import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
+import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
+
+import {
+ createRequest,
+ startWork,
+ startFlowing,
+ abort,
+} from 'react-server/src/ReactFlightServer';
+
+import {
+ createResponse,
+ close,
+ getRoot,
+} from 'react-server/src/ReactFlightReplyServer';
+
+import {decodeAction} from 'react-server/src/ReactFlightActionServer';
+
+export {
+ registerServerReference,
+ registerClientReference,
+ createClientModuleProxy,
+} from './ReactFlightTurbopackReferences';
+
+type Options = {
+ identifierPrefix?: string,
+ signal?: AbortSignal,
+ context?: Array<[string, ServerContextJSONValue]>,
+ onError?: (error: mixed) => void,
+ onPostpone?: (reason: string) => void,
+};
+
+function renderToReadableStream(
+ model: ReactClientValue,
+ turbopackMap: ClientManifest,
+ options?: Options,
+): ReadableStream {
+ const request = createRequest(
+ model,
+ turbopackMap,
+ options ? options.onError : undefined,
+ options ? options.context : undefined,
+ options ? options.identifierPrefix : undefined,
+ options ? options.onPostpone : undefined,
+ );
+ if (options && options.signal) {
+ const signal = options.signal;
+ if (signal.aborted) {
+ abort(request, (signal: any).reason);
+ } else {
+ const listener = () => {
+ abort(request, (signal: any).reason);
+ signal.removeEventListener('abort', listener);
+ };
+ signal.addEventListener('abort', listener);
+ }
+ }
+ const stream = new ReadableStream(
+ {
+ type: 'bytes',
+ start: (controller): ?Promise => {
+ startWork(request);
+ },
+ pull: (controller): ?Promise => {
+ startFlowing(request, controller);
+ },
+ cancel: (reason): ?Promise => {},
+ },
+ // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
+ {highWaterMark: 0},
+ );
+ return stream;
+}
+
+function decodeReply(
+ body: string | FormData,
+ turbopackMap: ServerManifest,
+): Thenable {
+ if (typeof body === 'string') {
+ const form = new FormData();
+ form.append('0', body);
+ body = form;
+ }
+ const response = createResponse(turbopackMap, '', body);
+ close(response);
+ return getRoot(response);
+}
+
+export {renderToReadableStream, decodeReply, decodeAction};
diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js
new file mode 100644
index 0000000000000..412e697af3ab9
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
+import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes';
+import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
+import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
+
+import {
+ createRequest,
+ startWork,
+ startFlowing,
+ abort,
+} from 'react-server/src/ReactFlightServer';
+
+import {
+ createResponse,
+ close,
+ getRoot,
+} from 'react-server/src/ReactFlightReplyServer';
+
+import {decodeAction} from 'react-server/src/ReactFlightActionServer';
+
+export {
+ registerServerReference,
+ registerClientReference,
+ createClientModuleProxy,
+} from './ReactFlightTurbopackReferences';
+
+type Options = {
+ identifierPrefix?: string,
+ signal?: AbortSignal,
+ context?: Array<[string, ServerContextJSONValue]>,
+ onError?: (error: mixed) => void,
+ onPostpone?: (reason: string) => void,
+};
+
+function renderToReadableStream(
+ model: ReactClientValue,
+ turbopackMap: ClientManifest,
+ options?: Options,
+): ReadableStream {
+ const request = createRequest(
+ model,
+ turbopackMap,
+ options ? options.onError : undefined,
+ options ? options.context : undefined,
+ options ? options.identifierPrefix : undefined,
+ options ? options.onPostpone : undefined,
+ );
+ if (options && options.signal) {
+ const signal = options.signal;
+ if (signal.aborted) {
+ abort(request, (signal: any).reason);
+ } else {
+ const listener = () => {
+ abort(request, (signal: any).reason);
+ signal.removeEventListener('abort', listener);
+ };
+ signal.addEventListener('abort', listener);
+ }
+ }
+ const stream = new ReadableStream(
+ {
+ type: 'bytes',
+ start: (controller): ?Promise => {
+ startWork(request);
+ },
+ pull: (controller): ?Promise => {
+ startFlowing(request, controller);
+ },
+ cancel: (reason): ?Promise => {},
+ },
+ // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
+ {highWaterMark: 0},
+ );
+ return stream;
+}
+
+function decodeReply(
+ body: string | FormData,
+ turbopackMap: ServerManifest,
+): Thenable {
+ if (typeof body === 'string') {
+ const form = new FormData();
+ form.append('0', body);
+ body = form;
+ }
+ const response = createResponse(turbopackMap, '', body);
+ close(response);
+ return getRoot(response);
+}
+
+export {renderToReadableStream, decodeReply, decodeAction};
diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMServerNode.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerNode.js
new file mode 100644
index 0000000000000..4cfe7a4044d61
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerNode.js
@@ -0,0 +1,170 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {
+ Request,
+ ReactClientValue,
+} from 'react-server/src/ReactFlightServer';
+import type {Destination} from 'react-server/src/ReactServerStreamConfigNode';
+import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
+import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
+import type {Busboy} from 'busboy';
+import type {Writable} from 'stream';
+import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes';
+
+import {
+ createRequest,
+ startWork,
+ startFlowing,
+ abort,
+} from 'react-server/src/ReactFlightServer';
+
+import {
+ createResponse,
+ reportGlobalError,
+ close,
+ resolveField,
+ resolveFileInfo,
+ resolveFileChunk,
+ resolveFileComplete,
+ getRoot,
+} from 'react-server/src/ReactFlightReplyServer';
+
+import {decodeAction} from 'react-server/src/ReactFlightActionServer';
+
+export {
+ registerServerReference,
+ registerClientReference,
+ createClientModuleProxy,
+} from './ReactFlightTurbopackReferences';
+
+function createDrainHandler(destination: Destination, request: Request) {
+ return () => startFlowing(request, destination);
+}
+
+type Options = {
+ onError?: (error: mixed) => void,
+ onPostpone?: (reason: string) => void,
+ context?: Array<[string, ServerContextJSONValue]>,
+ identifierPrefix?: string,
+};
+
+type PipeableStream = {
+ abort(reason: mixed): void,
+ pipe(destination: T): T,
+};
+
+function renderToPipeableStream(
+ model: ReactClientValue,
+ turbopackMap: ClientManifest,
+ options?: Options,
+): PipeableStream {
+ const request = createRequest(
+ model,
+ turbopackMap,
+ options ? options.onError : undefined,
+ options ? options.context : undefined,
+ options ? options.identifierPrefix : undefined,
+ options ? options.onPostpone : undefined,
+ );
+ let hasStartedFlowing = false;
+ startWork(request);
+ return {
+ pipe(destination: T): T {
+ if (hasStartedFlowing) {
+ throw new Error(
+ 'React currently only supports piping to one writable stream.',
+ );
+ }
+ hasStartedFlowing = true;
+ startFlowing(request, destination);
+ destination.on('drain', createDrainHandler(destination, request));
+ return destination;
+ },
+ abort(reason: mixed) {
+ abort(request, reason);
+ },
+ };
+}
+
+function decodeReplyFromBusboy(
+ busboyStream: Busboy,
+ turbopackMap: ServerManifest,
+): Thenable {
+ const response = createResponse(turbopackMap, '');
+ let pendingFiles = 0;
+ const queuedFields: Array = [];
+ busboyStream.on('field', (name, value) => {
+ if (pendingFiles > 0) {
+ // Because the 'end' event fires two microtasks after the next 'field'
+ // we would resolve files and fields out of order. To handle this properly
+ // we queue any fields we receive until the previous file is done.
+ queuedFields.push(name, value);
+ } else {
+ resolveField(response, name, value);
+ }
+ });
+ busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => {
+ if (encoding.toLowerCase() === 'base64') {
+ throw new Error(
+ "React doesn't accept base64 encoded file uploads because we don't expect " +
+ "form data passed from a browser to ever encode data that way. If that's " +
+ 'the wrong assumption, we can easily fix it.',
+ );
+ }
+ pendingFiles++;
+ const file = resolveFileInfo(response, name, filename, mimeType);
+ value.on('data', chunk => {
+ resolveFileChunk(response, file, chunk);
+ });
+ value.on('end', () => {
+ resolveFileComplete(response, name, file);
+ pendingFiles--;
+ if (pendingFiles === 0) {
+ // Release any queued fields
+ for (let i = 0; i < queuedFields.length; i += 2) {
+ resolveField(response, queuedFields[i], queuedFields[i + 1]);
+ }
+ queuedFields.length = 0;
+ }
+ });
+ });
+ busboyStream.on('finish', () => {
+ close(response);
+ });
+ busboyStream.on('error', err => {
+ reportGlobalError(
+ response,
+ // $FlowFixMe[incompatible-call] types Error and mixed are incompatible
+ err,
+ );
+ });
+ return getRoot(response);
+}
+
+function decodeReply(
+ body: string | FormData,
+ turbopackMap: ServerManifest,
+): Thenable {
+ if (typeof body === 'string') {
+ const form = new FormData();
+ form.append('0', body);
+ body = form;
+ }
+ const response = createResponse(turbopackMap, '', body);
+ close(response);
+ return getRoot(response);
+}
+
+export {
+ renderToPipeableStream,
+ decodeReplyFromBusboy,
+ decodeReply,
+ decodeAction,
+};
diff --git a/packages/react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler.js b/packages/react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler.js
new file mode 100644
index 0000000000000..73a2819a8cebd
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler.js
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
+import type {
+ ImportMetadata,
+ ImportManifestEntry,
+} from './shared/ReactFlightImportMetadata';
+
+import type {
+ ClientReference,
+ ServerReference,
+} from './ReactFlightTurbopackReferences';
+
+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 {
+ isClientReference,
+ isServerReference,
+} from './ReactFlightTurbopackReferences';
+
+export function getClientReferenceKey(
+ reference: ClientReference,
+): ClientReferenceKey {
+ return reference.$$async ? reference.$$id + '#async' : reference.$$id;
+}
+
+export function resolveClientReferenceMetadata(
+ config: ClientManifest,
+ clientReference: ClientReference,
+): ClientReferenceMetadata {
+ const modulePath = clientReference.$$id;
+ let name = '';
+ let resolvedModuleData = config[modulePath];
+ if (resolvedModuleData) {
+ // The potentially aliased name.
+ name = resolvedModuleData.name;
+ } else {
+ // We didn't find this specific export name but we might have the * export
+ // which contains this name as well.
+ // TODO: It's unfortunate that we now have to parse this string. We should
+ // probably go back to encoding path and name separately on the client reference.
+ const idx = modulePath.lastIndexOf('#');
+ if (idx !== -1) {
+ name = modulePath.slice(idx + 1);
+ resolvedModuleData = config[modulePath.slice(0, idx)];
+ }
+ if (!resolvedModuleData) {
+ throw new Error(
+ 'Could not find the module "' +
+ modulePath +
+ '" in the React Client Manifest. ' +
+ 'This is probably a bug in the React Server Components bundler.',
+ );
+ }
+ }
+ if (clientReference.$$async === true) {
+ return [resolvedModuleData.id, resolvedModuleData.chunks, name, 1];
+ } else {
+ return [resolvedModuleData.id, resolvedModuleData.chunks, name];
+ }
+}
+
+export function getServerReferenceId(
+ config: ClientManifest,
+ serverReference: ServerReference,
+): ServerReferenceId {
+ return serverReference.$$id;
+}
+
+export function getServerReferenceBoundArguments(
+ config: ClientManifest,
+ serverReference: ServerReference,
+): null | Array {
+ return serverReference.$$bound;
+}
diff --git a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js
new file mode 100644
index 0000000000000..066825857f44e
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js
@@ -0,0 +1,483 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import * as acorn from 'acorn-loose';
+
+type ResolveContext = {
+ conditions: Array,
+ parentURL: string | void,
+};
+
+type ResolveFunction = (
+ string,
+ ResolveContext,
+ ResolveFunction,
+) => {url: string} | Promise<{url: string}>;
+
+type GetSourceContext = {
+ format: string,
+};
+
+type GetSourceFunction = (
+ string,
+ GetSourceContext,
+ GetSourceFunction,
+) => Promise<{source: Source}>;
+
+type TransformSourceContext = {
+ format: string,
+ url: string,
+};
+
+type TransformSourceFunction = (
+ Source,
+ TransformSourceContext,
+ TransformSourceFunction,
+) => Promise<{source: Source}>;
+
+type LoadContext = {
+ conditions: Array,
+ format: string | null | void,
+ importAssertions: Object,
+};
+
+type LoadFunction = (
+ string,
+ LoadContext,
+ LoadFunction,
+) => Promise<{format: string, shortCircuit?: boolean, source: Source}>;
+
+type Source = string | ArrayBuffer | Uint8Array;
+
+let warnedAboutConditionsFlag = false;
+
+let stashedGetSource: null | GetSourceFunction = null;
+let stashedResolve: null | ResolveFunction = null;
+
+export async function resolve(
+ specifier: string,
+ context: ResolveContext,
+ defaultResolve: ResolveFunction,
+): Promise<{url: string}> {
+ // We stash this in case we end up needing to resolve export * statements later.
+ stashedResolve = defaultResolve;
+
+ if (!context.conditions.includes('react-server')) {
+ context = {
+ ...context,
+ conditions: [...context.conditions, 'react-server'],
+ };
+ if (!warnedAboutConditionsFlag) {
+ warnedAboutConditionsFlag = true;
+ // eslint-disable-next-line react-internal/no-production-logging
+ console.warn(
+ 'You did not run Node.js with the `--conditions react-server` flag. ' +
+ 'Any "react-server" override will only work with ESM imports.',
+ );
+ }
+ }
+ return await defaultResolve(specifier, context, defaultResolve);
+}
+
+export async function getSource(
+ url: string,
+ context: GetSourceContext,
+ defaultGetSource: GetSourceFunction,
+): Promise<{source: Source}> {
+ // We stash this in case we end up needing to resolve export * statements later.
+ stashedGetSource = defaultGetSource;
+ return defaultGetSource(url, context, defaultGetSource);
+}
+
+function addLocalExportedNames(names: Map, node: any) {
+ switch (node.type) {
+ case 'Identifier':
+ names.set(node.name, node.name);
+ return;
+ case 'ObjectPattern':
+ for (let i = 0; i < node.properties.length; i++)
+ addLocalExportedNames(names, node.properties[i]);
+ return;
+ case 'ArrayPattern':
+ for (let i = 0; i < node.elements.length; i++) {
+ const element = node.elements[i];
+ if (element) addLocalExportedNames(names, element);
+ }
+ return;
+ case 'Property':
+ addLocalExportedNames(names, node.value);
+ return;
+ case 'AssignmentPattern':
+ addLocalExportedNames(names, node.left);
+ return;
+ case 'RestElement':
+ addLocalExportedNames(names, node.argument);
+ return;
+ case 'ParenthesizedExpression':
+ addLocalExportedNames(names, node.expression);
+ return;
+ }
+}
+
+function transformServerModule(
+ source: string,
+ body: any,
+ url: string,
+ loader: LoadFunction,
+): string {
+ // If the same local name is exported more than once, we only need one of the names.
+ const localNames: Map = new Map();
+ const localTypes: Map = new Map();
+
+ for (let i = 0; i < body.length; i++) {
+ const node = body[i];
+ switch (node.type) {
+ case 'ExportAllDeclaration':
+ // If export * is used, the other file needs to explicitly opt into "use server" too.
+ break;
+ case 'ExportDefaultDeclaration':
+ if (node.declaration.type === 'Identifier') {
+ localNames.set(node.declaration.name, 'default');
+ } else if (node.declaration.type === 'FunctionDeclaration') {
+ if (node.declaration.id) {
+ localNames.set(node.declaration.id.name, 'default');
+ localTypes.set(node.declaration.id.name, 'function');
+ } else {
+ // TODO: This needs to be rewritten inline because it doesn't have a local name.
+ }
+ }
+ continue;
+ case 'ExportNamedDeclaration':
+ if (node.declaration) {
+ if (node.declaration.type === 'VariableDeclaration') {
+ const declarations = node.declaration.declarations;
+ for (let j = 0; j < declarations.length; j++) {
+ addLocalExportedNames(localNames, declarations[j].id);
+ }
+ } else {
+ const name = node.declaration.id.name;
+ localNames.set(name, name);
+ if (node.declaration.type === 'FunctionDeclaration') {
+ localTypes.set(name, 'function');
+ }
+ }
+ }
+ if (node.specifiers) {
+ const specifiers = node.specifiers;
+ for (let j = 0; j < specifiers.length; j++) {
+ const specifier = specifiers[j];
+ localNames.set(specifier.local.name, specifier.exported.name);
+ }
+ }
+ continue;
+ }
+ }
+ if (localNames.size === 0) {
+ return source;
+ }
+ let newSrc = source + '\n\n;';
+ newSrc +=
+ 'import {registerServerReference} from "react-server-dom-turbopack/server";\n';
+ localNames.forEach(function (exported, local) {
+ if (localTypes.get(local) !== 'function') {
+ // We first check if the export is a function and if so annotate it.
+ newSrc += 'if (typeof ' + local + ' === "function") ';
+ }
+ newSrc += 'registerServerReference(' + local + ',';
+ newSrc += JSON.stringify(url) + ',';
+ newSrc += JSON.stringify(exported) + ');\n';
+ });
+ return newSrc;
+}
+
+function addExportNames(names: Array, node: any) {
+ switch (node.type) {
+ case 'Identifier':
+ names.push(node.name);
+ return;
+ case 'ObjectPattern':
+ for (let i = 0; i < node.properties.length; i++)
+ addExportNames(names, node.properties[i]);
+ return;
+ case 'ArrayPattern':
+ for (let i = 0; i < node.elements.length; i++) {
+ const element = node.elements[i];
+ if (element) addExportNames(names, element);
+ }
+ return;
+ case 'Property':
+ addExportNames(names, node.value);
+ return;
+ case 'AssignmentPattern':
+ addExportNames(names, node.left);
+ return;
+ case 'RestElement':
+ addExportNames(names, node.argument);
+ return;
+ case 'ParenthesizedExpression':
+ addExportNames(names, node.expression);
+ return;
+ }
+}
+
+function resolveClientImport(
+ specifier: string,
+ parentURL: string,
+): {url: string} | Promise<{url: string}> {
+ // Resolve an import specifier as if it was loaded by the client. This doesn't use
+ // the overrides that this loader does but instead reverts to the default.
+ // This resolution algorithm will not necessarily have the same configuration
+ // as the actual client loader. It should mostly work and if it doesn't you can
+ // always convert to explicit exported names instead.
+ const conditions = ['node', 'import'];
+ if (stashedResolve === null) {
+ throw new Error(
+ 'Expected resolve to have been called before transformSource',
+ );
+ }
+ return stashedResolve(specifier, {conditions, parentURL}, stashedResolve);
+}
+
+async function parseExportNamesInto(
+ body: any,
+ names: Array,
+ parentURL: string,
+ loader: LoadFunction,
+): Promise {
+ for (let i = 0; i < body.length; i++) {
+ const node = body[i];
+ switch (node.type) {
+ case 'ExportAllDeclaration':
+ if (node.exported) {
+ addExportNames(names, node.exported);
+ continue;
+ } else {
+ const {url} = await resolveClientImport(node.source.value, parentURL);
+ const {source} = await loader(
+ url,
+ {format: 'module', conditions: [], importAssertions: {}},
+ loader,
+ );
+ if (typeof source !== 'string') {
+ throw new Error('Expected the transformed source to be a string.');
+ }
+ let childBody;
+ try {
+ childBody = acorn.parse(source, {
+ ecmaVersion: '2024',
+ sourceType: 'module',
+ }).body;
+ } catch (x) {
+ // eslint-disable-next-line react-internal/no-production-logging
+ console.error('Error parsing %s %s', url, x.message);
+ continue;
+ }
+ await parseExportNamesInto(childBody, names, url, loader);
+ continue;
+ }
+ case 'ExportDefaultDeclaration':
+ names.push('default');
+ continue;
+ case 'ExportNamedDeclaration':
+ if (node.declaration) {
+ if (node.declaration.type === 'VariableDeclaration') {
+ const declarations = node.declaration.declarations;
+ for (let j = 0; j < declarations.length; j++) {
+ addExportNames(names, declarations[j].id);
+ }
+ } else {
+ addExportNames(names, node.declaration.id);
+ }
+ }
+ if (node.specifiers) {
+ const specifiers = node.specifiers;
+ for (let j = 0; j < specifiers.length; j++) {
+ addExportNames(names, specifiers[j].exported);
+ }
+ }
+ continue;
+ }
+ }
+}
+
+async function transformClientModule(
+ body: any,
+ url: string,
+ loader: LoadFunction,
+): Promise {
+ const names: Array = [];
+
+ await parseExportNamesInto(body, names, url, loader);
+
+ if (names.length === 0) {
+ return '';
+ }
+
+ let newSrc =
+ 'import {registerClientReference} from "react-server-dom-turbopack/server";\n';
+ for (let i = 0; i < names.length; i++) {
+ const name = names[i];
+ if (name === 'default') {
+ newSrc += 'export default ';
+ newSrc += 'registerClientReference(function() {';
+ newSrc +=
+ 'throw new Error(' +
+ JSON.stringify(
+ `Attempted to call the default export of ${url} from the server` +
+ `but it's on the client. It's not possible to invoke a client function from ` +
+ `the server, it can only be rendered as a Component or passed to props of a` +
+ `Client Component.`,
+ ) +
+ ');';
+ } else {
+ newSrc += 'export const ' + name + ' = ';
+ newSrc += 'registerClientReference(function() {';
+ newSrc +=
+ 'throw new Error(' +
+ JSON.stringify(
+ `Attempted to call ${name}() from the server but ${name} is on the client. ` +
+ `It's not possible to invoke a client function from the server, it can ` +
+ `only be rendered as a Component or passed to props of a Client Component.`,
+ ) +
+ ');';
+ }
+ newSrc += '},';
+ newSrc += JSON.stringify(url) + ',';
+ newSrc += JSON.stringify(name) + ');\n';
+ }
+ return newSrc;
+}
+
+async function loadClientImport(
+ url: string,
+ defaultTransformSource: TransformSourceFunction,
+): Promise<{format: string, shortCircuit?: boolean, source: Source}> {
+ if (stashedGetSource === null) {
+ throw new Error(
+ 'Expected getSource to have been called before transformSource',
+ );
+ }
+ // TODO: Validate that this is another module by calling getFormat.
+ const {source} = await stashedGetSource(
+ url,
+ {format: 'module'},
+ stashedGetSource,
+ );
+ const result = await defaultTransformSource(
+ source,
+ {format: 'module', url},
+ defaultTransformSource,
+ );
+ return {format: 'module', source: result.source};
+}
+
+async function transformModuleIfNeeded(
+ source: string,
+ url: string,
+ loader: LoadFunction,
+): Promise {
+ // Do a quick check for the exact string. If it doesn't exist, don't
+ // bother parsing.
+ if (
+ source.indexOf('use client') === -1 &&
+ source.indexOf('use server') === -1
+ ) {
+ return source;
+ }
+
+ let body;
+ try {
+ body = acorn.parse(source, {
+ ecmaVersion: '2024',
+ sourceType: 'module',
+ }).body;
+ } catch (x) {
+ // eslint-disable-next-line react-internal/no-production-logging
+ console.error('Error parsing %s %s', url, x.message);
+ return source;
+ }
+
+ let useClient = false;
+ let useServer = false;
+ for (let i = 0; i < body.length; i++) {
+ const node = body[i];
+ if (node.type !== 'ExpressionStatement' || !node.directive) {
+ break;
+ }
+ if (node.directive === 'use client') {
+ useClient = true;
+ }
+ if (node.directive === 'use server') {
+ useServer = true;
+ }
+ }
+
+ if (!useClient && !useServer) {
+ return source;
+ }
+
+ if (useClient && useServer) {
+ throw new Error(
+ 'Cannot have both "use client" and "use server" directives in the same file.',
+ );
+ }
+
+ if (useClient) {
+ return transformClientModule(body, url, loader);
+ }
+
+ return transformServerModule(source, body, url, loader);
+}
+
+export async function transformSource(
+ source: Source,
+ context: TransformSourceContext,
+ defaultTransformSource: TransformSourceFunction,
+): Promise<{source: Source}> {
+ const transformed = await defaultTransformSource(
+ source,
+ context,
+ defaultTransformSource,
+ );
+ if (context.format === 'module') {
+ const transformedSource = transformed.source;
+ if (typeof transformedSource !== 'string') {
+ throw new Error('Expected source to have been transformed to a string.');
+ }
+ const newSrc = await transformModuleIfNeeded(
+ transformedSource,
+ context.url,
+ (url: string, ctx: LoadContext, defaultLoad: LoadFunction) => {
+ return loadClientImport(url, defaultTransformSource);
+ },
+ );
+ return {source: newSrc};
+ }
+ return transformed;
+}
+
+export async function load(
+ url: string,
+ context: LoadContext,
+ defaultLoad: LoadFunction,
+): Promise<{format: string, shortCircuit?: boolean, source: Source}> {
+ const result = await defaultLoad(url, context, defaultLoad);
+ if (result.format === 'module') {
+ if (typeof result.source !== 'string') {
+ throw new Error('Expected source to have been loaded into a string.');
+ }
+ const newSrc = await transformModuleIfNeeded(
+ result.source,
+ url,
+ defaultLoad,
+ );
+ return {format: 'module', source: newSrc};
+ }
+ return result;
+}
diff --git a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js
new file mode 100644
index 0000000000000..68c692530d6c5
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js
@@ -0,0 +1,110 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+const acorn = require('acorn-loose');
+
+const url = require('url');
+
+const Module = require('module');
+
+module.exports = function register() {
+ const Server: any = require('react-server-dom-turbopack/server');
+ const registerServerReference = Server.registerServerReference;
+ const createClientModuleProxy = Server.createClientModuleProxy;
+
+ // $FlowFixMe[prop-missing] found when upgrading Flow
+ const originalCompile = Module.prototype._compile;
+
+ // $FlowFixMe[prop-missing] found when upgrading Flow
+ Module.prototype._compile = function (
+ this: any,
+ content: string,
+ filename: string,
+ ): void {
+ // Do a quick check for the exact string. If it doesn't exist, don't
+ // bother parsing.
+ if (
+ content.indexOf('use client') === -1 &&
+ content.indexOf('use server') === -1
+ ) {
+ return originalCompile.apply(this, arguments);
+ }
+
+ let body;
+ try {
+ body = acorn.parse(content, {
+ ecmaVersion: '2024',
+ sourceType: 'source',
+ }).body;
+ } catch (x) {
+ // eslint-disable-next-line react-internal/no-production-logging
+ console.error('Error parsing %s %s', url, x.message);
+ return originalCompile.apply(this, arguments);
+ }
+
+ let useClient = false;
+ let useServer = false;
+ for (let i = 0; i < body.length; i++) {
+ const node = body[i];
+ if (node.type !== 'ExpressionStatement' || !node.directive) {
+ break;
+ }
+ if (node.directive === 'use client') {
+ useClient = true;
+ }
+ if (node.directive === 'use server') {
+ useServer = true;
+ }
+ }
+
+ if (!useClient && !useServer) {
+ return originalCompile.apply(this, arguments);
+ }
+
+ if (useClient && useServer) {
+ throw new Error(
+ 'Cannot have both "use client" and "use server" directives in the same file.',
+ );
+ }
+
+ if (useClient) {
+ const moduleId: string = (url.pathToFileURL(filename).href: any);
+ this.exports = createClientModuleProxy(moduleId);
+ }
+
+ if (useServer) {
+ originalCompile.apply(this, arguments);
+
+ const moduleId: string = (url.pathToFileURL(filename).href: any);
+
+ const exports = this.exports;
+
+ // This module is imported server to server, but opts in to exposing functions by
+ // reference. If there are any functions in the export.
+ if (typeof exports === 'function') {
+ // The module exports a function directly,
+ registerServerReference(
+ (exports: any),
+ moduleId,
+ // Represents the whole Module object instead of a particular import.
+ null,
+ );
+ } else {
+ const keys = Object.keys(exports);
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i];
+ const value = exports[keys[i]];
+ if (typeof value === 'function') {
+ registerServerReference((value: any), moduleId, key);
+ }
+ }
+ }
+ }
+ };
+};
diff --git a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js
new file mode 100644
index 0000000000000..9df0e43bd75e1
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js
@@ -0,0 +1,256 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
+
+export type ServerReference = T & {
+ $$typeof: symbol,
+ $$id: string,
+ $$bound: null | Array,
+};
+
+// eslint-disable-next-line no-unused-vars
+export type ClientReference = {
+ $$typeof: symbol,
+ $$id: string,
+ $$async: boolean,
+};
+
+const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference');
+const SERVER_REFERENCE_TAG = Symbol.for('react.server.reference');
+
+export function isClientReference(reference: Object): boolean {
+ return reference.$$typeof === CLIENT_REFERENCE_TAG;
+}
+
+export function isServerReference(reference: Object): boolean {
+ return reference.$$typeof === SERVER_REFERENCE_TAG;
+}
+
+export function registerClientReference(
+ proxyImplementation: any,
+ id: string,
+ exportName: string,
+): ClientReference {
+ return registerClientReferenceImpl(
+ proxyImplementation,
+ id + '#' + exportName,
+ false,
+ );
+}
+
+function registerClientReferenceImpl(
+ proxyImplementation: any,
+ id: string,
+ async: boolean,
+): ClientReference {
+ return Object.defineProperties(proxyImplementation, {
+ $$typeof: {value: CLIENT_REFERENCE_TAG},
+ $$id: {value: id},
+ $$async: {value: async},
+ });
+}
+
+// $FlowFixMe[method-unbinding]
+const FunctionBind = Function.prototype.bind;
+// $FlowFixMe[method-unbinding]
+const ArraySlice = Array.prototype.slice;
+function bind(this: ServerReference) {
+ // $FlowFixMe[unsupported-syntax]
+ const newFn = FunctionBind.apply(this, arguments);
+ if (this.$$typeof === SERVER_REFERENCE_TAG) {
+ const args = ArraySlice.call(arguments, 1);
+ newFn.$$typeof = SERVER_REFERENCE_TAG;
+ newFn.$$id = this.$$id;
+ newFn.$$bound = this.$$bound ? this.$$bound.concat(args) : args;
+ }
+ return newFn;
+}
+
+export function registerServerReference(
+ reference: ServerReference,
+ id: string,
+ exportName: null | string,
+): ServerReference {
+ return Object.defineProperties((reference: any), {
+ $$typeof: {value: SERVER_REFERENCE_TAG},
+ $$id: {value: exportName === null ? id : id + '#' + exportName},
+ $$bound: {value: null},
+ bind: {value: bind},
+ });
+}
+
+const PROMISE_PROTOTYPE = Promise.prototype;
+
+const deepProxyHandlers = {
+ get: function (target: Function, name: string, receiver: Proxy) {
+ switch (name) {
+ // These names are read by the Flight runtime if you end up using the exports object.
+ case '$$typeof':
+ // These names are a little too common. We should probably have a way to
+ // have the Flight runtime extract the inner target instead.
+ return target.$$typeof;
+ case '$$id':
+ return target.$$id;
+ case '$$async':
+ return target.$$async;
+ case 'name':
+ return target.name;
+ case 'displayName':
+ return undefined;
+ // We need to special case this because createElement reads it if we pass this
+ // reference.
+ case 'defaultProps':
+ return undefined;
+ // Avoid this attempting to be serialized.
+ case 'toJSON':
+ return undefined;
+ case Symbol.toPrimitive:
+ // $FlowFixMe[prop-missing]
+ return Object.prototype[Symbol.toPrimitive];
+ case 'Provider':
+ throw new Error(
+ `Cannot render a Client Context Provider on the Server. ` +
+ `Instead, you can export a Client Component wrapper ` +
+ `that itself renders a Client Context Provider.`,
+ );
+ }
+ // eslint-disable-next-line react-internal/safe-string-coercion
+ const expression = String(target.name) + '.' + String(name);
+ throw new Error(
+ `Cannot access ${expression} on the server. ` +
+ 'You cannot dot into a client module from a server component. ' +
+ 'You can only pass the imported name through.',
+ );
+ },
+ set: function () {
+ throw new Error('Cannot assign to a client module from a server module.');
+ },
+};
+
+const proxyHandlers = {
+ get: function (
+ target: Function,
+ name: string,
+ receiver: Proxy,
+ ): $FlowFixMe {
+ switch (name) {
+ // These names are read by the Flight runtime if you end up using the exports object.
+ case '$$typeof':
+ return target.$$typeof;
+ case '$$id':
+ return target.$$id;
+ case '$$async':
+ return target.$$async;
+ case 'name':
+ return target.name;
+ // We need to special case this because createElement reads it if we pass this
+ // reference.
+ case 'defaultProps':
+ return undefined;
+ // Avoid this attempting to be serialized.
+ case 'toJSON':
+ return undefined;
+ case Symbol.toPrimitive:
+ // $FlowFixMe[prop-missing]
+ return Object.prototype[Symbol.toPrimitive];
+ case '__esModule':
+ // Something is conditionally checking which export to use. We'll pretend to be
+ // an ESM compat module but then we'll check again on the client.
+ const moduleId = target.$$id;
+ target.default = registerClientReferenceImpl(
+ (function () {
+ throw new Error(
+ `Attempted to call the default export of ${moduleId} from the server ` +
+ `but it's on the client. It's not possible to invoke a client function from ` +
+ `the server, it can only be rendered as a Component or passed to props of a ` +
+ `Client Component.`,
+ );
+ }: any),
+ target.$$id + '#',
+ target.$$async,
+ );
+ return true;
+ case 'then':
+ if (target.then) {
+ // Use a cached value
+ return target.then;
+ }
+ if (!target.$$async) {
+ // If this module is expected to return a Promise (such as an AsyncModule) then
+ // we should resolve that with a client reference that unwraps the Promise on
+ // the client.
+
+ const clientReference: ClientReference =
+ registerClientReferenceImpl(({}: any), target.$$id, true);
+ const proxy = new Proxy(clientReference, proxyHandlers);
+
+ // Treat this as a resolved Promise for React's use()
+ target.status = 'fulfilled';
+ target.value = proxy;
+
+ const then = (target.then = registerClientReferenceImpl(
+ (function then(resolve, reject: any) {
+ // Expose to React.
+ return Promise.resolve(resolve(proxy));
+ }: any),
+ // If this is not used as a Promise but is treated as a reference to a `.then`
+ // export then we should treat it as a reference to that name.
+ target.$$id + '#then',
+ false,
+ ));
+ return then;
+ } else {
+ // Since typeof .then === 'function' is a feature test we'd continue recursing
+ // indefinitely if we return a function. Instead, we return an object reference
+ // if we check further.
+ return undefined;
+ }
+ }
+ let cachedReference = target[name];
+ if (!cachedReference) {
+ const reference: ClientReference = registerClientReferenceImpl(
+ (function () {
+ throw new Error(
+ // eslint-disable-next-line react-internal/safe-string-coercion
+ `Attempted to call ${String(name)}() from the server but ${String(
+ name,
+ )} is on the client. ` +
+ `It's not possible to invoke a client function from the server, it can ` +
+ `only be rendered as a Component or passed to props of a Client Component.`,
+ );
+ }: any),
+ target.$$id + '#' + name,
+ target.$$async,
+ );
+ Object.defineProperty((reference: any), 'name', {value: name});
+ cachedReference = target[name] = new Proxy(reference, deepProxyHandlers);
+ }
+ return cachedReference;
+ },
+ getPrototypeOf(target: Function): Object {
+ // Pretend to be a Promise in case anyone asks.
+ return PROMISE_PROTOTYPE;
+ },
+ set: function (): empty {
+ throw new Error('Cannot assign to a client module from a server module.');
+ },
+};
+
+export function createClientModuleProxy(
+ moduleId: string,
+): ClientReference {
+ const clientReference: ClientReference = registerClientReferenceImpl(
+ ({}: any),
+ // Represents the whole Module object instead of a particular import.
+ moduleId,
+ false,
+ );
+ return new Proxy(clientReference, proxyHandlers);
+}
diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js
new file mode 100644
index 0000000000000..dc57bd0e94255
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js
@@ -0,0 +1,1572 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+// Polyfills for test environment
+global.ReadableStream =
+ require('web-streams-polyfill/ponyfill/es6').ReadableStream;
+global.TextEncoder = require('util').TextEncoder;
+global.TextDecoder = require('util').TextDecoder;
+
+// Don't wait before processing work on the server.
+// TODO: we can replace this with FlightServer.act().
+global.setImmediate = cb => cb();
+
+let act;
+let use;
+let clientExports;
+let clientModuleError;
+let turbopackMap;
+let Stream;
+let FlightReact;
+let React;
+let FlightReactDOM;
+let ReactDOMClient;
+let ReactServerDOMServer;
+let ReactServerDOMClient;
+let ReactDOMFizzServer;
+let Suspense;
+let ErrorBoundary;
+let JSDOM;
+
+describe('ReactFlightDOM', () => {
+ beforeEach(() => {
+ // For this first reset we are going to load the dom-node version of react-server-dom-turbopack/server
+ // This can be thought of as essentially being the React Server Components scope with react-server
+ // condition
+ jest.resetModules();
+
+ JSDOM = require('jsdom').JSDOM;
+
+ // Simulate the condition resolution
+ jest.mock('react-server-dom-turbopack/server', () =>
+ require('react-server-dom-turbopack/server.node.unbundled'),
+ );
+ jest.mock('react', () => require('react/react.shared-subset'));
+
+ const TurbopackMock = require('./utils/TurbopackMock');
+ clientExports = TurbopackMock.clientExports;
+ clientModuleError = TurbopackMock.clientModuleError;
+ turbopackMap = TurbopackMock.turbopackMap;
+
+ ReactServerDOMServer = require('react-server-dom-turbopack/server');
+ FlightReact = require('react');
+ FlightReactDOM = require('react-dom');
+
+ // This reset is to load modules for the SSR/Browser scope.
+ jest.resetModules();
+ jest.unmock('react');
+ act = require('internal-test-utils').act;
+ Stream = require('stream');
+ React = require('react');
+ use = React.use;
+ Suspense = React.Suspense;
+ ReactDOMClient = require('react-dom/client');
+ ReactDOMFizzServer = require('react-dom/server.node');
+ ReactServerDOMClient = require('react-server-dom-turbopack/client');
+
+ ErrorBoundary = class extends React.Component {
+ state = {hasError: false, error: null};
+ static getDerivedStateFromError(error) {
+ return {
+ hasError: true,
+ error,
+ };
+ }
+ render() {
+ if (this.state.hasError) {
+ return this.props.fallback(this.state.error);
+ }
+ return this.props.children;
+ }
+ };
+ });
+
+ function getTestStream() {
+ const writable = new Stream.PassThrough();
+ const readable = new ReadableStream({
+ start(controller) {
+ writable.on('data', chunk => {
+ controller.enqueue(chunk);
+ });
+ writable.on('end', () => {
+ controller.close();
+ });
+ },
+ });
+ return {
+ readable,
+ writable,
+ };
+ }
+
+ const theInfinitePromise = new Promise(() => {});
+ function InfiniteSuspend() {
+ throw theInfinitePromise;
+ }
+
+ function getMeaningfulChildren(element) {
+ const children = [];
+ let node = element.firstChild;
+ while (node) {
+ if (node.nodeType === 1) {
+ if (
+ // some tags are ambiguous and might be hidden because they look like non-meaningful children
+ // so we have a global override where if this data attribute is included we also include the node
+ node.hasAttribute('data-meaningful') ||
+ (node.tagName === 'SCRIPT' &&
+ node.hasAttribute('src') &&
+ node.hasAttribute('async')) ||
+ (node.tagName !== 'SCRIPT' &&
+ node.tagName !== 'TEMPLATE' &&
+ node.tagName !== 'template' &&
+ !node.hasAttribute('hidden') &&
+ !node.hasAttribute('aria-hidden'))
+ ) {
+ const props = {};
+ const attributes = node.attributes;
+ for (let i = 0; i < attributes.length; i++) {
+ if (
+ attributes[i].name === 'id' &&
+ attributes[i].value.includes(':')
+ ) {
+ // We assume this is a React added ID that's a non-visual implementation detail.
+ continue;
+ }
+ props[attributes[i].name] = attributes[i].value;
+ }
+ props.children = getMeaningfulChildren(node);
+ children.push(React.createElement(node.tagName.toLowerCase(), props));
+ }
+ } else if (node.nodeType === 3) {
+ children.push(node.data);
+ }
+ node = node.nextSibling;
+ }
+ return children.length === 0
+ ? undefined
+ : children.length === 1
+ ? children[0]
+ : children;
+ }
+
+ it('should resolve HTML using Node streams', async () => {
+ function Text({children}) {
+ return {children} ;
+ }
+ function HTML() {
+ return (
+
+ hello
+ world
+
+ );
+ }
+
+ function App() {
+ const model = {
+ html: ,
+ };
+ return model;
+ }
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ );
+ pipe(writable);
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+ const model = await response;
+ expect(model).toEqual({
+ html: (
+
+ hello
+ world
+
+ ),
+ });
+ });
+
+ it('should resolve the root', async () => {
+ // Model
+ function Text({children}) {
+ return {children} ;
+ }
+ function HTML() {
+ return (
+
+ hello
+ world
+
+ );
+ }
+ function RootModel() {
+ return {
+ html: ,
+ };
+ }
+
+ // View
+ function Message({response}) {
+ return ;
+ }
+ function App({response}) {
+ return (
+ Loading...}>
+
+
+ );
+ }
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ );
+ pipe(writable);
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render( );
+ });
+ expect(container.innerHTML).toBe(
+ '',
+ );
+ });
+
+ it('should not get confused by $', async () => {
+ // Model
+ function RootModel() {
+ return {text: '$1'};
+ }
+
+ // View
+ function Message({response}) {
+ return {use(response).text}
;
+ }
+ function App({response}) {
+ return (
+ Loading...}>
+
+
+ );
+ }
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ );
+ pipe(writable);
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render( );
+ });
+ expect(container.innerHTML).toBe('$1
');
+ });
+
+ it('should not get confused by @', async () => {
+ // Model
+ function RootModel() {
+ return {text: '@div'};
+ }
+
+ // View
+ function Message({response}) {
+ return {use(response).text}
;
+ }
+ function App({response}) {
+ return (
+ Loading...}>
+
+
+ );
+ }
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ );
+ pipe(writable);
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render( );
+ });
+ expect(container.innerHTML).toBe('@div
');
+ });
+
+ it('should be able to esm compat test module references', async () => {
+ const ESMCompatModule = {
+ __esModule: true,
+ default: function ({greeting}) {
+ return greeting + ' World';
+ },
+ hi: 'Hello',
+ };
+
+ function Print({response}) {
+ return {use(response)}
;
+ }
+
+ function App({response}) {
+ return (
+ Loading...}>
+
+
+ );
+ }
+
+ function interopWebpack(obj) {
+ // Basically what Webpack's ESM interop feature testing does.
+ if (typeof obj === 'object' && obj.__esModule) {
+ return obj;
+ }
+ return Object.assign({default: obj}, obj);
+ }
+
+ const {default: Component, hi} = interopWebpack(
+ clientExports(ESMCompatModule),
+ );
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ );
+ pipe(writable);
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render( );
+ });
+ expect(container.innerHTML).toBe('Hello World
');
+ });
+
+ it('should be able to render a named component export', async () => {
+ const Module = {
+ Component: function ({greeting}) {
+ return greeting + ' World';
+ },
+ };
+
+ function Print({response}) {
+ return {use(response)}
;
+ }
+
+ function App({response}) {
+ return (
+ Loading...}>
+
+
+ );
+ }
+
+ const {Component} = clientExports(Module);
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ );
+ pipe(writable);
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render( );
+ });
+ expect(container.innerHTML).toBe('Hello World
');
+ });
+
+ it('should be able to render a module split named component export', async () => {
+ const Module = {
+ // This gets split into a separate module from the original one.
+ split: function ({greeting}) {
+ return greeting + ' World';
+ },
+ };
+
+ function Print({response}) {
+ return {use(response)}
;
+ }
+
+ function App({response}) {
+ return (
+ Loading...}>
+
+
+ );
+ }
+
+ const {split: Component} = clientExports(Module);
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ );
+ pipe(writable);
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render( );
+ });
+ expect(container.innerHTML).toBe('Hello World
');
+ });
+
+ it('should unwrap async module references', async () => {
+ const AsyncModule = Promise.resolve(function AsyncModule({text}) {
+ return 'Async: ' + text;
+ });
+
+ const AsyncModule2 = Promise.resolve({
+ exportName: 'Module',
+ });
+
+ function Print({response}) {
+ return {use(response)}
;
+ }
+
+ function App({response}) {
+ return (
+ Loading...}>
+
+
+ );
+ }
+
+ const AsyncModuleRef = await clientExports(AsyncModule);
+ const AsyncModuleRef2 = await clientExports(AsyncModule2);
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ );
+ pipe(writable);
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render( );
+ });
+ expect(container.innerHTML).toBe('Async: Module
');
+ });
+
+ it('should unwrap async module references using use', async () => {
+ const AsyncModule = Promise.resolve('Async Text');
+
+ function Print({response}) {
+ return use(response);
+ }
+
+ function App({response}) {
+ return (
+ Loading...}>
+
+
+ );
+ }
+
+ const AsyncModuleRef = clientExports(AsyncModule);
+
+ function ServerComponent() {
+ const text = FlightReact.use(AsyncModuleRef);
+ return {text}
;
+ }
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ );
+ pipe(writable);
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render( );
+ });
+ expect(container.innerHTML).toBe('Async Text
');
+ });
+
+ it('should be able to import a name called "then"', async () => {
+ const thenExports = {
+ then: function then() {
+ return 'and then';
+ },
+ };
+
+ function Print({response}) {
+ return {use(response)}
;
+ }
+
+ function App({response}) {
+ return (
+ Loading...}>
+
+
+ );
+ }
+
+ const ThenRef = clientExports(thenExports).then;
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ );
+ pipe(writable);
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render( );
+ });
+ expect(container.innerHTML).toBe('and then
');
+ });
+
+ it('throws when accessing a member below the client exports', () => {
+ const ClientModule = clientExports({
+ Component: {deep: 'thing'},
+ });
+ function dotting() {
+ return ClientModule.Component.deep;
+ }
+ expect(dotting).toThrowError(
+ 'Cannot access Component.deep on the server. ' +
+ 'You cannot dot into a client module from a server component. ' +
+ 'You can only pass the imported name through.',
+ );
+ });
+
+ it('does not throw when React inspects any deep props', () => {
+ const ClientModule = clientExports({
+ Component: function () {},
+ });
+ ;
+ });
+
+ it('throws when accessing a Context.Provider below the client exports', () => {
+ const Context = React.createContext();
+ const ClientModule = clientExports({
+ Context,
+ });
+ function dotting() {
+ return ClientModule.Context.Provider;
+ }
+ expect(dotting).toThrowError(
+ `Cannot render a Client Context Provider on the Server. ` +
+ `Instead, you can export a Client Component wrapper ` +
+ `that itself renders a Client Context Provider.`,
+ );
+ });
+
+ it('should progressively reveal server components', async () => {
+ let reportedErrors = [];
+
+ // Client Components
+
+ function MyErrorBoundary({children}) {
+ return (
+ (
+
+ {__DEV__ ? e.message + ' + ' : null}
+ {e.digest}
+
+ )}>
+ {children}
+
+ );
+ }
+
+ // Model
+ function Text({children}) {
+ return children;
+ }
+
+ function makeDelayedText() {
+ let _resolve, _reject;
+ let promise = new Promise((resolve, reject) => {
+ _resolve = () => {
+ promise = null;
+ resolve();
+ };
+ _reject = e => {
+ promise = null;
+ reject(e);
+ };
+ });
+ async function DelayedText({children}) {
+ await promise;
+ return {children} ;
+ }
+ return [DelayedText, _resolve, _reject];
+ }
+
+ const [Friends, resolveFriends] = makeDelayedText();
+ const [Name, resolveName] = makeDelayedText();
+ const [Posts, resolvePosts] = makeDelayedText();
+ const [Photos, resolvePhotos] = makeDelayedText();
+ const [Games, , rejectGames] = makeDelayedText();
+
+ // View
+ function ProfileDetails({avatar}) {
+ return (
+
+ :name:
+ {avatar}
+
+ );
+ }
+ function ProfileSidebar({friends}) {
+ return (
+
+ );
+ }
+ function ProfilePosts({posts}) {
+ return {posts}
;
+ }
+ function ProfileGames({games}) {
+ return {games}
;
+ }
+
+ const MyErrorBoundaryClient = clientExports(MyErrorBoundary);
+
+ function ProfileContent() {
+ return (
+ <>
+ :avatar:} />
+ (loading sidebar)
}>
+ :friends:} />
+
+ (loading posts)}>
+ :posts:} />
+
+
+ (loading games)}>
+ :games:} />
+
+
+ >
+ );
+ }
+
+ const model = {
+ rootContent: ,
+ };
+
+ function ProfilePage({response}) {
+ return use(response).rootContent;
+ }
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+ model,
+ turbopackMap,
+ {
+ onError(x) {
+ reportedErrors.push(x);
+ return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
+ },
+ },
+ );
+ pipe(writable);
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render(
+ (loading)}>
+
+ ,
+ );
+ });
+ expect(container.innerHTML).toBe('(loading)
');
+
+ // This isn't enough to show anything.
+ await act(() => {
+ resolveFriends();
+ });
+ expect(container.innerHTML).toBe('(loading)
');
+
+ // We can now show the details. Sidebar and posts are still loading.
+ await act(() => {
+ resolveName();
+ });
+ // Advance time enough to trigger a nested fallback.
+ await act(() => {
+ jest.advanceTimersByTime(500);
+ });
+ expect(container.innerHTML).toBe(
+ ':name::avatar:
' +
+ '(loading sidebar)
' +
+ '(loading posts)
' +
+ '(loading games)
',
+ );
+
+ expect(reportedErrors).toEqual([]);
+
+ const theError = new Error('Game over');
+ // Let's *fail* loading games.
+ await act(async () => {
+ await rejectGames(theError);
+ await 'the inner async function';
+ });
+ const expectedGamesValue = __DEV__
+ ? 'Game over + a dev digest
'
+ : 'digest("Game over")
';
+ expect(container.innerHTML).toBe(
+ ':name::avatar:
' +
+ '(loading sidebar)
' +
+ '(loading posts)
' +
+ expectedGamesValue,
+ );
+
+ expect(reportedErrors).toEqual([theError]);
+ reportedErrors = [];
+
+ // We can now show the sidebar.
+ await act(async () => {
+ await resolvePhotos();
+ await 'the inner async function';
+ });
+ expect(container.innerHTML).toBe(
+ ':name::avatar:
' +
+ ':photos::friends:
' +
+ '(loading posts)
' +
+ expectedGamesValue,
+ );
+
+ // Show everything.
+ await act(async () => {
+ await resolvePosts();
+ await 'the inner async function';
+ });
+ expect(container.innerHTML).toBe(
+ ':name::avatar:
' +
+ ':photos::friends:
' +
+ ':posts:
' +
+ expectedGamesValue,
+ );
+
+ expect(reportedErrors).toEqual([]);
+ });
+
+ it('should preserve state of client components on refetch', async () => {
+ // Client
+
+ function Page({response}) {
+ return use(response);
+ }
+
+ function Input() {
+ return ;
+ }
+
+ const InputClient = clientExports(Input);
+
+ // Server
+
+ function App({color}) {
+ // Verify both DOM and Client children.
+ return (
+
+
+
+
+ );
+ }
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+
+ const stream1 = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ );
+ pipe(stream1.writable);
+ const response1 = ReactServerDOMClient.createFromReadableStream(
+ stream1.readable,
+ );
+ await act(() => {
+ root.render(
+ (loading)}>
+
+ ,
+ );
+ });
+ expect(container.children.length).toBe(1);
+ expect(container.children[0].tagName).toBe('DIV');
+ expect(container.children[0].style.color).toBe('red');
+
+ // Change the DOM state for both inputs.
+ const inputA = container.children[0].children[0];
+ expect(inputA.tagName).toBe('INPUT');
+ inputA.value = 'hello';
+ const inputB = container.children[0].children[1];
+ expect(inputB.tagName).toBe('INPUT');
+ inputB.value = 'goodbye';
+
+ const stream2 = getTestStream();
+ const {pipe: pipe2} = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ );
+ pipe2(stream2.writable);
+ const response2 = ReactServerDOMClient.createFromReadableStream(
+ stream2.readable,
+ );
+ await act(() => {
+ root.render(
+ (loading)}>
+
+ ,
+ );
+ });
+ expect(container.children.length).toBe(1);
+ expect(container.children[0].tagName).toBe('DIV');
+ expect(container.children[0].style.color).toBe('blue');
+
+ // Verify we didn't destroy the DOM for either input.
+ expect(inputA === container.children[0].children[0]).toBe(true);
+ expect(inputA.tagName).toBe('INPUT');
+ expect(inputA.value).toBe('hello');
+ expect(inputB === container.children[0].children[1]).toBe(true);
+ expect(inputB.tagName).toBe('INPUT');
+ expect(inputB.value).toBe('goodbye');
+ });
+
+ it('should be able to complete after aborting and throw the reason client-side', async () => {
+ const reportedErrors = [];
+
+ const {writable, readable} = getTestStream();
+ const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
+
+
+
,
+ turbopackMap,
+ {
+ onError(x) {
+ reportedErrors.push(x);
+ const message = typeof x === 'string' ? x : x.message;
+ return __DEV__ ? 'a dev digest' : `digest("${message}")`;
+ },
+ },
+ );
+ pipe(writable);
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+
+ function App({res}) {
+ return use(res);
+ }
+
+ await act(() => {
+ root.render(
+ (
+
+ {__DEV__ ? e.message + ' + ' : null}
+ {e.digest}
+
+ )}>
+ (loading)}>
+
+
+ ,
+ );
+ });
+ expect(container.innerHTML).toBe('(loading)
');
+
+ await act(() => {
+ abort('for reasons');
+ });
+ if (__DEV__) {
+ expect(container.innerHTML).toBe(
+ 'Error: for reasons + a dev digest
',
+ );
+ } else {
+ expect(container.innerHTML).toBe('digest("for reasons")
');
+ }
+
+ expect(reportedErrors).toEqual(['for reasons']);
+ });
+
+ it('should be able to recover from a direct reference erroring client-side', async () => {
+ const reportedErrors = [];
+
+ const ClientComponent = clientExports(function ({prop}) {
+ return 'This should never render';
+ });
+
+ const ClientReference = clientModuleError(new Error('module init error'));
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+
+
+
,
+ turbopackMap,
+ {
+ onError(x) {
+ reportedErrors.push(x);
+ },
+ },
+ );
+ pipe(writable);
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+
+ function App({res}) {
+ return use(res);
+ }
+
+ await act(() => {
+ root.render(
+ {e.message}
}>
+ (loading)}>
+
+
+ ,
+ );
+ });
+ expect(container.innerHTML).toBe('module init error
');
+
+ expect(reportedErrors).toEqual([]);
+ });
+
+ it('should be able to recover from a direct reference erroring client-side async', async () => {
+ const reportedErrors = [];
+
+ const ClientComponent = clientExports(function ({prop}) {
+ return 'This should never render';
+ });
+
+ let rejectPromise;
+ const ClientReference = await clientExports(
+ new Promise((resolve, reject) => {
+ rejectPromise = reject;
+ }),
+ );
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+
+
+
,
+ turbopackMap,
+ {
+ onError(x) {
+ reportedErrors.push(x);
+ },
+ },
+ );
+ pipe(writable);
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+
+ function App({res}) {
+ return use(res);
+ }
+
+ await act(() => {
+ root.render(
+ {e.message}
}>
+ (loading)}>
+
+
+ ,
+ );
+ });
+
+ expect(container.innerHTML).toBe('(loading)
');
+
+ await act(() => {
+ rejectPromise(new Error('async module init error'));
+ });
+
+ expect(container.innerHTML).toBe('async module init error
');
+
+ expect(reportedErrors).toEqual([]);
+ });
+
+ it('should be able to recover from a direct reference erroring server-side', async () => {
+ const reportedErrors = [];
+
+ const ClientComponent = clientExports(function ({prop}) {
+ return 'This should never render';
+ });
+
+ // We simulate a bug in the Webpack bundler which causes an error on the server.
+ for (const id in turbopackMap) {
+ Object.defineProperty(turbopackMap, id, {
+ get: () => {
+ throw new Error('bug in the bundler');
+ },
+ });
+ }
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+
+
+
,
+ turbopackMap,
+ {
+ onError(x) {
+ reportedErrors.push(x.message);
+ return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
+ },
+ },
+ );
+ pipe(writable);
+
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+
+ function App({res}) {
+ return use(res);
+ }
+
+ await act(() => {
+ root.render(
+ (
+
+ {__DEV__ ? e.message + ' + ' : null}
+ {e.digest}
+
+ )}>
+ (loading)}>
+
+
+ ,
+ );
+ });
+ if (__DEV__) {
+ expect(container.innerHTML).toBe(
+ 'bug in the bundler + a dev digest
',
+ );
+ } else {
+ expect(container.innerHTML).toBe('digest("bug in the bundler")
');
+ }
+
+ expect(reportedErrors).toEqual(['bug in the bundler']);
+ });
+
+ it('should pass a Promise through props and be able use() it on the client', async () => {
+ async function getData() {
+ return 'async hello';
+ }
+
+ function Component({data}) {
+ const text = use(data);
+ return {text}
;
+ }
+
+ const ClientComponent = clientExports(Component);
+
+ function ServerComponent() {
+ const data = getData(); // no await here
+ return ;
+ }
+
+ function Print({response}) {
+ return use(response);
+ }
+
+ function App({response}) {
+ return (
+ Loading...}>
+
+
+ );
+ }
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ );
+ pipe(writable);
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render( );
+ });
+ expect(container.innerHTML).toBe('async hello
');
+ });
+
+ it('should throw on the client if a passed promise eventually rejects', async () => {
+ const reportedErrors = [];
+ const theError = new Error('Server throw');
+
+ async function getData() {
+ throw theError;
+ }
+
+ function Component({data}) {
+ const text = use(data);
+ return {text}
;
+ }
+
+ const ClientComponent = clientExports(Component);
+
+ function ServerComponent() {
+ const data = getData(); // no await here
+ return ;
+ }
+
+ function Await({response}) {
+ return use(response);
+ }
+
+ function App({response}) {
+ return (
+ Loading...}>
+ (
+
+ {__DEV__ ? e.message + ' + ' : null}
+ {e.digest}
+
+ )}>
+
+
+
+ );
+ }
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ {
+ onError(x) {
+ reportedErrors.push(x);
+ return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
+ },
+ },
+ );
+ pipe(writable);
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render( );
+ });
+ expect(container.innerHTML).toBe(
+ __DEV__
+ ? 'Server throw + a dev digest
'
+ : 'digest("Server throw")
',
+ );
+ expect(reportedErrors).toEqual([theError]);
+ });
+
+ it('should support float methods when rendering in Fiber', async () => {
+ function Component() {
+ return hello world
;
+ }
+
+ const ClientComponent = clientExports(Component);
+
+ async function ServerComponent() {
+ FlightReactDOM.prefetchDNS('d before');
+ FlightReactDOM.preconnect('c before');
+ FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
+ FlightReactDOM.preload('l before', {as: 'style'});
+ FlightReactDOM.preloadModule('lm before');
+ FlightReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'});
+ FlightReactDOM.preinit('i before', {as: 'script'});
+ FlightReactDOM.preinitModule('m before');
+ FlightReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'});
+ await 1;
+ FlightReactDOM.prefetchDNS('d after');
+ FlightReactDOM.preconnect('c after');
+ FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
+ FlightReactDOM.preload('l after', {as: 'style'});
+ FlightReactDOM.preloadModule('lm after');
+ FlightReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'});
+ FlightReactDOM.preinit('i after', {as: 'script'});
+ FlightReactDOM.preinitModule('m after');
+ FlightReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'});
+ return ;
+ }
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ );
+ pipe(writable);
+
+ let response = null;
+ function getResponse() {
+ if (response === null) {
+ response = ReactServerDOMClient.createFromReadableStream(readable);
+ }
+ return response;
+ }
+
+ function App() {
+ return getResponse();
+ }
+
+ // We pause to allow the float call after the await point to process before the
+ // HostDispatcher gets set for Fiber by createRoot. This is only needed in testing
+ // because the module graphs are not different and the HostDispatcher is shared.
+ // In a real environment the Fiber and Flight code would each have their own independent
+ // dispatcher.
+ // @TODO consider what happens when Server-Components-On-The-Client exist. we probably
+ // want to use the Fiber HostDispatcher there too since it is more about the host than the runtime
+ // but we need to make sure that actually makes sense
+ await 1;
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render( );
+ });
+
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+ expect(getMeaningfulChildren(container)).toEqual(hello world
);
+ });
+
+ it('should support float methods when rendering in Fizz', async () => {
+ function Component() {
+ return hello world
;
+ }
+
+ const ClientComponent = clientExports(Component);
+
+ async function ServerComponent() {
+ FlightReactDOM.prefetchDNS('d before');
+ FlightReactDOM.preconnect('c before');
+ FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
+ FlightReactDOM.preload('l before', {as: 'style'});
+ FlightReactDOM.preloadModule('lm before');
+ FlightReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'});
+ FlightReactDOM.preinit('i before', {as: 'script'});
+ FlightReactDOM.preinitModule('m before');
+ FlightReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'});
+ await 1;
+ FlightReactDOM.prefetchDNS('d after');
+ FlightReactDOM.preconnect('c after');
+ FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
+ FlightReactDOM.preload('l after', {as: 'style'});
+ FlightReactDOM.preloadModule('lm after');
+ FlightReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'});
+ FlightReactDOM.preinit('i after', {as: 'script'});
+ FlightReactDOM.preinitModule('m after');
+ FlightReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'});
+ return ;
+ }
+
+ const {writable: flightWritable, readable: flightReadable} =
+ getTestStream();
+ const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
+
+ // In a real environment you would want to call the render during the Fizz render.
+ // The reason we cannot do this in our test is because we don't actually have two separate
+ // module graphs and we are contriving the sequencing to work in a way where
+ // the right HostDispatcher is in scope during the Flight Server Float calls and the
+ // Flight Client hint dispatches
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ );
+ pipe(flightWritable);
+
+ let response = null;
+ function getResponse() {
+ if (response === null) {
+ response =
+ ReactServerDOMClient.createFromReadableStream(flightReadable);
+ }
+ return response;
+ }
+
+ function App() {
+ return (
+
+ {getResponse()}
+
+ );
+ }
+
+ await act(async () => {
+ ReactDOMFizzServer.renderToPipeableStream( ).pipe(fizzWritable);
+ });
+
+ const decoder = new TextDecoder();
+ const reader = fizzReadable.getReader();
+ let content = '';
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done) {
+ content += decoder.decode();
+ break;
+ }
+ content += decoder.decode(value, {stream: true});
+ }
+
+ const doc = new JSDOM(content).window.document;
+ expect(getMeaningfulChildren(doc)).toEqual(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hello world
+
+ ,
+ );
+ });
+
+ it('supports Float hints from concurrent Flight -> Fizz renders', async () => {
+ function Component() {
+ return hello world
;
+ }
+
+ const ClientComponent = clientExports(Component);
+
+ async function ServerComponent1() {
+ FlightReactDOM.preload('before1', {as: 'style'});
+ await 1;
+ FlightReactDOM.preload('after1', {as: 'style'});
+ return ;
+ }
+
+ async function ServerComponent2() {
+ FlightReactDOM.preload('before2', {as: 'style'});
+ await 1;
+ FlightReactDOM.preload('after2', {as: 'style'});
+ return ;
+ }
+
+ const {writable: flightWritable1, readable: flightReadable1} =
+ getTestStream();
+ const {writable: flightWritable2, readable: flightReadable2} =
+ getTestStream();
+
+ ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ ).pipe(flightWritable1);
+
+ ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ ).pipe(flightWritable2);
+
+ const responses = new Map();
+ function getResponse(stream) {
+ let response = responses.get(stream);
+ if (!response) {
+ response = ReactServerDOMClient.createFromReadableStream(stream);
+ responses.set(stream, response);
+ }
+ return response;
+ }
+
+ function App({stream}) {
+ return (
+
+ {getResponse(stream)}
+
+ );
+ }
+
+ // pausing to let Flight runtime tick. This is a test only artifact of the fact that
+ // we aren't operating separate module graphs for flight and fiber. In a real app
+ // each would have their own dispatcher and there would be no cross dispatching.
+ await 1;
+
+ const {writable: fizzWritable1, readable: fizzReadable1} = getTestStream();
+ const {writable: fizzWritable2, readable: fizzReadable2} = getTestStream();
+ await act(async () => {
+ ReactDOMFizzServer.renderToPipeableStream(
+ ,
+ ).pipe(fizzWritable1);
+ ReactDOMFizzServer.renderToPipeableStream(
+ ,
+ ).pipe(fizzWritable2);
+ });
+
+ async function read(stream) {
+ const decoder = new TextDecoder();
+ const reader = stream.getReader();
+ let buffer = '';
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done) {
+ buffer += decoder.decode();
+ break;
+ }
+ buffer += decoder.decode(value, {stream: true});
+ }
+ return buffer;
+ }
+
+ const [content1, content2] = await Promise.all([
+ read(fizzReadable1),
+ read(fizzReadable2),
+ ]);
+
+ expect(content1).toEqual(
+ ' ' +
+ 'hello world
',
+ );
+ expect(content2).toEqual(
+ ' ' +
+ 'hello world
',
+ );
+ });
+
+ it('supports deduping hints by Float key', async () => {
+ function Component() {
+ return hello world
;
+ }
+
+ const ClientComponent = clientExports(Component);
+
+ async function ServerComponent() {
+ FlightReactDOM.prefetchDNS('dns');
+ FlightReactDOM.preconnect('preconnect');
+ FlightReactDOM.preload('load', {as: 'style'});
+ FlightReactDOM.preinit('init', {as: 'script'});
+ // again but vary preconnect to demonstrate crossOrigin participates in the key
+ FlightReactDOM.prefetchDNS('dns');
+ FlightReactDOM.preconnect('preconnect', {crossOrigin: 'anonymous'});
+ FlightReactDOM.preload('load', {as: 'style'});
+ FlightReactDOM.preinit('init', {as: 'script'});
+ await 1;
+ // after an async point
+ FlightReactDOM.prefetchDNS('dns');
+ FlightReactDOM.preconnect('preconnect', {crossOrigin: 'use-credentials'});
+ FlightReactDOM.preload('load', {as: 'style'});
+ FlightReactDOM.preinit('init', {as: 'script'});
+ return ;
+ }
+
+ const {writable, readable} = getTestStream();
+
+ ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ ).pipe(writable);
+
+ const hintRows = [];
+ async function collectHints(stream) {
+ const decoder = new TextDecoder();
+ const reader = stream.getReader();
+ let buffer = '';
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done) {
+ buffer += decoder.decode();
+ if (buffer.includes(':H')) {
+ hintRows.push(buffer);
+ }
+ break;
+ }
+ buffer += decoder.decode(value, {stream: true});
+ let line;
+ while ((line = buffer.indexOf('\n')) > -1) {
+ const row = buffer.slice(0, line);
+ buffer = buffer.slice(line + 1);
+ if (row.includes(':H')) {
+ hintRows.push(row);
+ }
+ }
+ }
+ }
+
+ await collectHints(readable);
+ expect(hintRows.length).toEqual(6);
+ });
+});
diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMBrowser-test.js
new file mode 100644
index 0000000000000..94a7236e70652
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMBrowser-test.js
@@ -0,0 +1,1228 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+// Polyfills for test environment
+global.ReadableStream =
+ require('web-streams-polyfill/ponyfill/es6').ReadableStream;
+global.TextEncoder = require('util').TextEncoder;
+global.TextDecoder = require('util').TextDecoder;
+
+let clientExports;
+let serverExports;
+let turbopackMap;
+let turbopackServerMap;
+let act;
+let React;
+let ReactDOM;
+let ReactDOMClient;
+let ReactDOMFizzServer;
+let ReactServerDOMServer;
+let ReactServerDOMClient;
+let Suspense;
+let use;
+
+describe('ReactFlightDOMBrowser', () => {
+ beforeEach(() => {
+ jest.resetModules();
+
+ // Simulate the condition resolution
+ jest.mock('react-server-dom-turbopack/server', () =>
+ require('react-server-dom-turbopack/server.browser'),
+ );
+
+ act = require('internal-test-utils').act;
+ const TurbopackMock = require('./utils/TurbopackMock');
+ clientExports = TurbopackMock.clientExports;
+ serverExports = TurbopackMock.serverExports;
+ turbopackMap = TurbopackMock.turbopackMap;
+ turbopackServerMap = TurbopackMock.turbopackServerMap;
+ React = require('react');
+ ReactDOM = require('react-dom');
+ ReactDOMClient = require('react-dom/client');
+ ReactDOMFizzServer = require('react-dom/server.browser');
+ ReactServerDOMServer = require('react-server-dom-turbopack/server.browser');
+ ReactServerDOMClient = require('react-server-dom-turbopack/client');
+ Suspense = React.Suspense;
+ use = React.use;
+ });
+
+ function makeDelayedText(Model) {
+ let error, _resolve, _reject;
+ let promise = new Promise((resolve, reject) => {
+ _resolve = () => {
+ promise = null;
+ resolve();
+ };
+ _reject = e => {
+ error = e;
+ promise = null;
+ reject(e);
+ };
+ });
+ function DelayedText({children}, data) {
+ if (promise) {
+ throw promise;
+ }
+ if (error) {
+ throw error;
+ }
+ return {children} ;
+ }
+ return [DelayedText, _resolve, _reject];
+ }
+
+ const theInfinitePromise = new Promise(() => {});
+ function InfiniteSuspend() {
+ throw theInfinitePromise;
+ }
+
+ function requireServerRef(ref) {
+ let name = '';
+ let resolvedModuleData = turbopackServerMap[ref];
+ 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 = ref.lastIndexOf('#');
+ if (idx !== -1) {
+ name = ref.slice(idx + 1);
+ resolvedModuleData = turbopackServerMap[ref.slice(0, idx)];
+ }
+ if (!resolvedModuleData) {
+ throw new Error(
+ 'Could not find the module "' +
+ ref +
+ '" in the React Client Manifest. ' +
+ 'This is probably a bug in the React Server Components bundler.',
+ );
+ }
+ }
+ const mod = __turbopack_require__(resolvedModuleData.id);
+ if (name === '*') {
+ return mod;
+ }
+ return mod[name];
+ }
+
+ async function callServer(actionId, body) {
+ const fn = requireServerRef(actionId);
+ const args = await ReactServerDOMServer.decodeReply(
+ body,
+ turbopackServerMap,
+ );
+ return fn.apply(null, args);
+ }
+
+ it('should resolve HTML using W3C streams', async () => {
+ function Text({children}) {
+ return {children} ;
+ }
+ function HTML() {
+ return (
+
+ hello
+ world
+
+ );
+ }
+
+ function App() {
+ const model = {
+ html: {getResponse()}
+
+ );
+ }
+
+ // pausing to let Flight runtime tick. This is a test only artifact of the fact that
+ // we aren't operating separate module graphs for flight and fiber. In a real app
+ // each would have their own dispatcher and there would be no cross dispatching.
+ await 1;
+
+ let fizzStream;
+ await act(async () => {
+ fizzStream = await ReactDOMFizzServer.renderToReadableStream( );
+ });
+
+ const decoder = new TextDecoder();
+ const reader = fizzStream.getReader();
+ let content = '';
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done) {
+ content += decoder.decode();
+ break;
+ }
+ content += decoder.decode(value, {stream: true});
+ }
+
+ expect(content).toEqual(
+ '' +
+ 'hello world
',
+ );
+ });
+
+ // @gate enablePostpone
+ it('supports postpone in Server Components', async () => {
+ function Server() {
+ React.unstable_postpone('testing postpone');
+ return 'Not shown';
+ }
+
+ let postponed = null;
+
+ const stream = ReactServerDOMServer.renderToReadableStream(
+
+
+ ,
+ null,
+ {
+ onPostpone(reason) {
+ postponed = reason;
+ },
+ },
+ );
+ const response = ReactServerDOMClient.createFromReadableStream(stream);
+
+ function Client() {
+ return use(response);
+ }
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+ await act(async () => {
+ root.render(
+
+ Shell:
+
,
+ );
+ });
+ // We should have reserved the shell already. Which means that the Server
+ // Component should've been a lazy component.
+ expect(container.innerHTML).toContain('Shell:');
+ expect(container.innerHTML).toContain('Loading...');
+ expect(container.innerHTML).not.toContain('Not shown');
+
+ expect(postponed).toBe('testing postpone');
+ });
+});
diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMEdge-test.js
new file mode 100644
index 0000000000000..2dfdf97841bd7
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMEdge-test.js
@@ -0,0 +1,205 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+// Polyfills for test environment
+global.ReadableStream =
+ require('web-streams-polyfill/ponyfill/es6').ReadableStream;
+global.TextEncoder = require('util').TextEncoder;
+global.TextDecoder = require('util').TextDecoder;
+
+// Don't wait before processing work on the server.
+// TODO: we can replace this with FlightServer.act().
+global.setTimeout = cb => cb();
+
+let clientExports;
+let turbopackMap;
+let turbopackModules;
+let React;
+let ReactDOMServer;
+let ReactServerDOMServer;
+let ReactServerDOMClient;
+let use;
+
+describe('ReactFlightDOMEdge', () => {
+ beforeEach(() => {
+ jest.resetModules();
+
+ // Simulate the condition resolution
+ jest.mock('react-server-dom-turbopack/server', () =>
+ require('react-server-dom-turbopack/server.edge'),
+ );
+
+ const TurbopackMock = require('./utils/TurbopackMock');
+ clientExports = TurbopackMock.clientExports;
+ turbopackMap = TurbopackMock.turbopackMap;
+ turbopackModules = TurbopackMock.turbopackModules;
+ React = require('react');
+ ReactDOMServer = require('react-dom/server.edge');
+ ReactServerDOMServer = require('react-server-dom-turbopack/server.edge');
+ ReactServerDOMClient = require('react-server-dom-turbopack/client.edge');
+ use = React.use;
+ });
+
+ function passThrough(stream) {
+ // Simulate more realistic network by splitting up and rejoining some chunks.
+ // This lets us test that we don't accidentally rely on particular bounds of the chunks.
+ return new ReadableStream({
+ async start(controller) {
+ const reader = stream.getReader();
+ let prevChunk = new Uint8Array(0);
+ function push() {
+ reader.read().then(({done, value}) => {
+ if (done) {
+ controller.enqueue(prevChunk);
+ controller.close();
+ return;
+ }
+ const chunk = new Uint8Array(prevChunk.length + value.length);
+ chunk.set(prevChunk, 0);
+ chunk.set(value, prevChunk.length);
+ if (chunk.length > 50) {
+ controller.enqueue(chunk.subarray(0, chunk.length - 50));
+ prevChunk = chunk.subarray(chunk.length - 50);
+ } else {
+ prevChunk = chunk;
+ }
+ push();
+ });
+ }
+ push();
+ },
+ });
+ }
+
+ async function readResult(stream) {
+ const reader = stream.getReader();
+ let result = '';
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done) {
+ return result;
+ }
+ result += Buffer.from(value).toString('utf8');
+ }
+ }
+
+ it('should allow an alternative module mapping to be used for SSR', async () => {
+ function ClientComponent() {
+ return Client Component ;
+ }
+ // The Client build may not have the same IDs as the Server bundles for the same
+ // component.
+ const ClientComponentOnTheClient = clientExports(ClientComponent);
+ const ClientComponentOnTheServer = clientExports(ClientComponent);
+
+ // In the SSR bundle this module won't exist. We simulate this by deleting it.
+ const clientId = turbopackMap[ClientComponentOnTheClient.$$id].id;
+ delete turbopackModules[clientId];
+
+ // Instead, we have to provide a translation from the client meta data to the SSR
+ // meta data.
+ const ssrMetadata = turbopackMap[ClientComponentOnTheServer.$$id];
+ const translationMap = {
+ [clientId]: {
+ '*': ssrMetadata,
+ },
+ };
+
+ function App() {
+ return ;
+ }
+
+ const stream = ReactServerDOMServer.renderToReadableStream(
+ ,
+ turbopackMap,
+ );
+ const response = ReactServerDOMClient.createFromReadableStream(stream, {
+ ssrManifest: {
+ moduleMap: translationMap,
+ moduleLoading: null,
+ },
+ });
+
+ function ClientRoot() {
+ return use(response);
+ }
+
+ const ssrStream = await ReactDOMServer.renderToReadableStream(
+ ,
+ );
+ const result = await readResult(ssrStream);
+ expect(result).toEqual('Client Component ');
+ });
+
+ it('should encode long string in a compact format', async () => {
+ const testString = '"\n\t'.repeat(500) + '🙃';
+ const testString2 = 'hello'.repeat(400);
+
+ const stream = ReactServerDOMServer.renderToReadableStream({
+ text: testString,
+ text2: testString2,
+ });
+ const [stream1, stream2] = passThrough(stream).tee();
+
+ const serializedContent = await readResult(stream1);
+ // The content should be compact an unescaped
+ expect(serializedContent.length).toBeLessThan(4000);
+ expect(serializedContent).not.toContain('\\n');
+ expect(serializedContent).not.toContain('\\t');
+ expect(serializedContent).not.toContain('\\"');
+ expect(serializedContent).toContain('\t');
+
+ const result = await ReactServerDOMClient.createFromReadableStream(
+ stream2,
+ {
+ ssrManifest: {
+ moduleMap: null,
+ moduleLoading: null,
+ },
+ },
+ );
+ // Should still match the result when parsed
+ expect(result.text).toBe(testString);
+ expect(result.text2).toBe(testString2);
+ });
+
+ // @gate enableBinaryFlight
+ it('should be able to serialize any kind of typed array', async () => {
+ const buffer = new Uint8Array([
+ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
+ ]).buffer;
+ const buffers = [
+ buffer,
+ new Int8Array(buffer, 1),
+ new Uint8Array(buffer, 2),
+ new Uint8ClampedArray(buffer, 2),
+ new Int16Array(buffer, 2),
+ new Uint16Array(buffer, 2),
+ new Int32Array(buffer, 4),
+ new Uint32Array(buffer, 4),
+ new Float32Array(buffer, 4),
+ new Float64Array(buffer, 0),
+ new BigInt64Array(buffer, 0),
+ new BigUint64Array(buffer, 0),
+ new DataView(buffer, 3),
+ ];
+ const stream = passThrough(
+ ReactServerDOMServer.renderToReadableStream(buffers),
+ );
+ const result = await ReactServerDOMClient.createFromReadableStream(stream, {
+ ssrManifest: {
+ moduleMap: null,
+ moduleLoading: null,
+ },
+ });
+ expect(result).toEqual(buffers);
+ });
+});
diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMForm-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMForm-test.js
new file mode 100644
index 0000000000000..4c99860e0bb2e
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMForm-test.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.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+import {insertNodesAndExecuteScripts} from 'react-dom/src/test-utils/FizzTestUtils';
+
+// Polyfills for test environment
+global.ReadableStream =
+ require('web-streams-polyfill/ponyfill/es6').ReadableStream;
+global.TextEncoder = require('util').TextEncoder;
+global.TextDecoder = require('util').TextDecoder;
+
+// Don't wait before processing work on the server.
+// TODO: we can replace this with FlightServer.act().
+global.setTimeout = cb => cb();
+
+let container;
+let serverExports;
+let turbopackServerMap;
+let React;
+let ReactDOMServer;
+let ReactServerDOMServer;
+let ReactServerDOMClient;
+
+describe('ReactFlightDOMForm', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ // Simulate the condition resolution
+ jest.mock('react-server-dom-turbopack/server', () =>
+ require('react-server-dom-turbopack/server.edge'),
+ );
+ const TurbopackMock = require('./utils/TurbopackMock');
+ serverExports = TurbopackMock.serverExports;
+ turbopackServerMap = TurbopackMock.turbopackServerMap;
+ React = require('react');
+ ReactServerDOMServer = require('react-server-dom-turbopack/server.edge');
+ ReactServerDOMClient = require('react-server-dom-turbopack/client.edge');
+ ReactDOMServer = require('react-dom/server.edge');
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ });
+
+ async function POST(formData) {
+ const boundAction = await ReactServerDOMServer.decodeAction(
+ formData,
+ turbopackServerMap,
+ );
+ return boundAction();
+ }
+
+ function submit(submitter) {
+ const form = submitter.form || submitter;
+ if (!submitter.form) {
+ submitter = undefined;
+ }
+ const submitEvent = new Event('submit', {bubbles: true, cancelable: true});
+ submitEvent.submitter = submitter;
+ const returnValue = form.dispatchEvent(submitEvent);
+ if (!returnValue) {
+ return;
+ }
+ const action =
+ (submitter && submitter.getAttribute('formaction')) || form.action;
+ if (!/\s*javascript:/i.test(action)) {
+ const method = (submitter && submitter.formMethod) || form.method;
+ const encType = (submitter && submitter.formEnctype) || form.enctype;
+ if (method === 'post' && encType === 'multipart/form-data') {
+ let formData;
+ if (submitter) {
+ const temp = document.createElement('input');
+ temp.name = submitter.name;
+ temp.value = submitter.value;
+ submitter.parentNode.insertBefore(temp, submitter);
+ formData = new FormData(form);
+ temp.parentNode.removeChild(temp);
+ } else {
+ formData = new FormData(form);
+ }
+ return POST(formData);
+ }
+ throw new Error('Navigate to: ' + action);
+ }
+ }
+
+ async function readIntoContainer(stream) {
+ const reader = stream.getReader();
+ let result = '';
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done) {
+ break;
+ }
+ result += Buffer.from(value).toString('utf8');
+ }
+ const temp = document.createElement('div');
+ temp.innerHTML = result;
+ insertNodesAndExecuteScripts(temp, container, null);
+ }
+
+ // @gate enableFormActions
+ it('can submit a passed server action without hydrating it', async () => {
+ let foo = null;
+
+ const serverAction = serverExports(function action(formData) {
+ foo = formData.get('foo');
+ return 'hello';
+ });
+ function App() {
+ return (
+
+ );
+ }
+ const rscStream = ReactServerDOMServer.renderToReadableStream( );
+ const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
+ ssrManifest: {
+ moduleMap: null,
+ moduleLoading: null,
+ },
+ });
+ const ssrStream = await ReactDOMServer.renderToReadableStream(response);
+ await readIntoContainer(ssrStream);
+
+ const form = container.firstChild;
+
+ expect(foo).toBe(null);
+
+ const result = await submit(form);
+
+ expect(result).toBe('hello');
+ expect(foo).toBe('bar');
+ });
+
+ // @gate enableFormActions
+ it('can submit an imported server action without hydrating it', async () => {
+ let foo = null;
+
+ const ServerModule = serverExports(function action(formData) {
+ foo = formData.get('foo');
+ return 'hi';
+ });
+ const serverAction = ReactServerDOMClient.createServerReference(
+ ServerModule.$$id,
+ );
+ function App() {
+ return (
+
+ );
+ }
+
+ const ssrStream = await ReactDOMServer.renderToReadableStream( );
+ await readIntoContainer(ssrStream);
+
+ const form = container.firstChild;
+
+ expect(foo).toBe(null);
+
+ const result = await submit(form);
+
+ expect(result).toBe('hi');
+
+ expect(foo).toBe('bar');
+ });
+
+ // @gate enableFormActions
+ it('can submit a complex closure server action without hydrating it', async () => {
+ let foo = null;
+
+ const serverAction = serverExports(function action(bound, formData) {
+ foo = formData.get('foo') + bound.complex;
+ return 'hello';
+ });
+ function App() {
+ return (
+
+ );
+ }
+ const rscStream = ReactServerDOMServer.renderToReadableStream( );
+ const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
+ ssrManifest: {
+ moduleMap: null,
+ moduleLoading: null,
+ },
+ });
+ const ssrStream = await ReactDOMServer.renderToReadableStream(response);
+ await readIntoContainer(ssrStream);
+
+ const form = container.firstChild;
+
+ expect(foo).toBe(null);
+
+ const result = await submit(form);
+
+ expect(result).toBe('hello');
+ expect(foo).toBe('barobject');
+ });
+
+ // @gate enableFormActions
+ it('can submit a multiple complex closure server action without hydrating it', async () => {
+ let foo = null;
+
+ const serverAction = serverExports(function action(bound, formData) {
+ foo = formData.get('foo') + bound.complex;
+ return 'hello' + bound.complex;
+ });
+ function App() {
+ return (
+
+ );
+ }
+ const rscStream = ReactServerDOMServer.renderToReadableStream( );
+ const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
+ ssrManifest: {
+ moduleMap: null,
+ moduleLoading: null,
+ },
+ });
+ const ssrStream = await ReactDOMServer.renderToReadableStream(response);
+ await readIntoContainer(ssrStream);
+
+ const form = container.firstChild;
+
+ expect(foo).toBe(null);
+
+ const result = await submit(form.getElementsByTagName('button')[1]);
+
+ expect(result).toBe('helloc');
+ expect(foo).toBe('barc');
+ });
+});
diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMNode-test.js
new file mode 100644
index 0000000000000..5a7b14b3bbd4c
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMNode-test.js
@@ -0,0 +1,256 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+// Don't wait before processing work on the server.
+// TODO: we can replace this with FlightServer.act().
+global.setImmediate = cb => cb();
+
+let clientExports;
+let turbopackMap;
+let turbopackModules;
+let turbopackModuleLoading;
+let React;
+let ReactDOMServer;
+let ReactServerDOMServer;
+let ReactServerDOMClient;
+let Stream;
+let use;
+
+describe('ReactFlightDOMNode', () => {
+ beforeEach(() => {
+ jest.resetModules();
+
+ // Simulate the condition resolution
+ jest.mock('react', () => require('react/react.shared-subset'));
+ jest.mock('react-server-dom-turbopack/server', () =>
+ require('react-server-dom-turbopack/server.node'),
+ );
+ ReactServerDOMServer = require('react-server-dom-turbopack/server');
+
+ const TurbopackMock = require('./utils/TurbopackMock');
+ clientExports = TurbopackMock.clientExports;
+ turbopackMap = TurbopackMock.turbopackMap;
+ turbopackModules = TurbopackMock.turbopackModules;
+ turbopackModuleLoading = TurbopackMock.moduleLoading;
+
+ jest.resetModules();
+ jest.unmock('react');
+ jest.unmock('react-server-dom-turbopack/server');
+ jest.mock('react-server-dom-turbopack/client', () =>
+ require('react-server-dom-turbopack/client.node'),
+ );
+
+ React = require('react');
+ ReactDOMServer = require('react-dom/server.node');
+ ReactServerDOMClient = require('react-server-dom-turbopack/client');
+ Stream = require('stream');
+ use = React.use;
+ });
+
+ function readResult(stream) {
+ return new Promise((resolve, reject) => {
+ let buffer = '';
+ const writable = new Stream.PassThrough();
+ writable.setEncoding('utf8');
+ writable.on('data', chunk => {
+ buffer += chunk;
+ });
+ writable.on('error', error => {
+ reject(error);
+ });
+ writable.on('end', () => {
+ resolve(buffer);
+ });
+ stream.pipe(writable);
+ });
+ }
+
+ it('should allow an alternative module mapping to be used for SSR', async () => {
+ function ClientComponent() {
+ return Client Component ;
+ }
+ // The Client build may not have the same IDs as the Server bundles for the same
+ // component.
+ const ClientComponentOnTheClient = clientExports(
+ ClientComponent,
+ 'path/to/chunk.js',
+ );
+ const ClientComponentOnTheServer = clientExports(ClientComponent);
+
+ // In the SSR bundle this module won't exist. We simulate this by deleting it.
+ const clientId = turbopackMap[ClientComponentOnTheClient.$$id].id;
+ delete turbopackModules[clientId];
+
+ // Instead, we have to provide a translation from the client meta data to the SSR
+ // meta data.
+ const ssrMetadata = turbopackMap[ClientComponentOnTheServer.$$id];
+ const translationMap = {
+ [clientId]: {
+ '*': ssrMetadata,
+ },
+ };
+
+ function App() {
+ return ;
+ }
+
+ const stream = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ );
+ const readable = new Stream.PassThrough();
+
+ stream.pipe(readable);
+
+ let response;
+ function ClientRoot() {
+ if (!response) {
+ response = ReactServerDOMClient.createFromNodeStream(readable, {
+ moduleMap: translationMap,
+ moduleLoading: turbopackModuleLoading,
+ });
+ }
+ return use(response);
+ }
+
+ const ssrStream = await ReactDOMServer.renderToPipeableStream(
+ ,
+ );
+ const result = await readResult(ssrStream);
+ expect(result).toEqual(
+ 'Client Component ',
+ );
+ });
+
+ it('should encode long string in a compact format', async () => {
+ const testString = '"\n\t'.repeat(500) + '🙃';
+
+ const stream = ReactServerDOMServer.renderToPipeableStream({
+ text: testString,
+ });
+
+ const readable = new Stream.PassThrough();
+
+ const stringResult = readResult(readable);
+ const parsedResult = ReactServerDOMClient.createFromNodeStream(readable, {
+ moduleMap: turbopackMap,
+ moduleLoading: turbopackModuleLoading,
+ });
+
+ stream.pipe(readable);
+
+ const serializedContent = await stringResult;
+ // The content should be compact an unescaped
+ expect(serializedContent.length).toBeLessThan(2000);
+ expect(serializedContent).not.toContain('\\n');
+ expect(serializedContent).not.toContain('\\t');
+ expect(serializedContent).not.toContain('\\"');
+ expect(serializedContent).toContain('\t');
+
+ const result = await parsedResult;
+ // Should still match the result when parsed
+ expect(result.text).toBe(testString);
+ });
+
+ // @gate enableBinaryFlight
+ it('should be able to serialize any kind of typed array', async () => {
+ const buffer = new Uint8Array([
+ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
+ ]).buffer;
+ const buffers = [
+ buffer,
+ new Int8Array(buffer, 1),
+ new Uint8Array(buffer, 2),
+ new Uint8ClampedArray(buffer, 2),
+ new Int16Array(buffer, 2),
+ new Uint16Array(buffer, 2),
+ new Int32Array(buffer, 4),
+ new Uint32Array(buffer, 4),
+ new Float32Array(buffer, 4),
+ new Float64Array(buffer, 0),
+ new BigInt64Array(buffer, 0),
+ new BigUint64Array(buffer, 0),
+ new DataView(buffer, 3),
+ ];
+ const stream = ReactServerDOMServer.renderToPipeableStream(buffers);
+ const readable = new Stream.PassThrough();
+ const promise = ReactServerDOMClient.createFromNodeStream(readable, {
+ moduleMap: turbopackMap,
+ moduleLoading: turbopackModuleLoading,
+ });
+ stream.pipe(readable);
+ const result = await promise;
+ expect(result).toEqual(buffers);
+ });
+
+ it('should allow accept a nonce option for Flight preinitialized scripts', async () => {
+ function ClientComponent() {
+ return Client Component ;
+ }
+ // The Client build may not have the same IDs as the Server bundles for the same
+ // component.
+ const ClientComponentOnTheClient = clientExports(
+ ClientComponent,
+ 'path/to/chunk.js',
+ );
+ const ClientComponentOnTheServer = clientExports(ClientComponent);
+
+ // In the SSR bundle this module won't exist. We simulate this by deleting it.
+ const clientId = turbopackMap[ClientComponentOnTheClient.$$id].id;
+ delete turbopackModules[clientId];
+
+ // Instead, we have to provide a translation from the client meta data to the SSR
+ // meta data.
+ const ssrMetadata = turbopackMap[ClientComponentOnTheServer.$$id];
+ const translationMap = {
+ [clientId]: {
+ '*': ssrMetadata,
+ },
+ };
+ const ssrManifest = {
+ moduleMap: translationMap,
+ moduleLoading: turbopackModuleLoading,
+ };
+
+ function App() {
+ return ;
+ }
+
+ const stream = ReactServerDOMServer.renderToPipeableStream(
+ ,
+ turbopackMap,
+ );
+ const readable = new Stream.PassThrough();
+ let response;
+
+ stream.pipe(readable);
+
+ function ClientRoot() {
+ if (response) return use(response);
+ response = ReactServerDOMClient.createFromNodeStream(
+ readable,
+ ssrManifest,
+ {
+ nonce: 'r4nd0m',
+ },
+ );
+ return use(response);
+ }
+
+ const ssrStream = await ReactDOMServer.renderToPipeableStream(
+ ,
+ );
+ const result = await readResult(ssrStream);
+ expect(result).toEqual(
+ 'Client Component ',
+ );
+ });
+});
diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMReply-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMReply-test.js
new file mode 100644
index 0000000000000..d6c4c318d7b38
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMReply-test.js
@@ -0,0 +1,232 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+// Polyfills for test environment
+global.ReadableStream =
+ require('web-streams-polyfill/ponyfill/es6').ReadableStream;
+global.TextEncoder = require('util').TextEncoder;
+global.TextDecoder = require('util').TextDecoder;
+
+// let serverExports;
+let turbopackServerMap;
+let ReactServerDOMServer;
+let ReactServerDOMClient;
+
+describe('ReactFlightDOMReply', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ // Simulate the condition resolution
+ jest.mock('react-server-dom-turbopack/server', () =>
+ require('react-server-dom-turbopack/server.browser'),
+ );
+ const TurbopackMock = require('./utils/TurbopackMock');
+ // serverExports = TurbopackMock.serverExports;
+ turbopackServerMap = TurbopackMock.turbopackServerMap;
+ ReactServerDOMServer = require('react-server-dom-turbopack/server.browser');
+ ReactServerDOMClient = require('react-server-dom-turbopack/client');
+ });
+
+ // This method should exist on File but is not implemented in JSDOM
+ async function arrayBuffer(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = function () {
+ return resolve(reader.result);
+ };
+ reader.onerror = function () {
+ return reject(reader.error);
+ };
+ reader.readAsArrayBuffer(file);
+ });
+ }
+
+ it('can pass undefined as a reply', async () => {
+ const body = await ReactServerDOMClient.encodeReply(undefined);
+ const missing = await ReactServerDOMServer.decodeReply(
+ body,
+ turbopackServerMap,
+ );
+ expect(missing).toBe(undefined);
+
+ const body2 = await ReactServerDOMClient.encodeReply({
+ array: [undefined, null, undefined],
+ prop: undefined,
+ });
+ const object = await ReactServerDOMServer.decodeReply(
+ body2,
+ turbopackServerMap,
+ );
+ expect(object.array.length).toBe(3);
+ expect(object.array[0]).toBe(undefined);
+ expect(object.array[1]).toBe(null);
+ expect(object.array[3]).toBe(undefined);
+ expect(object.prop).toBe(undefined);
+ // These should really be true but our deserialization doesn't currently deal with it.
+ expect('3' in object.array).toBe(false);
+ expect('prop' in object).toBe(false);
+ });
+
+ it('can pass an iterable as a reply', async () => {
+ const body = await ReactServerDOMClient.encodeReply({
+ [Symbol.iterator]: function* () {
+ yield 'A';
+ yield 'B';
+ yield 'C';
+ },
+ });
+ const iterable = await ReactServerDOMServer.decodeReply(
+ body,
+ turbopackServerMap,
+ );
+ const items = [];
+ // eslint-disable-next-line no-for-of-loops/no-for-of-loops
+ for (const item of iterable) {
+ items.push(item);
+ }
+ expect(items).toEqual(['A', 'B', 'C']);
+ });
+
+ it('can pass weird numbers as a reply', async () => {
+ const nums = [0, -0, Infinity, -Infinity, NaN];
+ const body = await ReactServerDOMClient.encodeReply(nums);
+ const nums2 = await ReactServerDOMServer.decodeReply(
+ body,
+ turbopackServerMap,
+ );
+
+ expect(nums).toEqual(nums2);
+ expect(nums.every((n, i) => Object.is(n, nums2[i]))).toBe(true);
+ });
+
+ it('can pass a BigInt as a reply', async () => {
+ const body = await ReactServerDOMClient.encodeReply(90071992547409910000n);
+ const n = await ReactServerDOMServer.decodeReply(body, turbopackServerMap);
+
+ expect(n).toEqual(90071992547409910000n);
+ });
+
+ it('can pass FormData as a reply', async () => {
+ const formData = new FormData();
+ formData.set('hello', 'world');
+ formData.append('list', '1');
+ formData.append('list', '2');
+ formData.append('list', '3');
+ const typedArray = new Uint8Array([0, 1, 2, 3]);
+ const blob = new Blob([typedArray]);
+ formData.append('blob', blob, 'filename.blob');
+
+ const body = await ReactServerDOMClient.encodeReply(formData);
+ const formData2 = await ReactServerDOMServer.decodeReply(
+ body,
+ turbopackServerMap,
+ );
+
+ expect(formData2).not.toBe(formData);
+ expect(Array.from(formData2).length).toBe(5);
+ expect(formData2.get('hello')).toBe('world');
+ expect(formData2.getAll('list')).toEqual(['1', '2', '3']);
+ const blob2 = formData.get('blob');
+ expect(blob2.size).toBe(4);
+ expect(blob2.name).toBe('filename.blob');
+ expect(blob2.type).toBe('');
+ const typedArray2 = new Uint8Array(await arrayBuffer(blob2));
+ expect(typedArray2).toEqual(typedArray);
+ });
+
+ it('can pass multiple Files in FormData', async () => {
+ const typedArrayA = new Uint8Array([0, 1, 2, 3]);
+ const typedArrayB = new Uint8Array([4, 5]);
+ const blobA = new Blob([typedArrayA]);
+ const blobB = new Blob([typedArrayB]);
+ const formData = new FormData();
+ formData.append('filelist', 'string');
+ formData.append('filelist', blobA);
+ formData.append('filelist', blobB);
+
+ const body = await ReactServerDOMClient.encodeReply(formData);
+ const formData2 = await ReactServerDOMServer.decodeReply(
+ body,
+ turbopackServerMap,
+ );
+
+ const filelist2 = formData2.getAll('filelist');
+ expect(filelist2.length).toBe(3);
+ expect(filelist2[0]).toBe('string');
+ const blobA2 = filelist2[1];
+ expect(blobA2.size).toBe(4);
+ expect(blobA2.name).toBe('blob');
+ expect(blobA2.type).toBe('');
+ const typedArrayA2 = new Uint8Array(await arrayBuffer(blobA2));
+ expect(typedArrayA2).toEqual(typedArrayA);
+ const blobB2 = filelist2[2];
+ expect(blobB2.size).toBe(2);
+ expect(blobB2.name).toBe('blob');
+ expect(blobB2.type).toBe('');
+ const typedArrayB2 = new Uint8Array(await arrayBuffer(blobB2));
+ expect(typedArrayB2).toEqual(typedArrayB);
+ });
+
+ it('can pass two independent FormData with same keys', async () => {
+ const formDataA = new FormData();
+ formDataA.set('greeting', 'hello');
+ const formDataB = new FormData();
+ formDataB.set('greeting', 'hi');
+
+ const body = await ReactServerDOMClient.encodeReply({
+ a: formDataA,
+ b: formDataB,
+ });
+ const {a: formDataA2, b: formDataB2} =
+ await ReactServerDOMServer.decodeReply(body, turbopackServerMap);
+
+ expect(Array.from(formDataA2).length).toBe(1);
+ expect(Array.from(formDataB2).length).toBe(1);
+ expect(formDataA2.get('greeting')).toBe('hello');
+ expect(formDataB2.get('greeting')).toBe('hi');
+ });
+
+ it('can pass a Date as a reply', async () => {
+ const d = new Date(1234567890123);
+ const body = await ReactServerDOMClient.encodeReply(d);
+ const d2 = await ReactServerDOMServer.decodeReply(body, turbopackServerMap);
+
+ expect(d).toEqual(d2);
+ expect(d % 1000).toEqual(123); // double-check the milliseconds made it through
+ });
+
+ it('can pass a Map as a reply', async () => {
+ const objKey = {obj: 'key'};
+ const m = new Map([
+ ['hi', {greet: 'world'}],
+ [objKey, 123],
+ ]);
+ const body = await ReactServerDOMClient.encodeReply(m);
+ const m2 = await ReactServerDOMServer.decodeReply(body, turbopackServerMap);
+
+ expect(m2 instanceof Map).toBe(true);
+ expect(m2.size).toBe(2);
+ expect(m2.get('hi').greet).toBe('world');
+ expect(m2).toEqual(m);
+ });
+
+ it('can pass a Set as a reply', async () => {
+ const objKey = {obj: 'key'};
+ const s = new Set(['hi', objKey]);
+
+ const body = await ReactServerDOMClient.encodeReply(s);
+ const s2 = await ReactServerDOMServer.decodeReply(body, turbopackServerMap);
+
+ expect(s2 instanceof Set).toBe(true);
+ expect(s2.size).toBe(2);
+ expect(s2.has('hi')).toBe(true);
+ expect(s2).toEqual(s);
+ });
+});
diff --git a/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js b/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js
new file mode 100644
index 0000000000000..7b10593220b60
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js
@@ -0,0 +1,148 @@
+/**
+ * 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.
+ */
+
+'use strict';
+
+const url = require('url');
+const Module = require('module');
+
+let turbopackModuleIdx = 0;
+const turbopackServerModules = {};
+const turbopackClientModules = {};
+const turbopackErroredModules = {};
+const turbopackServerMap = {};
+const turbopackClientMap = {};
+global.__turbopack_require__ = function (id) {
+ if (turbopackErroredModules[id]) {
+ throw turbopackErroredModules[id];
+ }
+ return turbopackClientModules[id] || turbopackServerModules[id];
+};
+
+const previousCompile = Module.prototype._compile;
+
+const register = require('react-server-dom-turbopack/node-register');
+// Register node compile
+register();
+
+const nodeCompile = Module.prototype._compile;
+
+if (previousCompile === nodeCompile) {
+ throw new Error(
+ 'Expected the Node loader to register the _compile extension',
+ );
+}
+
+Module.prototype._compile = previousCompile;
+
+exports.turbopackMap = turbopackClientMap;
+exports.turbopackModules = turbopackClientModules;
+exports.turbopackServerMap = turbopackServerMap;
+exports.moduleLoading = {
+ prefix: '/prefix/',
+};
+
+exports.clientModuleError = function clientModuleError(moduleError) {
+ const idx = '' + turbopackModuleIdx++;
+ turbopackErroredModules[idx] = moduleError;
+ const path = url.pathToFileURL(idx).href;
+ turbopackClientMap[path] = {
+ id: idx,
+ chunks: [],
+ name: '*',
+ };
+ const mod = {exports: {}};
+ nodeCompile.call(mod, '"use client"', idx);
+ return mod.exports;
+};
+
+exports.clientExports = function clientExports(moduleExports, chunkUrl) {
+ const chunks = [];
+ if (chunkUrl !== undefined) {
+ chunks.push(chunkUrl);
+ }
+ const idx = '' + turbopackModuleIdx++;
+ turbopackClientModules[idx] = moduleExports;
+ const path = url.pathToFileURL(idx).href;
+ turbopackClientMap[path] = {
+ id: idx,
+ chunks,
+ name: '*',
+ };
+ // We only add this if this test is testing ESM compat.
+ if ('__esModule' in moduleExports) {
+ turbopackClientMap[path + '#'] = {
+ id: idx,
+ chunks,
+ name: '',
+ };
+ }
+ if (typeof moduleExports.then === 'function') {
+ moduleExports.then(
+ asyncModuleExports => {
+ for (const name in asyncModuleExports) {
+ turbopackClientMap[path + '#' + name] = {
+ id: idx,
+ chunks,
+ name: name,
+ };
+ }
+ },
+ () => {},
+ );
+ }
+ if ('split' in moduleExports) {
+ // If we're testing module splitting, we encode this name in a separate module id.
+ const splitIdx = '' + turbopackModuleIdx++;
+ turbopackClientModules[splitIdx] = {
+ s: moduleExports.split,
+ };
+ turbopackClientMap[path + '#split'] = {
+ id: splitIdx,
+ chunks,
+ name: 's',
+ };
+ }
+ const mod = {exports: {}};
+ nodeCompile.call(mod, '"use client"', idx);
+ return mod.exports;
+};
+
+// This tests server to server references. There's another case of client to server references.
+exports.serverExports = function serverExports(moduleExports) {
+ const idx = '' + turbopackModuleIdx++;
+ turbopackServerModules[idx] = moduleExports;
+ const path = url.pathToFileURL(idx).href;
+ turbopackServerMap[path] = {
+ id: idx,
+ chunks: [],
+ name: '*',
+ };
+ // We only add this if this test is testing ESM compat.
+ if ('__esModule' in moduleExports) {
+ turbopackServerMap[path + '#'] = {
+ id: idx,
+ chunks: [],
+ name: '',
+ };
+ }
+ if ('split' in moduleExports) {
+ // If we're testing module splitting, we encode this name in a separate module id.
+ const splitIdx = '' + turbopackModuleIdx++;
+ turbopackServerModules[splitIdx] = {
+ s: moduleExports.split,
+ };
+ turbopackServerMap[path + '#split'] = {
+ id: splitIdx,
+ chunks: [],
+ name: 's',
+ };
+ }
+ const mod = {exports: moduleExports};
+ nodeCompile.call(mod, '"use server"', idx);
+ return mod.exports;
+};
diff --git a/packages/react-server-dom-turbopack/src/shared/ReactFlightImportMetadata.js b/packages/react-server-dom-turbopack/src/shared/ReactFlightImportMetadata.js
new file mode 100644
index 0000000000000..60460d9c1d6d9
--- /dev/null
+++ b/packages/react-server-dom-turbopack/src/shared/ReactFlightImportMetadata.js
@@ -0,0 +1,39 @@
+/**
+ * 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 an array of filenames
+ chunks: Array,
+ name: string,
+};
+
+// This is the parsed shape of the wire format which is why it is
+// condensed to only the essentialy information
+export type ImportMetadata =
+ | [
+ /* id */ string,
+ /* chunk filenames */ Array,
+ /* name */ string,
+ /* async */ 1,
+ ]
+ | [/* id */ string, /* chunk filenames */ 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/src/forks/ReactFlightServerConfig.dom-browser-turbopack.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-turbopack.js
new file mode 100644
index 0000000000000..3fdeac94f5775
--- /dev/null
+++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-turbopack.js
@@ -0,0 +1,16 @@
+/**
+ * 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';
+
+export * from 'react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler';
+export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
+
+export const supportsRequestStorage = false;
+export const requestStorage: AsyncLocalStorage = (null: any);
diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-turbopack.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-turbopack.js
new file mode 100644
index 0000000000000..2beb3986d0726
--- /dev/null
+++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-turbopack.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
+ */
+import type {Request} from 'react-server/src/ReactFlightServer';
+
+export * from 'react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler';
+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);
diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-turbopack.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-turbopack.js
new file mode 100644
index 0000000000000..f85af8f3c296c
--- /dev/null
+++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-turbopack.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
+ */
+
+import {AsyncLocalStorage} from 'async_hooks';
+
+import type {Request} from 'react-server/src/ReactFlightServer';
+
+export * from 'react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler';
+export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
+
+export const supportsRequestStorage = true;
+export const requestStorage: AsyncLocalStorage =
+ new AsyncLocalStorage();
diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js
index 97db3a53a4318..a175f4f5910cf 100644
--- a/scripts/flow/environment.js
+++ b/scripts/flow/environment.js
@@ -78,6 +78,11 @@ declare var __webpack_require__: ((id: string) => any) & {
u: string => string,
};
+declare function __turbopack_load__(id: string): Promise;
+declare var __turbopack_require__: ((id: string) => any) & {
+ u: string => string,
+};
+
declare module 'fs/promises' {
declare var access: (path: string, mode?: number) => Promise;
declare var lstat: (
diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js
index cd4187a34887a..31d15ecc7b875 100644
--- a/scripts/rollup/bundles.js
+++ b/scripts/rollup/bundles.js
@@ -432,6 +432,109 @@ const bundles = [
externals: ['url', 'module', 'react-server-dom-webpack/server'],
},
+ /******* React Server DOM Turbopack Server *******/
+ {
+ bundleTypes: [NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD],
+ moduleType: RENDERER,
+ entry: 'react-server-dom-turbopack/server.browser',
+ global: 'ReactServerDOMServer',
+ minifyWithProdErrorCodes: false,
+ wrapWithModuleBoundaries: false,
+ externals: ['react', 'react-dom'],
+ },
+ {
+ bundleTypes: [NODE_DEV, NODE_PROD],
+ moduleType: RENDERER,
+ entry: 'react-server-dom-turbopack/server.node',
+ global: 'ReactServerDOMServer',
+ minifyWithProdErrorCodes: false,
+ wrapWithModuleBoundaries: false,
+ externals: ['react', 'util', 'async_hooks', 'react-dom'],
+ },
+ {
+ bundleTypes: [NODE_DEV, NODE_PROD],
+ moduleType: RENDERER,
+ entry: 'react-server-dom-turbopack/server.node.unbundled',
+ global: 'ReactServerDOMServer',
+ minifyWithProdErrorCodes: false,
+ wrapWithModuleBoundaries: false,
+ externals: ['react', 'util', 'async_hooks', 'react-dom'],
+ },
+ {
+ bundleTypes: [NODE_DEV, NODE_PROD],
+ moduleType: RENDERER,
+ entry: 'react-server-dom-turbopack/server.edge',
+ global: 'ReactServerDOMServer',
+ minifyWithProdErrorCodes: false,
+ wrapWithModuleBoundaries: false,
+ externals: ['react', 'util', 'async_hooks', 'react-dom'],
+ },
+
+ /******* React Server DOM Turbopack Client *******/
+ {
+ bundleTypes: [NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD],
+ moduleType: RENDERER,
+ entry: 'react-server-dom-turbopack/client.browser',
+ global: 'ReactServerDOMClient',
+ minifyWithProdErrorCodes: false,
+ wrapWithModuleBoundaries: false,
+ externals: ['react', 'react-dom'],
+ },
+ {
+ bundleTypes: [NODE_DEV, NODE_PROD],
+ moduleType: RENDERER,
+ entry: 'react-server-dom-turbopack/client.node',
+ global: 'ReactServerDOMClient',
+ minifyWithProdErrorCodes: false,
+ wrapWithModuleBoundaries: false,
+ externals: ['react', 'react-dom', 'util'],
+ },
+ {
+ bundleTypes: [NODE_DEV, NODE_PROD],
+ moduleType: RENDERER,
+ entry: 'react-server-dom-turbopack/client.node.unbundled',
+ global: 'ReactServerDOMClient',
+ minifyWithProdErrorCodes: false,
+ wrapWithModuleBoundaries: false,
+ externals: ['react', 'react-dom', 'util'],
+ },
+ {
+ bundleTypes: [NODE_DEV, NODE_PROD],
+ moduleType: RENDERER,
+ entry: 'react-server-dom-turbopack/client.edge',
+ global: 'ReactServerDOMClient',
+ minifyWithProdErrorCodes: false,
+ wrapWithModuleBoundaries: false,
+ externals: ['react', 'react-dom'],
+ },
+
+ /******* React Server DOM Turbopack Plugin *******/
+ // There is no plugin the moment because Turbopack
+ // does not expose a plugin interface yet.
+
+ /******* React Server DOM Turbopack Node.js Loader *******/
+ {
+ bundleTypes: [ESM_PROD],
+ moduleType: RENDERER_UTILS,
+ entry: 'react-server-dom-turbopack/node-loader',
+ global: 'ReactServerTurbopackNodeLoader',
+ minifyWithProdErrorCodes: false,
+ wrapWithModuleBoundaries: false,
+ externals: ['acorn'],
+ },
+
+ /******* React Server DOM Turbopack Node.js CommonJS Loader *******/
+ {
+ bundleTypes: [NODE_ES2015],
+ moduleType: RENDERER_UTILS,
+ entry: 'react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister',
+ name: 'react-server-dom-turbopack-node-register',
+ global: 'ReactFlightWebpackNodeRegister',
+ minifyWithProdErrorCodes: false,
+ wrapWithModuleBoundaries: false,
+ externals: ['url', 'module', 'react-server-dom-turbopack/server'],
+ },
+
/******* React Server DOM ESM Server *******/
{
bundleTypes: [NODE_DEV, NODE_PROD],
diff --git a/scripts/rollup/validate/eslintrc.cjs.js b/scripts/rollup/validate/eslintrc.cjs.js
index b3ea021a619f4..63406a6a6b245 100644
--- a/scripts/rollup/validate/eslintrc.cjs.js
+++ b/scripts/rollup/validate/eslintrc.cjs.js
@@ -56,6 +56,10 @@ module.exports = {
__webpack_chunk_load__: 'readonly',
__webpack_require__: 'readonly',
+ // Flight Turbopack
+ __turbopack_load__: 'readonly',
+ __turbopack_require__: 'readonly',
+
// jest
expect: 'readonly',
jest: 'readonly',
diff --git a/scripts/rollup/validate/eslintrc.cjs2015.js b/scripts/rollup/validate/eslintrc.cjs2015.js
index 435d5e5c820b6..3c8ade7946c71 100644
--- a/scripts/rollup/validate/eslintrc.cjs2015.js
+++ b/scripts/rollup/validate/eslintrc.cjs2015.js
@@ -56,6 +56,10 @@ module.exports = {
__webpack_chunk_load__: 'readonly',
__webpack_require__: 'readonly',
+ // Flight Turbopack
+ __turbopack_load__: 'readonly',
+ __turbopack_require__: 'readonly',
+
// jest
expect: 'readonly',
jest: 'readonly',
diff --git a/scripts/rollup/validate/eslintrc.esm.js b/scripts/rollup/validate/eslintrc.esm.js
index 11c72a68f9362..a46004a25bed1 100644
--- a/scripts/rollup/validate/eslintrc.esm.js
+++ b/scripts/rollup/validate/eslintrc.esm.js
@@ -56,6 +56,10 @@ module.exports = {
__webpack_chunk_load__: 'readonly',
__webpack_require__: 'readonly',
+ // Flight Turbopack
+ __turbopack_load__: 'readonly',
+ __turbopack_require__: 'readonly',
+
// jest
expect: 'readonly',
jest: 'readonly',
diff --git a/scripts/rollup/validate/eslintrc.umd.js b/scripts/rollup/validate/eslintrc.umd.js
index 020bfe0bb6b0c..3d35c688bdbf0 100644
--- a/scripts/rollup/validate/eslintrc.umd.js
+++ b/scripts/rollup/validate/eslintrc.umd.js
@@ -61,6 +61,10 @@ module.exports = {
__webpack_chunk_load__: 'readonly',
__webpack_require__: 'readonly',
+ // Flight Turbopack
+ __turbopack_load__: 'readonly',
+ __turbopack_require__: 'readonly',
+
// jest
jest: 'readonly',
diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js
index c958a4a4ccfae..6f2ba535a27d1 100644
--- a/scripts/shared/inlinedHostConfigs.js
+++ b/scripts/shared/inlinedHostConfigs.js
@@ -49,6 +49,115 @@ module.exports = [
isFlowTyped: true,
isServerSupported: true,
},
+ {
+ shortName: 'dom-node-webpack',
+ entryPoints: [
+ 'react-server-dom-webpack/server.node',
+ 'react-server-dom-webpack/client.node',
+ ],
+ paths: [
+ 'react-dom',
+ 'react-dom-bindings',
+ 'react-dom/client',
+ '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-server-dom-webpack',
+ 'react-server-dom-webpack/client.node',
+ 'react-server-dom-webpack/server',
+ 'react-server-dom-webpack/server.node',
+ 'react-server-dom-webpack/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node
+ 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js',
+ 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer.js',
+ 'react-server-dom-webpack/node-register',
+ 'react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js',
+ 'react-devtools',
+ 'react-devtools-core',
+ 'react-devtools-shell',
+ 'react-devtools-shared',
+ 'react-interactions',
+ 'shared/ReactDOMSharedInternals',
+ ],
+ isFlowTyped: true,
+ isServerSupported: true,
+ },
+ {
+ shortName: 'dom-node-turbopack',
+ entryPoints: [
+ 'react-server-dom-turbopack/server.node.unbundled',
+ 'react-server-dom-turbopack/client.node.unbundled',
+ ],
+ paths: [
+ 'react-dom',
+ 'react-dom-bindings',
+ 'react-dom/client',
+ '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-server-dom-turbopack',
+ 'react-server-dom-turbopack/client.node.unbundled',
+ 'react-server-dom-turbopack/server',
+ 'react-server-dom-turbopack/server.node.unbundled',
+ 'react-server-dom-turbopack/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node.unbundled
+ 'react-server-dom-turbopack/src/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node.unbundled
+ 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js',
+ 'react-server-dom-turbopack/node-register',
+ 'react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js',
+ 'react-devtools',
+ 'react-devtools-core',
+ 'react-devtools-shell',
+ 'react-devtools-shared',
+ 'react-interactions',
+ 'shared/ReactDOMSharedInternals',
+ ],
+ isFlowTyped: true,
+ isServerSupported: true,
+ },
+ {
+ shortName: 'dom-node-turbopack-bundled',
+ entryPoints: [
+ 'react-server-dom-turbopack/server.node',
+ 'react-server-dom-turbopack/client.node',
+ ],
+ paths: [
+ 'react-dom',
+ 'react-dom-bindings',
+ 'react-dom/client',
+ '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-server-dom-turbopack',
+ 'react-server-dom-turbopack/client.node',
+ 'react-server-dom-turbopack/server',
+ 'react-server-dom-turbopack/server.node',
+ 'react-server-dom-turbopack/src/ReactFlightDOMServerNode.js', // react-server-dom-turbopack/server.node
+ 'react-server-dom-turbopack/src/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node
+ 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js',
+ 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js',
+ 'react-server-dom-turbopack/node-register',
+ 'react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js',
+ 'react-devtools',
+ 'react-devtools-core',
+ 'react-devtools-shell',
+ 'react-devtools-shared',
+ 'react-interactions',
+ 'shared/ReactDOMSharedInternals',
+ ],
+ isFlowTyped: true,
+ isServerSupported: true,
+ },
{
shortName: 'dom-bun',
entryPoints: ['react-dom', 'react-dom/src/server/react-dom-server.bun.js'],
@@ -127,6 +236,36 @@ module.exports = [
isFlowTyped: true,
isServerSupported: true,
},
+ {
+ shortName: 'dom-browser-turbopack',
+ entryPoints: [
+ 'react-server-dom-turbopack/client.browser',
+ 'react-server-dom-turbopack/server.browser',
+ ],
+ paths: [
+ 'react-dom',
+ 'react-dom/client',
+ 'react-dom/server',
+ 'react-dom/server.node',
+ 'react-dom-bindings',
+ 'react-server-dom-turbopack',
+ 'react-server-dom-turbopack/client',
+ 'react-server-dom-turbopack/client.browser',
+ 'react-server-dom-turbopack/server.browser',
+ 'react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js', // react-server-dom-turbopack/client.browser
+ 'react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js', // react-server-dom-turbopack/server.browser
+ 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js',
+ 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser.js',
+ 'react-devtools',
+ 'react-devtools-core',
+ 'react-devtools-shell',
+ 'react-devtools-shared',
+ 'react-interactions',
+ 'shared/ReactDOMSharedInternals',
+ ],
+ isFlowTyped: true,
+ isServerSupported: true,
+ },
{
shortName: 'dom-edge-webpack',
entryPoints: [
@@ -163,38 +302,33 @@ module.exports = [
isServerSupported: true,
},
{
- shortName: 'dom-node-webpack',
+ shortName: 'dom-edge-turbopack',
entryPoints: [
- 'react-server-dom-webpack/server.node',
- 'react-server-dom-webpack/client.node',
+ 'react-server-dom-turbopack/server.edge',
+ 'react-server-dom-turbopack/client.edge',
],
paths: [
'react-dom',
'react-dom/src/ReactDOMSharedSubset.js',
'react-dom-bindings',
'react-dom/client',
- '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-server-dom-webpack',
- 'react-server-dom-webpack/client.node',
- 'react-server-dom-webpack/server',
- 'react-server-dom-webpack/server.node',
- 'react-server-dom-webpack/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node
- 'react-server-dom-webpack/src/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node
- 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js',
- 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer.js',
- 'react-server-dom-webpack/node-register',
- 'react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js',
+ 'react-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-server-dom-turbopack',
+ 'react-server-dom-turbopack/client.edge',
+ 'react-server-dom-turbopack/server.edge',
+ 'react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js', // react-server-dom-webpack/client.edge
+ 'react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js', // react-server-dom-webpack/server.edge
+ 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js',
+ 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js',
'react-devtools',
'react-devtools-core',
'react-devtools-shell',
'react-devtools-shared',
- 'react-interactions',
'shared/ReactDOMSharedInternals',
],
isFlowTyped: true,