[Flight] Implement `react-server-dom-turbopack` (#27315)
stacked on #27314 Turbopack requires a different module loading strategy than Webpack and as such this PR implements a new package `react-server-dom-turbopack` which largely follows the `react-server-dom-webpack` but is implemented for this new bundler
Josh Story committed
Sep 27, 2023 at 10:03 UTC
f81c0f1ed962036f25b87281b3c18cef3860ded8
68 files changed
+6733
-21
.eslintrc.js
+8
@@ -326,6 +326,7 @@ module.exports = {
326
'packages/react-refresh/**/*.js',
327
'packages/react-server-dom-esm/**/*.js',
328
'packages/react-server-dom-webpack/**/*.js',
329
+ 'packages/react-server-dom-turbopack/**/*.js',
330
'packages/react-test-renderer/**/*.js',
331
'packages/react-debug-tools/**/*.js',
332
'packages/react-devtools-extensions/**/*.js',
@@ -427,6 +428,13 @@ module.exports = {
428
__webpack_require__: 'readonly',
429
},
430
},
431
+ {
432
+ files: ['packages/react-server-dom-turbopack/**/*.js'],
433
+ globals: {
434
+ __turbopack_load__: 'readonly',
435
+ __turbopack_require__: 'readonly',
436
+ },
437
+ },
438
{
439
files: ['packages/scheduler/**/*.js'],
440
globals: {
packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js
new
+15
@@ -0,0 +1,15 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export * from 'react-client/src/ReactFlightClientConfigBrowser';
11
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack';
12
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser';
13
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser';
14
+export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
15
+export const usedWithSSR = false;
packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js
new
+15
@@ -0,0 +1,15 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export * from 'react-client/src/ReactFlightClientConfigBrowser';
11
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack';
12
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer';
13
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer';
14
+export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
15
+export const usedWithSSR = true;
packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js
new
+15
@@ -0,0 +1,15 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export * from 'react-client/src/ReactFlightClientConfigNode';
11
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack';
12
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer';
13
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer';
14
+export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
15
+export const usedWithSSR = true;
packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js
new
+14
@@ -0,0 +1,14 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export * from 'react-client/src/ReactFlightClientConfigNode';
11
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode';
12
+export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer';
13
+export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
14
+export const usedWithSSR = true;
packages/react-server-dom-turbopack/README.md
new
+5
@@ -0,0 +1,5 @@
1
+# react-server-dom-turbopack
2
+
3
+Experimental React Flight bindings for DOM using Turbopack.
4
+
5
+**Use it at your own risk.**
packages/react-server-dom-turbopack/client.browser.js
new
+10
@@ -0,0 +1,10 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export * from './src/ReactFlightDOMClientBrowser';
packages/react-server-dom-turbopack/client.edge.js
new
+10
@@ -0,0 +1,10 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export * from './src/ReactFlightDOMClientEdge';
packages/react-server-dom-turbopack/client.js
new
+10
@@ -0,0 +1,10 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export * from './client.browser';
packages/react-server-dom-turbopack/client.node.js
new
+10
@@ -0,0 +1,10 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export * from './src/ReactFlightDOMClientNode';
packages/react-server-dom-turbopack/client.node.unbundled.js
new
+10
@@ -0,0 +1,10 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export * from './src/ReactFlightDOMClientNode';
packages/react-server-dom-turbopack/esm/package.json
new
+3
@@ -0,0 +1,3 @@
1
+{
2
+ "type": "module"
3
+}
packages/react-server-dom-turbopack/esm/react-server-dom-turbopack-node-loader.production.min.js
new
+10
@@ -0,0 +1,10 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export * from '../src/ReactFlightTurbopackNodeLoader.js';
packages/react-server-dom-turbopack/index.js
new
+10
@@ -0,0 +1,10 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+throw new Error('Use react-server-dom-turbopack/client instead.');
packages/react-server-dom-turbopack/node-register.js
new
+10
@@ -0,0 +1,10 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+module.exports = require('./src/ReactFlightTurbopackNodeRegister');
packages/react-server-dom-turbopack/npm/client.browser.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/react-server-dom-turbopack-client.browser.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/react-server-dom-turbopack-client.browser.development.js');
7
+}
packages/react-server-dom-turbopack/npm/client.edge.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/react-server-dom-turbopack-client.edge.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/react-server-dom-turbopack-client.edge.development.js');
7
+}
packages/react-server-dom-turbopack/npm/client.js
new
+3
@@ -0,0 +1,3 @@
1
+'use strict';
2
+
3
+module.exports = require('./client.browser');
packages/react-server-dom-turbopack/npm/client.node.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/react-server-dom-turbopack-client.node.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/react-server-dom-turbopack-client.node.development.js');
7
+}
packages/react-server-dom-turbopack/npm/client.node.unbundled.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/react-server-dom-turbopack-client.node.unbundled.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/react-server-dom-turbopack-client.node.unbundled.development.js');
7
+}
packages/react-server-dom-turbopack/npm/esm/package.json
new
+3
@@ -0,0 +1,3 @@
1
+{
2
+ "type": "module"
3
+}
packages/react-server-dom-turbopack/npm/index.js
new
+12
@@ -0,0 +1,12 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+'use strict';
11
+
12
+throw new Error('Use react-server-dom-turbopack/client instead.');
packages/react-server-dom-turbopack/npm/node-register.js
new
+3
@@ -0,0 +1,3 @@
1
+'use strict';
2
+
3
+module.exports = require('./cjs/react-server-dom-turbopack-node-register.js');
packages/react-server-dom-turbopack/npm/server.browser.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/react-server-dom-turbopack-server.browser.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/react-server-dom-turbopack-server.browser.development.js');
7
+}
packages/react-server-dom-turbopack/npm/server.edge.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/react-server-dom-turbopack-server.edge.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/react-server-dom-turbopack-server.edge.development.js');
7
+}
packages/react-server-dom-turbopack/npm/server.js
new
+6
@@ -0,0 +1,6 @@
1
+'use strict';
2
+
3
+throw new Error(
4
+ 'The React Server Writer cannot be used outside a react-server environment. ' +
5
+ 'You must configure Node.js using the `--conditions react-server` flag.'
6
+);
packages/react-server-dom-turbopack/npm/server.node.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/react-server-dom-turbopack-server.node.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/react-server-dom-turbopack-server.node.development.js');
7
+}
packages/react-server-dom-turbopack/npm/server.node.unbundled.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/react-server-dom-turbopack-server.node.unbundled.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/react-server-dom-turbopack-server.node.unbundled.development.js');
7
+}
packages/react-server-dom-turbopack/package.json
new
+95
@@ -0,0 +1,95 @@
1
+{
2
+ "name": "react-server-dom-turbopack",
3
+ "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.",
4
+ "version": "18.2.0",
5
+ "keywords": [
6
+ "react"
7
+ ],
8
+ "homepage": "https://reactjs.org/",
9
+ "bugs": "https://github.com/facebook/react/issues",
10
+ "license": "MIT",
11
+ "files": [
12
+ "LICENSE",
13
+ "README.md",
14
+ "index.js",
15
+ "client.js",
16
+ "client.browser.js",
17
+ "client.edge.js",
18
+ "client.node.js",
19
+ "client.node.unbundled.js",
20
+ "server.js",
21
+ "server.browser.js",
22
+ "server.edge.js",
23
+ "server.node.js",
24
+ "server.node.unbundled.js",
25
+ "node-register.js",
26
+ "cjs/",
27
+ "umd/",
28
+ "esm/"
29
+ ],
30
+ "exports": {
31
+ ".": "./index.js",
32
+ "./client": {
33
+ "workerd": "./client.edge.js",
34
+ "deno": "./client.edge.js",
35
+ "worker": "./client.edge.js",
36
+ "node": {
37
+ "turbopack": "./client.node.js",
38
+ "webpack": "./client.node.js",
39
+ "default": "./client.node.unbundled.js"
40
+ },
41
+ "edge-light": "./client.edge.js",
42
+ "browser": "./client.browser.js",
43
+ "default": "./client.browser.js"
44
+ },
45
+ "./client.browser": "./client.browser.js",
46
+ "./client.edge": "./client.edge.js",
47
+ "./client.node": "./client.node.js",
48
+ "./client.node.unbundled": "./client.node.unbundled.js",
49
+ "./server": {
50
+ "react-server": {
51
+ "workerd": "./server.edge.js",
52
+ "deno": "./server.browser.js",
53
+ "node": {
54
+ "turbopack": "./server.node.js",
55
+ "webpack": "./server.node.js",
56
+ "default": "./server.node.unbundled.js"
57
+ },
58
+ "edge-light": "./server.edge.js",
59
+ "browser": "./server.browser.js"
60
+ },
61
+ "default": "./server.js"
62
+ },
63
+ "./server.browser": "./server.browser.js",
64
+ "./server.edge": "./server.edge.js",
65
+ "./server.node": "./server.node.js",
66
+ "./server.node.unbundled": "./server.node.unbundled.js",
67
+ "./node-loader": "./esm/react-server-dom-turbopack-node-loader.production.min.js",
68
+ "./node-register": "./node-register.js",
69
+ "./src/*": "./src/*.js",
70
+ "./package.json": "./package.json"
71
+ },
72
+ "main": "index.js",
73
+ "repository": {
74
+ "type" : "git",
75
+ "url" : "https://github.com/facebook/react.git",
76
+ "directory": "packages/react-server-dom-turbopack"
77
+ },
78
+ "engines": {
79
+ "node": ">=0.10.0"
80
+ },
81
+ "peerDependencies": {
82
+ "react": "^18.2.0",
83
+ "react-dom": "^18.2.0"
84
+ },
85
+ "dependencies": {
86
+ "acorn-loose": "^8.3.0",
87
+ "neo-async": "^2.6.1",
88
+ "loose-envify": "^1.1.0"
89
+ },
90
+ "browserify": {
91
+ "transform": [
92
+ "loose-envify"
93
+ ]
94
+ }
95
+}
packages/react-server-dom-turbopack/server.browser.js
new
+10
@@ -0,0 +1,10 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export * from './src/ReactFlightDOMServerBrowser';
packages/react-server-dom-turbopack/server.edge.js
new
+10
@@ -0,0 +1,10 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export * from './src/ReactFlightDOMServerEdge';
packages/react-server-dom-turbopack/server.js
new
+13
@@ -0,0 +1,13 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+throw new Error(
11
+ 'The React Server cannot be used outside a react-server environment. ' +
12
+ 'You must configure Node.js using the `--conditions react-server` flag.',
13
+);
packages/react-server-dom-turbopack/server.node.js
new
+10
@@ -0,0 +1,10 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export * from './src/ReactFlightDOMServerNode';
packages/react-server-dom-turbopack/server.node.unbundled.js
new
+10
@@ -0,0 +1,10 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export * from './src/ReactFlightDOMServerNode';
packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js
new
+162
@@ -0,0 +1,162 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {
11
+ Thenable,
12
+ FulfilledThenable,
13
+ RejectedThenable,
14
+} from 'shared/ReactTypes';
15
+
16
+import type {ImportMetadata} from './shared/ReactFlightImportMetadata';
17
+import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig';
18
+
19
+import {
20
+ ID,
21
+ CHUNKS,
22
+ NAME,
23
+ isAsyncImport,
24
+} from './shared/ReactFlightImportMetadata';
25
+import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig';
26
+
27
+export type SSRModuleMap = {
28
+ [clientId: string]: {
29
+ [clientExportName: string]: ClientReference<any>,
30
+ },
31
+};
32
+
33
+export type ServerManifest = void;
34
+
35
+export type ServerReferenceId = string;
36
+
37
+export opaque type ClientReferenceMetadata = ImportMetadata;
38
+
39
+// eslint-disable-next-line no-unused-vars
40
+export opaque type ClientReference<T> = {
41
+ specifier: string,
42
+ name: string,
43
+ async?: boolean,
44
+};
45
+
46
+// The reason this function needs to defined here in this file instead of just
47
+// being exported directly from the WebpackDestination... file is because the
48
+// ClientReferenceMetadata is opaque and we can't unwrap it there.
49
+// This should get inlined and we could also just implement an unwrapping function
50
+// though that risks it getting used in places it shouldn't be. This is unfortunate
51
+// but currently it seems to be the best option we have.
52
+export function prepareDestinationForModule(
53
+ moduleLoading: ModuleLoading,
54
+ nonce: ?string,
55
+ metadata: ClientReferenceMetadata,
56
+) {
57
+ prepareDestinationWithChunks(moduleLoading, metadata[CHUNKS], nonce);
58
+}
59
+
60
+export function resolveClientReference<T>(
61
+ bundlerConfig: SSRModuleMap,
62
+ metadata: ClientReferenceMetadata,
63
+): ClientReference<T> {
64
+ const moduleExports = bundlerConfig[metadata[ID]];
65
+ let resolvedModuleData = moduleExports[metadata[NAME]];
66
+ let name;
67
+ if (resolvedModuleData) {
68
+ // The potentially aliased name.
69
+ name = resolvedModuleData.name;
70
+ } else {
71
+ // If we don't have this specific name, we might have the full module.
72
+ resolvedModuleData = moduleExports['*'];
73
+ if (!resolvedModuleData) {
74
+ throw new Error(
75
+ 'Could not find the module "' +
76
+ metadata[ID] +
77
+ '" in the React SSR Manifest. ' +
78
+ 'This is probably a bug in the React Server Components bundler.',
79
+ );
80
+ }
81
+ name = metadata[NAME];
82
+ }
83
+ return {
84
+ specifier: resolvedModuleData.specifier,
85
+ name: name,
86
+ async: isAsyncImport(metadata),
87
+ };
88
+}
89
+
90
+export function resolveServerReference<T>(
91
+ bundlerConfig: ServerManifest,
92
+ id: ServerReferenceId,
93
+): ClientReference<T> {
94
+ const idx = id.lastIndexOf('#');
95
+ const specifier = id.slice(0, idx);
96
+ const name = id.slice(idx + 1);
97
+ return {specifier, name};
98
+}
99
+
100
+const asyncModuleCache: Map<string, Thenable<any>> = new Map();
101
+
102
+export function preloadModule<T>(
103
+ metadata: ClientReference<T>,
104
+): null | Thenable<any> {
105
+ const existingPromise = asyncModuleCache.get(metadata.specifier);
106
+ if (existingPromise) {
107
+ if (existingPromise.status === 'fulfilled') {
108
+ return null;
109
+ }
110
+ return existingPromise;
111
+ } else {
112
+ // $FlowFixMe[unsupported-syntax]
113
+ let modulePromise: Promise<T> = import(metadata.specifier);
114
+ if (metadata.async) {
115
+ // If the module is async, it must have been a CJS module.
116
+ // CJS modules are accessed through the default export in
117
+ // Node.js so we have to get the default export to get the
118
+ // full module exports.
119
+ modulePromise = modulePromise.then(function (value) {
120
+ return (value: any).default;
121
+ });
122
+ }
123
+ modulePromise.then(
124
+ value => {
125
+ const fulfilledThenable: FulfilledThenable<mixed> =
126
+ (modulePromise: any);
127
+ fulfilledThenable.status = 'fulfilled';
128
+ fulfilledThenable.value = value;
129
+ },
130
+ reason => {
131
+ const rejectedThenable: RejectedThenable<mixed> = (modulePromise: any);
132
+ rejectedThenable.status = 'rejected';
133
+ rejectedThenable.reason = reason;
134
+ },
135
+ );
136
+ asyncModuleCache.set(metadata.specifier, modulePromise);
137
+ return modulePromise;
138
+ }
139
+}
140
+
141
+export function requireModule<T>(metadata: ClientReference<T>): T {
142
+ let moduleExports;
143
+ // We assume that preloadModule has been called before, which
144
+ // should have added something to the module cache.
145
+ const promise: any = asyncModuleCache.get(metadata.specifier);
146
+ if (promise.status === 'fulfilled') {
147
+ moduleExports = promise.value;
148
+ } else {
149
+ throw promise.reason;
150
+ }
151
+ if (metadata.name === '*') {
152
+ // This is a placeholder value that represents that the caller imported this
153
+ // as a CommonJS module as is.
154
+ return moduleExports;
155
+ }
156
+ if (metadata.name === '') {
157
+ // This is a placeholder value that represents that the caller accessed the
158
+ // default property of this if it was an ESM interop module.
159
+ return moduleExports.default;
160
+ }
161
+ return moduleExports[metadata.name];
162
+}
packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js
new
+232
@@ -0,0 +1,232 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {
11
+ Thenable,
12
+ FulfilledThenable,
13
+ RejectedThenable,
14
+} from 'shared/ReactTypes';
15
+
16
+import type {
17
+ ImportMetadata,
18
+ ImportManifestEntry,
19
+} from './shared/ReactFlightImportMetadata';
20
+import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig';
21
+
22
+import {
23
+ ID,
24
+ CHUNKS,
25
+ NAME,
26
+ isAsyncImport,
27
+} from './shared/ReactFlightImportMetadata';
28
+
29
+import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig';
30
+
31
+import {loadChunk} from 'react-client/src/ReactFlightClientConfig';
32
+
33
+export type SSRModuleMap = null | {
34
+ [clientId: string]: {
35
+ [clientExportName: string]: ClientReferenceManifestEntry,
36
+ },
37
+};
38
+
39
+export type ServerManifest = {
40
+ [id: string]: ImportManifestEntry,
41
+};
42
+
43
+export type ServerReferenceId = string;
44
+
45
+export opaque type ClientReferenceManifestEntry = ImportManifestEntry;
46
+export opaque type ClientReferenceMetadata = ImportMetadata;
47
+
48
+// eslint-disable-next-line no-unused-vars
49
+export opaque type ClientReference<T> = ClientReferenceMetadata;
50
+
51
+// The reason this function needs to defined here in this file instead of just
52
+// being exported directly from the TurbopackDestination... file is because the
53
+// ClientReferenceMetadata is opaque and we can't unwrap it there.
54
+// This should get inlined and we could also just implement an unwrapping function
55
+// though that risks it getting used in places it shouldn't be. This is unfortunate
56
+// but currently it seems to be the best option we have.
57
+export function prepareDestinationForModule(
58
+ moduleLoading: ModuleLoading,
59
+ nonce: ?string,
60
+ metadata: ClientReferenceMetadata,
61
+) {
62
+ prepareDestinationWithChunks(moduleLoading, metadata[CHUNKS], nonce);
63
+}
64
+
65
+export function resolveClientReference<T>(
66
+ bundlerConfig: SSRModuleMap,
67
+ metadata: ClientReferenceMetadata,
68
+): ClientReference<T> {
69
+ if (bundlerConfig) {
70
+ const moduleExports = bundlerConfig[metadata[ID]];
71
+ let resolvedModuleData = moduleExports[metadata[NAME]];
72
+ let name;
73
+ if (resolvedModuleData) {
74
+ // The potentially aliased name.
75
+ name = resolvedModuleData.name;
76
+ } else {
77
+ // If we don't have this specific name, we might have the full module.
78
+ resolvedModuleData = moduleExports['*'];
79
+ if (!resolvedModuleData) {
80
+ throw new Error(
81
+ 'Could not find the module "' +
82
+ metadata[ID] +
83
+ '" in the React SSR Manifest. ' +
84
+ 'This is probably a bug in the React Server Components bundler.',
85
+ );
86
+ }
87
+ name = metadata[NAME];
88
+ }
89
+ if (isAsyncImport(metadata)) {
90
+ return [
91
+ resolvedModuleData.id,
92
+ resolvedModuleData.chunks,
93
+ name,
94
+ 1 /* async */,
95
+ ];
96
+ } else {
97
+ return [resolvedModuleData.id, resolvedModuleData.chunks, name];
98
+ }
99
+ }
100
+ return metadata;
101
+}
102
+
103
+export function resolveServerReference<T>(
104
+ bundlerConfig: ServerManifest,
105
+ id: ServerReferenceId,
106
+): ClientReference<T> {
107
+ let name = '';
108
+ let resolvedModuleData = bundlerConfig[id];
109
+ if (resolvedModuleData) {
110
+ // The potentially aliased name.
111
+ name = resolvedModuleData.name;
112
+ } else {
113
+ // We didn't find this specific export name but we might have the * export
114
+ // which contains this name as well.
115
+ // TODO: It's unfortunate that we now have to parse this string. We should
116
+ // probably go back to encoding path and name separately on the client reference.
117
+ const idx = id.lastIndexOf('#');
118
+ if (idx !== -1) {
119
+ name = id.slice(idx + 1);
120
+ resolvedModuleData = bundlerConfig[id.slice(0, idx)];
121
+ }
122
+ if (!resolvedModuleData) {
123
+ throw new Error(
124
+ 'Could not find the module "' +
125
+ id +
126
+ '" in the React Server Manifest. ' +
127
+ 'This is probably a bug in the React Server Components bundler.',
128
+ );
129
+ }
130
+ }
131
+ // TODO: This needs to return async: true if it's an async module.
132
+ return [resolvedModuleData.id, resolvedModuleData.chunks, name];
133
+}
134
+
135
+// The chunk cache contains all the chunks we've preloaded so far.
136
+// If they're still pending they're a thenable. This map also exists
137
+// in Turbopack but unfortunately it's not exposed so we have to
138
+// replicate it in user space. null means that it has already loaded.
139
+const chunkCache: Map<string, null | Promise<any>> = new Map();
140
+
141
+function requireAsyncModule(id: string): null | Thenable<any> {
142
+ // We've already loaded all the chunks. We can require the module.
143
+ const promise = __turbopack_require__(id);
144
+ if (typeof promise.then !== 'function') {
145
+ // This wasn't a promise after all.
146
+ return null;
147
+ } else if (promise.status === 'fulfilled') {
148
+ // This module was already resolved earlier.
149
+ return null;
150
+ } else {
151
+ // Instrument the Promise to stash the result.
152
+ promise.then(
153
+ value => {
154
+ const fulfilledThenable: FulfilledThenable<mixed> = (promise: any);
155
+ fulfilledThenable.status = 'fulfilled';
156
+ fulfilledThenable.value = value;
157
+ },
158
+ reason => {
159
+ const rejectedThenable: RejectedThenable<mixed> = (promise: any);
160
+ rejectedThenable.status = 'rejected';
161
+ rejectedThenable.reason = reason;
162
+ },
163
+ );
164
+ return promise;
165
+ }
166
+}
167
+
168
+function ignoreReject() {
169
+ // We rely on rejected promises to be handled by another listener.
170
+}
171
+// Start preloading the modules since we might need them soon.
172
+// This function doesn't suspend.
173
+export function preloadModule<T>(
174
+ metadata: ClientReference<T>,
175
+): null | Thenable<any> {
176
+ const chunks = metadata[CHUNKS];
177
+ const promises = [];
178
+ for (let i = 0; i < chunks.length; i++) {
179
+ const chunkFilename = chunks[i];
180
+ const entry = chunkCache.get(chunkFilename);
181
+ if (entry === undefined) {
182
+ const thenable = loadChunk(chunkFilename);
183
+ promises.push(thenable);
184
+ // $FlowFixMe[method-unbinding]
185
+ const resolve = chunkCache.set.bind(chunkCache, chunkFilename, null);
186
+ thenable.then(resolve, ignoreReject);
187
+ chunkCache.set(chunkFilename, thenable);
188
+ } else if (entry !== null) {
189
+ promises.push(entry);
190
+ }
191
+ }
192
+ if (isAsyncImport(metadata)) {
193
+ if (promises.length === 0) {
194
+ return requireAsyncModule(metadata[ID]);
195
+ } else {
196
+ return Promise.all(promises).then(() => {
197
+ return requireAsyncModule(metadata[ID]);
198
+ });
199
+ }
200
+ } else if (promises.length > 0) {
201
+ return Promise.all(promises);
202
+ } else {
203
+ return null;
204
+ }
205
+}
206
+
207
+// Actually require the module or suspend if it's not yet ready.
208
+// Increase priority if necessary.
209
+export function requireModule<T>(metadata: ClientReference<T>): T {
210
+ let moduleExports = __turbopack_require__(metadata[ID]);
211
+ if (isAsyncImport(metadata)) {
212
+ if (typeof moduleExports.then !== 'function') {
213
+ // This wasn't a promise after all.
214
+ } else if (moduleExports.status === 'fulfilled') {
215
+ // This Promise should've been instrumented by preloadModule.
216
+ moduleExports = moduleExports.value;
217
+ } else {
218
+ throw moduleExports.reason;
219
+ }
220
+ }
221
+ if (metadata[NAME] === '*') {
222
+ // This is a placeholder value that represents that the caller imported this
223
+ // as a CommonJS module as is.
224
+ return moduleExports;
225
+ }
226
+ if (metadata[NAME] === '') {
227
+ // This is a placeholder value that represents that the caller accessed the
228
+ // default property of this if it was an ESM interop module.
229
+ return moduleExports.__esModule ? moduleExports.default : moduleExports;
230
+ }
231
+ return moduleExports[metadata[NAME]];
232
+}
packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser.js
new
+12
@@ -0,0 +1,12 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export function loadChunk(filename: string): Promise<mixed> {
11
+ return __turbopack_load__(filename);
12
+}
packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js
new
+12
@@ -0,0 +1,12 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export function loadChunk(filename: string): Promise<mixed> {
11
+ return __turbopack_load__(filename);
12
+}
packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser.js
new
+18
@@ -0,0 +1,18 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export type ModuleLoading = null;
11
+
12
+export function prepareDestinationWithChunks(
13
+ moduleLoading: ModuleLoading,
14
+ chunks: mixed,
15
+ nonce: ?string,
16
+) {
17
+ // In the browser we don't need to prepare our destination since the browser is the Destination
18
+}
packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer.js
new
+32
@@ -0,0 +1,32 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import {preinitScriptForSSR} from 'react-client/src/ReactFlightClientConfig';
11
+
12
+export type ModuleLoading = null | {
13
+ prefix: string,
14
+ crossOrigin?: 'use-credentials' | '',
15
+};
16
+
17
+export function prepareDestinationWithChunks(
18
+ moduleLoading: ModuleLoading,
19
+ // Chunks are single-indexed filenames
20
+ chunks: Array<string>,
21
+ nonce: ?string,
22
+) {
23
+ if (moduleLoading !== null) {
24
+ for (let i = 0; i < chunks.length; i++) {
25
+ preinitScriptForSSR(
26
+ moduleLoading.prefix + chunks[i],
27
+ nonce,
28
+ moduleLoading.crossOrigin,
29
+ );
30
+ }
31
+ }
32
+}
packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js
new
+111
@@ -0,0 +1,111 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {Thenable} from 'shared/ReactTypes.js';
11
+
12
+import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
13
+
14
+import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
15
+
16
+import {
17
+ createResponse,
18
+ getRoot,
19
+ reportGlobalError,
20
+ processBinaryChunk,
21
+ close,
22
+} from 'react-client/src/ReactFlightClient';
23
+
24
+import {
25
+ processReply,
26
+ createServerReference,
27
+} from 'react-client/src/ReactFlightReplyClient';
28
+
29
+type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
30
+
31
+export type Options = {
32
+ callServer?: CallServerCallback,
33
+};
34
+
35
+function createResponseFromOptions(options: void | Options) {
36
+ return createResponse(
37
+ null,
38
+ null,
39
+ options && options.callServer ? options.callServer : undefined,
40
+ undefined, // nonce
41
+ );
42
+}
43
+
44
+function startReadingFromStream(
45
+ response: FlightResponse,
46
+ stream: ReadableStream,
47
+): void {
48
+ const reader = stream.getReader();
49
+ function progress({
50
+ done,
51
+ value,
52
+ }: {
53
+ done: boolean,
54
+ value: ?any,
55
+ ...
56
+ }): void | Promise<void> {
57
+ if (done) {
58
+ close(response);
59
+ return;
60
+ }
61
+ const buffer: Uint8Array = (value: any);
62
+ processBinaryChunk(response, buffer);
63
+ return reader.read().then(progress).catch(error);
64
+ }
65
+ function error(e: any) {
66
+ reportGlobalError(response, e);
67
+ }
68
+ reader.read().then(progress).catch(error);
69
+}
70
+
71
+function createFromReadableStream<T>(
72
+ stream: ReadableStream,
73
+ options?: Options,
74
+): Thenable<T> {
75
+ const response: FlightResponse = createResponseFromOptions(options);
76
+ startReadingFromStream(response, stream);
77
+ return getRoot(response);
78
+}
79
+
80
+function createFromFetch<T>(
81
+ promiseForResponse: Promise<Response>,
82
+ options?: Options,
83
+): Thenable<T> {
84
+ const response: FlightResponse = createResponseFromOptions(options);
85
+ promiseForResponse.then(
86
+ function (r) {
87
+ startReadingFromStream(response, (r.body: any));
88
+ },
89
+ function (e) {
90
+ reportGlobalError(response, e);
91
+ },
92
+ );
93
+ return getRoot(response);
94
+}
95
+
96
+function encodeReply(
97
+ value: ReactServerValue,
98
+): Promise<
99
+ string | URLSearchParams | FormData,
100
+> /* We don't use URLSearchParams yet but maybe */ {
101
+ return new Promise((resolve, reject) => {
102
+ processReply(value, '', resolve, reject);
103
+ });
104
+}
105
+
106
+export {
107
+ createFromFetch,
108
+ createFromReadableStream,
109
+ encodeReply,
110
+ createServerReference,
111
+};
packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js
new
+115
@@ -0,0 +1,115 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {Thenable} from 'shared/ReactTypes.js';
11
+
12
+import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
13
+
14
+import type {
15
+ SSRModuleMap,
16
+ ModuleLoading,
17
+} from 'react-client/src/ReactFlightClientConfig';
18
+
19
+type SSRManifest = {
20
+ moduleMap: SSRModuleMap,
21
+ moduleLoading: ModuleLoading,
22
+};
23
+
24
+import {
25
+ createResponse,
26
+ getRoot,
27
+ reportGlobalError,
28
+ processBinaryChunk,
29
+ close,
30
+} from 'react-client/src/ReactFlightClient';
31
+
32
+import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient';
33
+
34
+function noServerCall() {
35
+ throw new Error(
36
+ 'Server Functions cannot be called during initial render. ' +
37
+ 'This would create a fetch waterfall. Try to use a Server Component ' +
38
+ 'to pass data to Client Components instead.',
39
+ );
40
+}
41
+
42
+export function createServerReference<A: Iterable<any>, T>(
43
+ id: any,
44
+ callServer: any,
45
+): (...A) => Promise<T> {
46
+ return createServerReferenceImpl(id, noServerCall);
47
+}
48
+
49
+export type Options = {
50
+ ssrManifest: SSRManifest,
51
+ nonce?: string,
52
+};
53
+
54
+function createResponseFromOptions(options: Options) {
55
+ return createResponse(
56
+ options.ssrManifest.moduleMap,
57
+ options.ssrManifest.moduleLoading,
58
+ noServerCall,
59
+ typeof options.nonce === 'string' ? options.nonce : undefined,
60
+ );
61
+}
62
+
63
+function startReadingFromStream(
64
+ response: FlightResponse,
65
+ stream: ReadableStream,
66
+): void {
67
+ const reader = stream.getReader();
68
+ function progress({
69
+ done,
70
+ value,
71
+ }: {
72
+ done: boolean,
73
+ value: ?any,
74
+ ...
75
+ }): void | Promise<void> {
76
+ if (done) {
77
+ close(response);
78
+ return;
79
+ }
80
+ const buffer: Uint8Array = (value: any);
81
+ processBinaryChunk(response, buffer);
82
+ return reader.read().then(progress).catch(error);
83
+ }
84
+ function error(e: any) {
85
+ reportGlobalError(response, e);
86
+ }
87
+ reader.read().then(progress).catch(error);
88
+}
89
+
90
+function createFromReadableStream<T>(
91
+ stream: ReadableStream,
92
+ options: Options,
93
+): Thenable<T> {
94
+ const response: FlightResponse = createResponseFromOptions(options);
95
+ startReadingFromStream(response, stream);
96
+ return getRoot(response);
97
+}
98
+
99
+function createFromFetch<T>(
100
+ promiseForResponse: Promise<Response>,
101
+ options: Options,
102
+): Thenable<T> {
103
+ const response: FlightResponse = createResponseFromOptions(options);
104
+ promiseForResponse.then(
105
+ function (r) {
106
+ startReadingFromStream(response, (r.body: any));
107
+ },
108
+ function (e) {
109
+ reportGlobalError(response, e);
110
+ },
111
+ );
112
+ return getRoot(response);
113
+}
114
+
115
+export {createFromFetch, createFromReadableStream};
packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js
new
+75
@@ -0,0 +1,75 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {Thenable} from 'shared/ReactTypes.js';
11
+
12
+import type {Response} from 'react-client/src/ReactFlightClient';
13
+
14
+import type {
15
+ SSRModuleMap,
16
+ ModuleLoading,
17
+} from 'react-client/src/ReactFlightClientConfig';
18
+
19
+type SSRManifest = {
20
+ moduleMap: SSRModuleMap,
21
+ moduleLoading: ModuleLoading,
22
+};
23
+
24
+import type {Readable} from 'stream';
25
+
26
+import {
27
+ createResponse,
28
+ getRoot,
29
+ reportGlobalError,
30
+ processBinaryChunk,
31
+ close,
32
+} from 'react-client/src/ReactFlightClient';
33
+
34
+import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient';
35
+
36
+function noServerCall() {
37
+ throw new Error(
38
+ 'Server Functions cannot be called during initial render. ' +
39
+ 'This would create a fetch waterfall. Try to use a Server Component ' +
40
+ 'to pass data to Client Components instead.',
41
+ );
42
+}
43
+export type Options = {
44
+ nonce?: string,
45
+};
46
+
47
+export function createServerReference<A: Iterable<any>, T>(
48
+ id: any,
49
+ callServer: any,
50
+): (...A) => Promise<T> {
51
+ return createServerReferenceImpl(id, noServerCall);
52
+}
53
+
54
+function createFromNodeStream<T>(
55
+ stream: Readable,
56
+ ssrManifest: SSRManifest,
57
+ options?: Options,
58
+): Thenable<T> {
59
+ const response: Response = createResponse(
60
+ ssrManifest.moduleMap,
61
+ ssrManifest.moduleLoading,
62
+ noServerCall,
63
+ options && typeof options.nonce === 'string' ? options.nonce : undefined,
64
+ );
65
+ stream.on('data', chunk => {
66
+ processBinaryChunk(response, chunk);
67
+ });
68
+ stream.on('error', error => {
69
+ reportGlobalError(response, error);
70
+ });
71
+ stream.on('end', () => close(response));
72
+ return getRoot(response);
73
+}
74
+
75
+export {createFromNodeStream};
packages/react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js
new
+100
@@ -0,0 +1,100 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
11
+import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes';
12
+import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
13
+import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
14
+
15
+import {
16
+ createRequest,
17
+ startWork,
18
+ startFlowing,
19
+ abort,
20
+} from 'react-server/src/ReactFlightServer';
21
+
22
+import {
23
+ createResponse,
24
+ close,
25
+ getRoot,
26
+} from 'react-server/src/ReactFlightReplyServer';
27
+
28
+import {decodeAction} from 'react-server/src/ReactFlightActionServer';
29
+
30
+export {
31
+ registerServerReference,
32
+ registerClientReference,
33
+ createClientModuleProxy,
34
+} from './ReactFlightTurbopackReferences';
35
+
36
+type Options = {
37
+ identifierPrefix?: string,
38
+ signal?: AbortSignal,
39
+ context?: Array<[string, ServerContextJSONValue]>,
40
+ onError?: (error: mixed) => void,
41
+ onPostpone?: (reason: string) => void,
42
+};
43
+
44
+function renderToReadableStream(
45
+ model: ReactClientValue,
46
+ turbopackMap: ClientManifest,
47
+ options?: Options,
48
+): ReadableStream {
49
+ const request = createRequest(
50
+ model,
51
+ turbopackMap,
52
+ options ? options.onError : undefined,
53
+ options ? options.context : undefined,
54
+ options ? options.identifierPrefix : undefined,
55
+ options ? options.onPostpone : undefined,
56
+ );
57
+ if (options && options.signal) {
58
+ const signal = options.signal;
59
+ if (signal.aborted) {
60
+ abort(request, (signal: any).reason);
61
+ } else {
62
+ const listener = () => {
63
+ abort(request, (signal: any).reason);
64
+ signal.removeEventListener('abort', listener);
65
+ };
66
+ signal.addEventListener('abort', listener);
67
+ }
68
+ }
69
+ const stream = new ReadableStream(
70
+ {
71
+ type: 'bytes',
72
+ start: (controller): ?Promise<void> => {
73
+ startWork(request);
74
+ },
75
+ pull: (controller): ?Promise<void> => {
76
+ startFlowing(request, controller);
77
+ },
78
+ cancel: (reason): ?Promise<void> => {},
79
+ },
80
+ // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
81
+ {highWaterMark: 0},
82
+ );
83
+ return stream;
84
+}
85
+
86
+function decodeReply<T>(
87
+ body: string | FormData,
88
+ turbopackMap: ServerManifest,
89
+): Thenable<T> {
90
+ if (typeof body === 'string') {
91
+ const form = new FormData();
92
+ form.append('0', body);
93
+ body = form;
94
+ }
95
+ const response = createResponse(turbopackMap, '', body);
96
+ close(response);
97
+ return getRoot(response);
98
+}
99
+
100
+export {renderToReadableStream, decodeReply, decodeAction};
packages/react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js
new
+100
@@ -0,0 +1,100 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
11
+import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes';
12
+import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
13
+import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
14
+
15
+import {
16
+ createRequest,
17
+ startWork,
18
+ startFlowing,
19
+ abort,
20
+} from 'react-server/src/ReactFlightServer';
21
+
22
+import {
23
+ createResponse,
24
+ close,
25
+ getRoot,
26
+} from 'react-server/src/ReactFlightReplyServer';
27
+
28
+import {decodeAction} from 'react-server/src/ReactFlightActionServer';
29
+
30
+export {
31
+ registerServerReference,
32
+ registerClientReference,
33
+ createClientModuleProxy,
34
+} from './ReactFlightTurbopackReferences';
35
+
36
+type Options = {
37
+ identifierPrefix?: string,
38
+ signal?: AbortSignal,
39
+ context?: Array<[string, ServerContextJSONValue]>,
40
+ onError?: (error: mixed) => void,
41
+ onPostpone?: (reason: string) => void,
42
+};
43
+
44
+function renderToReadableStream(
45
+ model: ReactClientValue,
46
+ turbopackMap: ClientManifest,
47
+ options?: Options,
48
+): ReadableStream {
49
+ const request = createRequest(
50
+ model,
51
+ turbopackMap,
52
+ options ? options.onError : undefined,
53
+ options ? options.context : undefined,
54
+ options ? options.identifierPrefix : undefined,
55
+ options ? options.onPostpone : undefined,
56
+ );
57
+ if (options && options.signal) {
58
+ const signal = options.signal;
59
+ if (signal.aborted) {
60
+ abort(request, (signal: any).reason);
61
+ } else {
62
+ const listener = () => {
63
+ abort(request, (signal: any).reason);
64
+ signal.removeEventListener('abort', listener);
65
+ };
66
+ signal.addEventListener('abort', listener);
67
+ }
68
+ }
69
+ const stream = new ReadableStream(
70
+ {
71
+ type: 'bytes',
72
+ start: (controller): ?Promise<void> => {
73
+ startWork(request);
74
+ },
75
+ pull: (controller): ?Promise<void> => {
76
+ startFlowing(request, controller);
77
+ },
78
+ cancel: (reason): ?Promise<void> => {},
79
+ },
80
+ // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
81
+ {highWaterMark: 0},
82
+ );
83
+ return stream;
84
+}
85
+
86
+function decodeReply<T>(
87
+ body: string | FormData,
88
+ turbopackMap: ServerManifest,
89
+): Thenable<T> {
90
+ if (typeof body === 'string') {
91
+ const form = new FormData();
92
+ form.append('0', body);
93
+ body = form;
94
+ }
95
+ const response = createResponse(turbopackMap, '', body);
96
+ close(response);
97
+ return getRoot(response);
98
+}
99
+
100
+export {renderToReadableStream, decodeReply, decodeAction};
packages/react-server-dom-turbopack/src/ReactFlightDOMServerNode.js
new
+170
@@ -0,0 +1,170 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {
11
+ Request,
12
+ ReactClientValue,
13
+} from 'react-server/src/ReactFlightServer';
14
+import type {Destination} from 'react-server/src/ReactServerStreamConfigNode';
15
+import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
16
+import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
17
+import type {Busboy} from 'busboy';
18
+import type {Writable} from 'stream';
19
+import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes';
20
+
21
+import {
22
+ createRequest,
23
+ startWork,
24
+ startFlowing,
25
+ abort,
26
+} from 'react-server/src/ReactFlightServer';
27
+
28
+import {
29
+ createResponse,
30
+ reportGlobalError,
31
+ close,
32
+ resolveField,
33
+ resolveFileInfo,
34
+ resolveFileChunk,
35
+ resolveFileComplete,
36
+ getRoot,
37
+} from 'react-server/src/ReactFlightReplyServer';
38
+
39
+import {decodeAction} from 'react-server/src/ReactFlightActionServer';
40
+
41
+export {
42
+ registerServerReference,
43
+ registerClientReference,
44
+ createClientModuleProxy,
45
+} from './ReactFlightTurbopackReferences';
46
+
47
+function createDrainHandler(destination: Destination, request: Request) {
48
+ return () => startFlowing(request, destination);
49
+}
50
+
51
+type Options = {
52
+ onError?: (error: mixed) => void,
53
+ onPostpone?: (reason: string) => void,
54
+ context?: Array<[string, ServerContextJSONValue]>,
55
+ identifierPrefix?: string,
56
+};
57
+
58
+type PipeableStream = {
59
+ abort(reason: mixed): void,
60
+ pipe<T: Writable>(destination: T): T,
61
+};
62
+
63
+function renderToPipeableStream(
64
+ model: ReactClientValue,
65
+ turbopackMap: ClientManifest,
66
+ options?: Options,
67
+): PipeableStream {
68
+ const request = createRequest(
69
+ model,
70
+ turbopackMap,
71
+ options ? options.onError : undefined,
72
+ options ? options.context : undefined,
73
+ options ? options.identifierPrefix : undefined,
74
+ options ? options.onPostpone : undefined,
75
+ );
76
+ let hasStartedFlowing = false;
77
+ startWork(request);
78
+ return {
79
+ pipe<T: Writable>(destination: T): T {
80
+ if (hasStartedFlowing) {
81
+ throw new Error(
82
+ 'React currently only supports piping to one writable stream.',
83
+ );
84
+ }
85
+ hasStartedFlowing = true;
86
+ startFlowing(request, destination);
87
+ destination.on('drain', createDrainHandler(destination, request));
88
+ return destination;
89
+ },
90
+ abort(reason: mixed) {
91
+ abort(request, reason);
92
+ },
93
+ };
94
+}
95
+
96
+function decodeReplyFromBusboy<T>(
97
+ busboyStream: Busboy,
98
+ turbopackMap: ServerManifest,
99
+): Thenable<T> {
100
+ const response = createResponse(turbopackMap, '');
101
+ let pendingFiles = 0;
102
+ const queuedFields: Array<string> = [];
103
+ busboyStream.on('field', (name, value) => {
104
+ if (pendingFiles > 0) {
105
+ // Because the 'end' event fires two microtasks after the next 'field'
106
+ // we would resolve files and fields out of order. To handle this properly
107
+ // we queue any fields we receive until the previous file is done.
108
+ queuedFields.push(name, value);
109
+ } else {
110
+ resolveField(response, name, value);
111
+ }
112
+ });
113
+ busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => {
114
+ if (encoding.toLowerCase() === 'base64') {
115
+ throw new Error(
116
+ "React doesn't accept base64 encoded file uploads because we don't expect " +
117
+ "form data passed from a browser to ever encode data that way. If that's " +
118
+ 'the wrong assumption, we can easily fix it.',
119
+ );
120
+ }
121
+ pendingFiles++;
122
+ const file = resolveFileInfo(response, name, filename, mimeType);
123
+ value.on('data', chunk => {
124
+ resolveFileChunk(response, file, chunk);
125
+ });
126
+ value.on('end', () => {
127
+ resolveFileComplete(response, name, file);
128
+ pendingFiles--;
129
+ if (pendingFiles === 0) {
130
+ // Release any queued fields
131
+ for (let i = 0; i < queuedFields.length; i += 2) {
132
+ resolveField(response, queuedFields[i], queuedFields[i + 1]);
133
+ }
134
+ queuedFields.length = 0;
135
+ }
136
+ });
137
+ });
138
+ busboyStream.on('finish', () => {
139
+ close(response);
140
+ });
141
+ busboyStream.on('error', err => {
142
+ reportGlobalError(
143
+ response,
144
+ // $FlowFixMe[incompatible-call] types Error and mixed are incompatible
145
+ err,
146
+ );
147
+ });
148
+ return getRoot(response);
149
+}
150
+
151
+function decodeReply<T>(
152
+ body: string | FormData,
153
+ turbopackMap: ServerManifest,
154
+): Thenable<T> {
155
+ if (typeof body === 'string') {
156
+ const form = new FormData();
157
+ form.append('0', body);
158
+ body = form;
159
+ }
160
+ const response = createResponse(turbopackMap, '', body);
161
+ close(response);
162
+ return getRoot(response);
163
+}
164
+
165
+export {
166
+ renderToPipeableStream,
167
+ decodeReplyFromBusboy,
168
+ decodeReply,
169
+ decodeAction,
170
+};
packages/react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler.js
new
+93
@@ -0,0 +1,93 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
11
+import type {
12
+ ImportMetadata,
13
+ ImportManifestEntry,
14
+} from './shared/ReactFlightImportMetadata';
15
+
16
+import type {
17
+ ClientReference,
18
+ ServerReference,
19
+} from './ReactFlightTurbopackReferences';
20
+
21
+export type {ClientReference, ServerReference};
22
+
23
+export type ClientManifest = {
24
+ [id: string]: ClientReferenceManifestEntry,
25
+};
26
+
27
+export type ServerReferenceId = string;
28
+
29
+export type ClientReferenceMetadata = ImportMetadata;
30
+export opaque type ClientReferenceManifestEntry = ImportManifestEntry;
31
+
32
+export type ClientReferenceKey = string;
33
+
34
+export {
35
+ isClientReference,
36
+ isServerReference,
37
+} from './ReactFlightTurbopackReferences';
38
+
39
+export function getClientReferenceKey(
40
+ reference: ClientReference<any>,
41
+): ClientReferenceKey {
42
+ return reference.$$async ? reference.$$id + '#async' : reference.$$id;
43
+}
44
+
45
+export function resolveClientReferenceMetadata<T>(
46
+ config: ClientManifest,
47
+ clientReference: ClientReference<T>,
48
+): ClientReferenceMetadata {
49
+ const modulePath = clientReference.$$id;
50
+ let name = '';
51
+ let resolvedModuleData = config[modulePath];
52
+ if (resolvedModuleData) {
53
+ // The potentially aliased name.
54
+ name = resolvedModuleData.name;
55
+ } else {
56
+ // We didn't find this specific export name but we might have the * export
57
+ // which contains this name as well.
58
+ // TODO: It's unfortunate that we now have to parse this string. We should
59
+ // probably go back to encoding path and name separately on the client reference.
60
+ const idx = modulePath.lastIndexOf('#');
61
+ if (idx !== -1) {
62
+ name = modulePath.slice(idx + 1);
63
+ resolvedModuleData = config[modulePath.slice(0, idx)];
64
+ }
65
+ if (!resolvedModuleData) {
66
+ throw new Error(
67
+ 'Could not find the module "' +
68
+ modulePath +
69
+ '" in the React Client Manifest. ' +
70
+ 'This is probably a bug in the React Server Components bundler.',
71
+ );
72
+ }
73
+ }
74
+ if (clientReference.$$async === true) {
75
+ return [resolvedModuleData.id, resolvedModuleData.chunks, name, 1];
76
+ } else {
77
+ return [resolvedModuleData.id, resolvedModuleData.chunks, name];
78
+ }
79
+}
80
+
81
+export function getServerReferenceId<T>(
82
+ config: ClientManifest,
83
+ serverReference: ServerReference<T>,
84
+): ServerReferenceId {
85
+ return serverReference.$$id;
86
+}
87
+
88
+export function getServerReferenceBoundArguments<T>(
89
+ config: ClientManifest,
90
+ serverReference: ServerReference<T>,
91
+): null | Array<ReactClientValue> {
92
+ return serverReference.$$bound;
93
+}
packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js
new
+483
@@ -0,0 +1,483 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import * as acorn from 'acorn-loose';
11
+
12
+type ResolveContext = {
13
+ conditions: Array<string>,
14
+ parentURL: string | void,
15
+};
16
+
17
+type ResolveFunction = (
18
+ string,
19
+ ResolveContext,
20
+ ResolveFunction,
21
+) => {url: string} | Promise<{url: string}>;
22
+
23
+type GetSourceContext = {
24
+ format: string,
25
+};
26
+
27
+type GetSourceFunction = (
28
+ string,
29
+ GetSourceContext,
30
+ GetSourceFunction,
31
+) => Promise<{source: Source}>;
32
+
33
+type TransformSourceContext = {
34
+ format: string,
35
+ url: string,
36
+};
37
+
38
+type TransformSourceFunction = (
39
+ Source,
40
+ TransformSourceContext,
41
+ TransformSourceFunction,
42
+) => Promise<{source: Source}>;
43
+
44
+type LoadContext = {
45
+ conditions: Array<string>,
46
+ format: string | null | void,
47
+ importAssertions: Object,
48
+};
49
+
50
+type LoadFunction = (
51
+ string,
52
+ LoadContext,
53
+ LoadFunction,
54
+) => Promise<{format: string, shortCircuit?: boolean, source: Source}>;
55
+
56
+type Source = string | ArrayBuffer | Uint8Array;
57
+
58
+let warnedAboutConditionsFlag = false;
59
+
60
+let stashedGetSource: null | GetSourceFunction = null;
61
+let stashedResolve: null | ResolveFunction = null;
62
+
63
+export async function resolve(
64
+ specifier: string,
65
+ context: ResolveContext,
66
+ defaultResolve: ResolveFunction,
67
+): Promise<{url: string}> {
68
+ // We stash this in case we end up needing to resolve export * statements later.
69
+ stashedResolve = defaultResolve;
70
+
71
+ if (!context.conditions.includes('react-server')) {
72
+ context = {
73
+ ...context,
74
+ conditions: [...context.conditions, 'react-server'],
75
+ };
76
+ if (!warnedAboutConditionsFlag) {
77
+ warnedAboutConditionsFlag = true;
78
+ // eslint-disable-next-line react-internal/no-production-logging
79
+ console.warn(
80
+ 'You did not run Node.js with the `--conditions react-server` flag. ' +
81
+ 'Any "react-server" override will only work with ESM imports.',
82
+ );
83
+ }
84
+ }
85
+ return await defaultResolve(specifier, context, defaultResolve);
86
+}
87
+
88
+export async function getSource(
89
+ url: string,
90
+ context: GetSourceContext,
91
+ defaultGetSource: GetSourceFunction,
92
+): Promise<{source: Source}> {
93
+ // We stash this in case we end up needing to resolve export * statements later.
94
+ stashedGetSource = defaultGetSource;
95
+ return defaultGetSource(url, context, defaultGetSource);
96
+}
97
+
98
+function addLocalExportedNames(names: Map<string, string>, node: any) {
99
+ switch (node.type) {
100
+ case 'Identifier':
101
+ names.set(node.name, node.name);
102
+ return;
103
+ case 'ObjectPattern':
104
+ for (let i = 0; i < node.properties.length; i++)
105
+ addLocalExportedNames(names, node.properties[i]);
106
+ return;
107
+ case 'ArrayPattern':
108
+ for (let i = 0; i < node.elements.length; i++) {
109
+ const element = node.elements[i];
110
+ if (element) addLocalExportedNames(names, element);
111
+ }
112
+ return;
113
+ case 'Property':
114
+ addLocalExportedNames(names, node.value);
115
+ return;
116
+ case 'AssignmentPattern':
117
+ addLocalExportedNames(names, node.left);
118
+ return;
119
+ case 'RestElement':
120
+ addLocalExportedNames(names, node.argument);
121
+ return;
122
+ case 'ParenthesizedExpression':
123
+ addLocalExportedNames(names, node.expression);
124
+ return;
125
+ }
126
+}
127
+
128
+function transformServerModule(
129
+ source: string,
130
+ body: any,
131
+ url: string,
132
+ loader: LoadFunction,
133
+): string {
134
+ // If the same local name is exported more than once, we only need one of the names.
135
+ const localNames: Map<string, string> = new Map();
136
+ const localTypes: Map<string, string> = new Map();
137
+
138
+ for (let i = 0; i < body.length; i++) {
139
+ const node = body[i];
140
+ switch (node.type) {
141
+ case 'ExportAllDeclaration':
142
+ // If export * is used, the other file needs to explicitly opt into "use server" too.
143
+ break;
144
+ case 'ExportDefaultDeclaration':
145
+ if (node.declaration.type === 'Identifier') {
146
+ localNames.set(node.declaration.name, 'default');
147
+ } else if (node.declaration.type === 'FunctionDeclaration') {
148
+ if (node.declaration.id) {
149
+ localNames.set(node.declaration.id.name, 'default');
150
+ localTypes.set(node.declaration.id.name, 'function');
151
+ } else {
152
+ // TODO: This needs to be rewritten inline because it doesn't have a local name.
153
+ }
154
+ }
155
+ continue;
156
+ case 'ExportNamedDeclaration':
157
+ if (node.declaration) {
158
+ if (node.declaration.type === 'VariableDeclaration') {
159
+ const declarations = node.declaration.declarations;
160
+ for (let j = 0; j < declarations.length; j++) {
161
+ addLocalExportedNames(localNames, declarations[j].id);
162
+ }
163
+ } else {
164
+ const name = node.declaration.id.name;
165
+ localNames.set(name, name);
166
+ if (node.declaration.type === 'FunctionDeclaration') {
167
+ localTypes.set(name, 'function');
168
+ }
169
+ }
170
+ }
171
+ if (node.specifiers) {
172
+ const specifiers = node.specifiers;
173
+ for (let j = 0; j < specifiers.length; j++) {
174
+ const specifier = specifiers[j];
175
+ localNames.set(specifier.local.name, specifier.exported.name);
176
+ }
177
+ }
178
+ continue;
179
+ }
180
+ }
181
+ if (localNames.size === 0) {
182
+ return source;
183
+ }
184
+ let newSrc = source + '\n\n;';
185
+ newSrc +=
186
+ 'import {registerServerReference} from "react-server-dom-turbopack/server";\n';
187
+ localNames.forEach(function (exported, local) {
188
+ if (localTypes.get(local) !== 'function') {
189
+ // We first check if the export is a function and if so annotate it.
190
+ newSrc += 'if (typeof ' + local + ' === "function") ';
191
+ }
192
+ newSrc += 'registerServerReference(' + local + ',';
193
+ newSrc += JSON.stringify(url) + ',';
194
+ newSrc += JSON.stringify(exported) + ');\n';
195
+ });
196
+ return newSrc;
197
+}
198
+
199
+function addExportNames(names: Array<string>, node: any) {
200
+ switch (node.type) {
201
+ case 'Identifier':
202
+ names.push(node.name);
203
+ return;
204
+ case 'ObjectPattern':
205
+ for (let i = 0; i < node.properties.length; i++)
206
+ addExportNames(names, node.properties[i]);
207
+ return;
208
+ case 'ArrayPattern':
209
+ for (let i = 0; i < node.elements.length; i++) {
210
+ const element = node.elements[i];
211
+ if (element) addExportNames(names, element);
212
+ }
213
+ return;
214
+ case 'Property':
215
+ addExportNames(names, node.value);
216
+ return;
217
+ case 'AssignmentPattern':
218
+ addExportNames(names, node.left);
219
+ return;
220
+ case 'RestElement':
221
+ addExportNames(names, node.argument);
222
+ return;
223
+ case 'ParenthesizedExpression':
224
+ addExportNames(names, node.expression);
225
+ return;
226
+ }
227
+}
228
+
229
+function resolveClientImport(
230
+ specifier: string,
231
+ parentURL: string,
232
+): {url: string} | Promise<{url: string}> {
233
+ // Resolve an import specifier as if it was loaded by the client. This doesn't use
234
+ // the overrides that this loader does but instead reverts to the default.
235
+ // This resolution algorithm will not necessarily have the same configuration
236
+ // as the actual client loader. It should mostly work and if it doesn't you can
237
+ // always convert to explicit exported names instead.
238
+ const conditions = ['node', 'import'];
239
+ if (stashedResolve === null) {
240
+ throw new Error(
241
+ 'Expected resolve to have been called before transformSource',
242
+ );
243
+ }
244
+ return stashedResolve(specifier, {conditions, parentURL}, stashedResolve);
245
+}
246
+
247
+async function parseExportNamesInto(
248
+ body: any,
249
+ names: Array<string>,
250
+ parentURL: string,
251
+ loader: LoadFunction,
252
+): Promise<void> {
253
+ for (let i = 0; i < body.length; i++) {
254
+ const node = body[i];
255
+ switch (node.type) {
256
+ case 'ExportAllDeclaration':
257
+ if (node.exported) {
258
+ addExportNames(names, node.exported);
259
+ continue;
260
+ } else {
261
+ const {url} = await resolveClientImport(node.source.value, parentURL);
262
+ const {source} = await loader(
263
+ url,
264
+ {format: 'module', conditions: [], importAssertions: {}},
265
+ loader,
266
+ );
267
+ if (typeof source !== 'string') {
268
+ throw new Error('Expected the transformed source to be a string.');
269
+ }
270
+ let childBody;
271
+ try {
272
+ childBody = acorn.parse(source, {
273
+ ecmaVersion: '2024',
274
+ sourceType: 'module',
275
+ }).body;
276
+ } catch (x) {
277
+ // eslint-disable-next-line react-internal/no-production-logging
278
+ console.error('Error parsing %s %s', url, x.message);
279
+ continue;
280
+ }
281
+ await parseExportNamesInto(childBody, names, url, loader);
282
+ continue;
283
+ }
284
+ case 'ExportDefaultDeclaration':
285
+ names.push('default');
286
+ continue;
287
+ case 'ExportNamedDeclaration':
288
+ if (node.declaration) {
289
+ if (node.declaration.type === 'VariableDeclaration') {
290
+ const declarations = node.declaration.declarations;
291
+ for (let j = 0; j < declarations.length; j++) {
292
+ addExportNames(names, declarations[j].id);
293
+ }
294
+ } else {
295
+ addExportNames(names, node.declaration.id);
296
+ }
297
+ }
298
+ if (node.specifiers) {
299
+ const specifiers = node.specifiers;
300
+ for (let j = 0; j < specifiers.length; j++) {
301
+ addExportNames(names, specifiers[j].exported);
302
+ }
303
+ }
304
+ continue;
305
+ }
306
+ }
307
+}
308
+
309
+async function transformClientModule(
310
+ body: any,
311
+ url: string,
312
+ loader: LoadFunction,
313
+): Promise<string> {
314
+ const names: Array<string> = [];
315
+
316
+ await parseExportNamesInto(body, names, url, loader);
317
+
318
+ if (names.length === 0) {
319
+ return '';
320
+ }
321
+
322
+ let newSrc =
323
+ 'import {registerClientReference} from "react-server-dom-turbopack/server";\n';
324
+ for (let i = 0; i < names.length; i++) {
325
+ const name = names[i];
326
+ if (name === 'default') {
327
+ newSrc += 'export default ';
328
+ newSrc += 'registerClientReference(function() {';
329
+ newSrc +=
330
+ 'throw new Error(' +
331
+ JSON.stringify(
332
+ `Attempted to call the default export of ${url} from the server` +
333
+ `but it's on the client. It's not possible to invoke a client function from ` +
334
+ `the server, it can only be rendered as a Component or passed to props of a` +
335
+ `Client Component.`,
336
+ ) +
337
+ ');';
338
+ } else {
339
+ newSrc += 'export const ' + name + ' = ';
340
+ newSrc += 'registerClientReference(function() {';
341
+ newSrc +=
342
+ 'throw new Error(' +
343
+ JSON.stringify(
344
+ `Attempted to call ${name}() from the server but ${name} is on the client. ` +
345
+ `It's not possible to invoke a client function from the server, it can ` +
346
+ `only be rendered as a Component or passed to props of a Client Component.`,
347
+ ) +
348
+ ');';
349
+ }
350
+ newSrc += '},';
351
+ newSrc += JSON.stringify(url) + ',';
352
+ newSrc += JSON.stringify(name) + ');\n';
353
+ }
354
+ return newSrc;
355
+}
356
+
357
+async function loadClientImport(
358
+ url: string,
359
+ defaultTransformSource: TransformSourceFunction,
360
+): Promise<{format: string, shortCircuit?: boolean, source: Source}> {
361
+ if (stashedGetSource === null) {
362
+ throw new Error(
363
+ 'Expected getSource to have been called before transformSource',
364
+ );
365
+ }
366
+ // TODO: Validate that this is another module by calling getFormat.
367
+ const {source} = await stashedGetSource(
368
+ url,
369
+ {format: 'module'},
370
+ stashedGetSource,
371
+ );
372
+ const result = await defaultTransformSource(
373
+ source,
374
+ {format: 'module', url},
375
+ defaultTransformSource,
376
+ );
377
+ return {format: 'module', source: result.source};
378
+}
379
+
380
+async function transformModuleIfNeeded(
381
+ source: string,
382
+ url: string,
383
+ loader: LoadFunction,
384
+): Promise<string> {
385
+ // Do a quick check for the exact string. If it doesn't exist, don't
386
+ // bother parsing.
387
+ if (
388
+ source.indexOf('use client') === -1 &&
389
+ source.indexOf('use server') === -1
390
+ ) {
391
+ return source;
392
+ }
393
+
394
+ let body;
395
+ try {
396
+ body = acorn.parse(source, {
397
+ ecmaVersion: '2024',
398
+ sourceType: 'module',
399
+ }).body;
400
+ } catch (x) {
401
+ // eslint-disable-next-line react-internal/no-production-logging
402
+ console.error('Error parsing %s %s', url, x.message);
403
+ return source;
404
+ }
405
+
406
+ let useClient = false;
407
+ let useServer = false;
408
+ for (let i = 0; i < body.length; i++) {
409
+ const node = body[i];
410
+ if (node.type !== 'ExpressionStatement' || !node.directive) {
411
+ break;
412
+ }
413
+ if (node.directive === 'use client') {
414
+ useClient = true;
415
+ }
416
+ if (node.directive === 'use server') {
417
+ useServer = true;
418
+ }
419
+ }
420
+
421
+ if (!useClient && !useServer) {
422
+ return source;
423
+ }
424
+
425
+ if (useClient && useServer) {
426
+ throw new Error(
427
+ 'Cannot have both "use client" and "use server" directives in the same file.',
428
+ );
429
+ }
430
+
431
+ if (useClient) {
432
+ return transformClientModule(body, url, loader);
433
+ }
434
+
435
+ return transformServerModule(source, body, url, loader);
436
+}
437
+
438
+export async function transformSource(
439
+ source: Source,
440
+ context: TransformSourceContext,
441
+ defaultTransformSource: TransformSourceFunction,
442
+): Promise<{source: Source}> {
443
+ const transformed = await defaultTransformSource(
444
+ source,
445
+ context,
446
+ defaultTransformSource,
447
+ );
448
+ if (context.format === 'module') {
449
+ const transformedSource = transformed.source;
450
+ if (typeof transformedSource !== 'string') {
451
+ throw new Error('Expected source to have been transformed to a string.');
452
+ }
453
+ const newSrc = await transformModuleIfNeeded(
454
+ transformedSource,
455
+ context.url,
456
+ (url: string, ctx: LoadContext, defaultLoad: LoadFunction) => {
457
+ return loadClientImport(url, defaultTransformSource);
458
+ },
459
+ );
460
+ return {source: newSrc};
461
+ }
462
+ return transformed;
463
+}
464
+
465
+export async function load(
466
+ url: string,
467
+ context: LoadContext,
468
+ defaultLoad: LoadFunction,
469
+): Promise<{format: string, shortCircuit?: boolean, source: Source}> {
470
+ const result = await defaultLoad(url, context, defaultLoad);
471
+ if (result.format === 'module') {
472
+ if (typeof result.source !== 'string') {
473
+ throw new Error('Expected source to have been loaded into a string.');
474
+ }
475
+ const newSrc = await transformModuleIfNeeded(
476
+ result.source,
477
+ url,
478
+ defaultLoad,
479
+ );
480
+ return {format: 'module', source: newSrc};
481
+ }
482
+ return result;
483
+}
packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js
new
+110
@@ -0,0 +1,110 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+const acorn = require('acorn-loose');
11
+
12
+const url = require('url');
13
+
14
+const Module = require('module');
15
+
16
+module.exports = function register() {
17
+ const Server: any = require('react-server-dom-turbopack/server');
18
+ const registerServerReference = Server.registerServerReference;
19
+ const createClientModuleProxy = Server.createClientModuleProxy;
20
+
21
+ // $FlowFixMe[prop-missing] found when upgrading Flow
22
+ const originalCompile = Module.prototype._compile;
23
+
24
+ // $FlowFixMe[prop-missing] found when upgrading Flow
25
+ Module.prototype._compile = function (
26
+ this: any,
27
+ content: string,
28
+ filename: string,
29
+ ): void {
30
+ // Do a quick check for the exact string. If it doesn't exist, don't
31
+ // bother parsing.
32
+ if (
33
+ content.indexOf('use client') === -1 &&
34
+ content.indexOf('use server') === -1
35
+ ) {
36
+ return originalCompile.apply(this, arguments);
37
+ }
38
+
39
+ let body;
40
+ try {
41
+ body = acorn.parse(content, {
42
+ ecmaVersion: '2024',
43
+ sourceType: 'source',
44
+ }).body;
45
+ } catch (x) {
46
+ // eslint-disable-next-line react-internal/no-production-logging
47
+ console.error('Error parsing %s %s', url, x.message);
48
+ return originalCompile.apply(this, arguments);
49
+ }
50
+
51
+ let useClient = false;
52
+ let useServer = false;
53
+ for (let i = 0; i < body.length; i++) {
54
+ const node = body[i];
55
+ if (node.type !== 'ExpressionStatement' || !node.directive) {
56
+ break;
57
+ }
58
+ if (node.directive === 'use client') {
59
+ useClient = true;
60
+ }
61
+ if (node.directive === 'use server') {
62
+ useServer = true;
63
+ }
64
+ }
65
+
66
+ if (!useClient && !useServer) {
67
+ return originalCompile.apply(this, arguments);
68
+ }
69
+
70
+ if (useClient && useServer) {
71
+ throw new Error(
72
+ 'Cannot have both "use client" and "use server" directives in the same file.',
73
+ );
74
+ }
75
+
76
+ if (useClient) {
77
+ const moduleId: string = (url.pathToFileURL(filename).href: any);
78
+ this.exports = createClientModuleProxy(moduleId);
79
+ }
80
+
81
+ if (useServer) {
82
+ originalCompile.apply(this, arguments);
83
+
84
+ const moduleId: string = (url.pathToFileURL(filename).href: any);
85
+
86
+ const exports = this.exports;
87
+
88
+ // This module is imported server to server, but opts in to exposing functions by
89
+ // reference. If there are any functions in the export.
90
+ if (typeof exports === 'function') {
91
+ // The module exports a function directly,
92
+ registerServerReference(
93
+ (exports: any),
94
+ moduleId,
95
+ // Represents the whole Module object instead of a particular import.
96
+ null,
97
+ );
98
+ } else {
99
+ const keys = Object.keys(exports);
100
+ for (let i = 0; i < keys.length; i++) {
101
+ const key = keys[i];
102
+ const value = exports[keys[i]];
103
+ if (typeof value === 'function') {
104
+ registerServerReference((value: any), moduleId, key);
105
+ }
106
+ }
107
+ }
108
+ }
109
+ };
110
+};
packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js
new
+256
@@ -0,0 +1,256 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
11
+
12
+export type ServerReference<T: Function> = T & {
13
+ $$typeof: symbol,
14
+ $$id: string,
15
+ $$bound: null | Array<ReactClientValue>,
16
+};
17
+
18
+// eslint-disable-next-line no-unused-vars
19
+export type ClientReference<T> = {
20
+ $$typeof: symbol,
21
+ $$id: string,
22
+ $$async: boolean,
23
+};
24
+
25
+const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference');
26
+const SERVER_REFERENCE_TAG = Symbol.for('react.server.reference');
27
+
28
+export function isClientReference(reference: Object): boolean {
29
+ return reference.$$typeof === CLIENT_REFERENCE_TAG;
30
+}
31
+
32
+export function isServerReference(reference: Object): boolean {
33
+ return reference.$$typeof === SERVER_REFERENCE_TAG;
34
+}
35
+
36
+export function registerClientReference<T>(
37
+ proxyImplementation: any,
38
+ id: string,
39
+ exportName: string,
40
+): ClientReference<T> {
41
+ return registerClientReferenceImpl(
42
+ proxyImplementation,
43
+ id + '#' + exportName,
44
+ false,
45
+ );
46
+}
47
+
48
+function registerClientReferenceImpl<T>(
49
+ proxyImplementation: any,
50
+ id: string,
51
+ async: boolean,
52
+): ClientReference<T> {
53
+ return Object.defineProperties(proxyImplementation, {
54
+ $$typeof: {value: CLIENT_REFERENCE_TAG},
55
+ $$id: {value: id},
56
+ $$async: {value: async},
57
+ });
58
+}
59
+
60
+// $FlowFixMe[method-unbinding]
61
+const FunctionBind = Function.prototype.bind;
62
+// $FlowFixMe[method-unbinding]
63
+const ArraySlice = Array.prototype.slice;
64
+function bind(this: ServerReference<any>) {
65
+ // $FlowFixMe[unsupported-syntax]
66
+ const newFn = FunctionBind.apply(this, arguments);
67
+ if (this.$$typeof === SERVER_REFERENCE_TAG) {
68
+ const args = ArraySlice.call(arguments, 1);
69
+ newFn.$$typeof = SERVER_REFERENCE_TAG;
70
+ newFn.$$id = this.$$id;
71
+ newFn.$$bound = this.$$bound ? this.$$bound.concat(args) : args;
72
+ }
73
+ return newFn;
74
+}
75
+
76
+export function registerServerReference<T>(
77
+ reference: ServerReference<T>,
78
+ id: string,
79
+ exportName: null | string,
80
+): ServerReference<T> {
81
+ return Object.defineProperties((reference: any), {
82
+ $$typeof: {value: SERVER_REFERENCE_TAG},
83
+ $$id: {value: exportName === null ? id : id + '#' + exportName},
84
+ $$bound: {value: null},
85
+ bind: {value: bind},
86
+ });
87
+}
88
+
89
+const PROMISE_PROTOTYPE = Promise.prototype;
90
+
91
+const deepProxyHandlers = {
92
+ get: function (target: Function, name: string, receiver: Proxy<Function>) {
93
+ switch (name) {
94
+ // These names are read by the Flight runtime if you end up using the exports object.
95
+ case '$$typeof':
96
+ // These names are a little too common. We should probably have a way to
97
+ // have the Flight runtime extract the inner target instead.
98
+ return target.$$typeof;
99
+ case '$$id':
100
+ return target.$$id;
101
+ case '$$async':
102
+ return target.$$async;
103
+ case 'name':
104
+ return target.name;
105
+ case 'displayName':
106
+ return undefined;
107
+ // We need to special case this because createElement reads it if we pass this
108
+ // reference.
109
+ case 'defaultProps':
110
+ return undefined;
111
+ // Avoid this attempting to be serialized.
112
+ case 'toJSON':
113
+ return undefined;
114
+ case Symbol.toPrimitive:
115
+ // $FlowFixMe[prop-missing]
116
+ return Object.prototype[Symbol.toPrimitive];
117
+ case 'Provider':
118
+ throw new Error(
119
+ `Cannot render a Client Context Provider on the Server. ` +
120
+ `Instead, you can export a Client Component wrapper ` +
121
+ `that itself renders a Client Context Provider.`,
122
+ );
123
+ }
124
+ // eslint-disable-next-line react-internal/safe-string-coercion
125
+ const expression = String(target.name) + '.' + String(name);
126
+ throw new Error(
127
+ `Cannot access ${expression} on the server. ` +
128
+ 'You cannot dot into a client module from a server component. ' +
129
+ 'You can only pass the imported name through.',
130
+ );
131
+ },
132
+ set: function () {
133
+ throw new Error('Cannot assign to a client module from a server module.');
134
+ },
135
+};
136
+
137
+const proxyHandlers = {
138
+ get: function (
139
+ target: Function,
140
+ name: string,
141
+ receiver: Proxy<Function>,
142
+ ): $FlowFixMe {
143
+ switch (name) {
144
+ // These names are read by the Flight runtime if you end up using the exports object.
145
+ case '$$typeof':
146
+ return target.$$typeof;
147
+ case '$$id':
148
+ return target.$$id;
149
+ case '$$async':
150
+ return target.$$async;
151
+ case 'name':
152
+ return target.name;
153
+ // We need to special case this because createElement reads it if we pass this
154
+ // reference.
155
+ case 'defaultProps':
156
+ return undefined;
157
+ // Avoid this attempting to be serialized.
158
+ case 'toJSON':
159
+ return undefined;
160
+ case Symbol.toPrimitive:
161
+ // $FlowFixMe[prop-missing]
162
+ return Object.prototype[Symbol.toPrimitive];
163
+ case '__esModule':
164
+ // Something is conditionally checking which export to use. We'll pretend to be
165
+ // an ESM compat module but then we'll check again on the client.
166
+ const moduleId = target.$$id;
167
+ target.default = registerClientReferenceImpl(
168
+ (function () {
169
+ throw new Error(
170
+ `Attempted to call the default export of ${moduleId} from the server ` +
171
+ `but it's on the client. It's not possible to invoke a client function from ` +
172
+ `the server, it can only be rendered as a Component or passed to props of a ` +
173
+ `Client Component.`,
174
+ );
175
+ }: any),
176
+ target.$$id + '#',
177
+ target.$$async,
178
+ );
179
+ return true;
180
+ case 'then':
181
+ if (target.then) {
182
+ // Use a cached value
183
+ return target.then;
184
+ }
185
+ if (!target.$$async) {
186
+ // If this module is expected to return a Promise (such as an AsyncModule) then
187
+ // we should resolve that with a client reference that unwraps the Promise on
188
+ // the client.
189
+
190
+ const clientReference: ClientReference<any> =
191
+ registerClientReferenceImpl(({}: any), target.$$id, true);
192
+ const proxy = new Proxy(clientReference, proxyHandlers);
193
+
194
+ // Treat this as a resolved Promise for React's use()
195
+ target.status = 'fulfilled';
196
+ target.value = proxy;
197
+
198
+ const then = (target.then = registerClientReferenceImpl(
199
+ (function then(resolve, reject: any) {
200
+ // Expose to React.
201
+ return Promise.resolve(resolve(proxy));
202
+ }: any),
203
+ // If this is not used as a Promise but is treated as a reference to a `.then`
204
+ // export then we should treat it as a reference to that name.
205
+ target.$$id + '#then',
206
+ false,
207
+ ));
208
+ return then;
209
+ } else {
210
+ // Since typeof .then === 'function' is a feature test we'd continue recursing
211
+ // indefinitely if we return a function. Instead, we return an object reference
212
+ // if we check further.
213
+ return undefined;
214
+ }
215
+ }
216
+ let cachedReference = target[name];
217
+ if (!cachedReference) {
218
+ const reference: ClientReference<any> = registerClientReferenceImpl(
219
+ (function () {
220
+ throw new Error(
221
+ // eslint-disable-next-line react-internal/safe-string-coercion
222
+ `Attempted to call ${String(name)}() from the server but ${String(
223
+ name,
224
+ )} is on the client. ` +
225
+ `It's not possible to invoke a client function from the server, it can ` +
226
+ `only be rendered as a Component or passed to props of a Client Component.`,
227
+ );
228
+ }: any),
229
+ target.$$id + '#' + name,
230
+ target.$$async,
231
+ );
232
+ Object.defineProperty((reference: any), 'name', {value: name});
233
+ cachedReference = target[name] = new Proxy(reference, deepProxyHandlers);
234
+ }
235
+ return cachedReference;
236
+ },
237
+ getPrototypeOf(target: Function): Object {
238
+ // Pretend to be a Promise in case anyone asks.
239
+ return PROMISE_PROTOTYPE;
240
+ },
241
+ set: function (): empty {
242
+ throw new Error('Cannot assign to a client module from a server module.');
243
+ },
244
+};
245
+
246
+export function createClientModuleProxy<T>(
247
+ moduleId: string,
248
+): ClientReference<T> {
249
+ const clientReference: ClientReference<T> = registerClientReferenceImpl(
250
+ ({}: any),
251
+ // Represents the whole Module object instead of a particular import.
252
+ moduleId,
253
+ false,
254
+ );
255
+ return new Proxy(clientReference, proxyHandlers);
256
+}
packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js
new
+1572
@@ -0,0 +1,1572 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+// Polyfills for test environment
13
+global.ReadableStream =
14
+ require('web-streams-polyfill/ponyfill/es6').ReadableStream;
15
+global.TextEncoder = require('util').TextEncoder;
16
+global.TextDecoder = require('util').TextDecoder;
17
+
18
+// Don't wait before processing work on the server.
19
+// TODO: we can replace this with FlightServer.act().
20
+global.setImmediate = cb => cb();
21
+
22
+let act;
23
+let use;
24
+let clientExports;
25
+let clientModuleError;
26
+let turbopackMap;
27
+let Stream;
28
+let FlightReact;
29
+let React;
30
+let FlightReactDOM;
31
+let ReactDOMClient;
32
+let ReactServerDOMServer;
33
+let ReactServerDOMClient;
34
+let ReactDOMFizzServer;
35
+let Suspense;
36
+let ErrorBoundary;
37
+let JSDOM;
38
+
39
+describe('ReactFlightDOM', () => {
40
+ beforeEach(() => {
41
+ // For this first reset we are going to load the dom-node version of react-server-dom-turbopack/server
42
+ // This can be thought of as essentially being the React Server Components scope with react-server
43
+ // condition
44
+ jest.resetModules();
45
+
46
+ JSDOM = require('jsdom').JSDOM;
47
+
48
+ // Simulate the condition resolution
49
+ jest.mock('react-server-dom-turbopack/server', () =>
50
+ require('react-server-dom-turbopack/server.node.unbundled'),
51
+ );
52
+ jest.mock('react', () => require('react/react.shared-subset'));
53
+
54
+ const TurbopackMock = require('./utils/TurbopackMock');
55
+ clientExports = TurbopackMock.clientExports;
56
+ clientModuleError = TurbopackMock.clientModuleError;
57
+ turbopackMap = TurbopackMock.turbopackMap;
58
+
59
+ ReactServerDOMServer = require('react-server-dom-turbopack/server');
60
+ FlightReact = require('react');
61
+ FlightReactDOM = require('react-dom');
62
+
63
+ // This reset is to load modules for the SSR/Browser scope.
64
+ jest.resetModules();
65
+ jest.unmock('react');
66
+ act = require('internal-test-utils').act;
67
+ Stream = require('stream');
68
+ React = require('react');
69
+ use = React.use;
70
+ Suspense = React.Suspense;
71
+ ReactDOMClient = require('react-dom/client');
72
+ ReactDOMFizzServer = require('react-dom/server.node');
73
+ ReactServerDOMClient = require('react-server-dom-turbopack/client');
74
+
75
+ ErrorBoundary = class extends React.Component {
76
+ state = {hasError: false, error: null};
77
+ static getDerivedStateFromError(error) {
78
+ return {
79
+ hasError: true,
80
+ error,
81
+ };
82
+ }
83
+ render() {
84
+ if (this.state.hasError) {
85
+ return this.props.fallback(this.state.error);
86
+ }
87
+ return this.props.children;
88
+ }
89
+ };
90
+ });
91
+
92
+ function getTestStream() {
93
+ const writable = new Stream.PassThrough();
94
+ const readable = new ReadableStream({
95
+ start(controller) {
96
+ writable.on('data', chunk => {
97
+ controller.enqueue(chunk);
98
+ });
99
+ writable.on('end', () => {
100
+ controller.close();
101
+ });
102
+ },
103
+ });
104
+ return {
105
+ readable,
106
+ writable,
107
+ };
108
+ }
109
+
110
+ const theInfinitePromise = new Promise(() => {});
111
+ function InfiniteSuspend() {
112
+ throw theInfinitePromise;
113
+ }
114
+
115
+ function getMeaningfulChildren(element) {
116
+ const children = [];
117
+ let node = element.firstChild;
118
+ while (node) {
119
+ if (node.nodeType === 1) {
120
+ if (
121
+ // some tags are ambiguous and might be hidden because they look like non-meaningful children
122
+ // so we have a global override where if this data attribute is included we also include the node
123
+ node.hasAttribute('data-meaningful') ||
124
+ (node.tagName === 'SCRIPT' &&
125
+ node.hasAttribute('src') &&
126
+ node.hasAttribute('async')) ||
127
+ (node.tagName !== 'SCRIPT' &&
128
+ node.tagName !== 'TEMPLATE' &&
129
+ node.tagName !== 'template' &&
130
+ !node.hasAttribute('hidden') &&
131
+ !node.hasAttribute('aria-hidden'))
132
+ ) {
133
+ const props = {};
134
+ const attributes = node.attributes;
135
+ for (let i = 0; i < attributes.length; i++) {
136
+ if (
137
+ attributes[i].name === 'id' &&
138
+ attributes[i].value.includes(':')
139
+ ) {
140
+ // We assume this is a React added ID that's a non-visual implementation detail.
141
+ continue;
142
+ }
143
+ props[attributes[i].name] = attributes[i].value;
144
+ }
145
+ props.children = getMeaningfulChildren(node);
146
+ children.push(React.createElement(node.tagName.toLowerCase(), props));
147
+ }
148
+ } else if (node.nodeType === 3) {
149
+ children.push(node.data);
150
+ }
151
+ node = node.nextSibling;
152
+ }
153
+ return children.length === 0
154
+ ? undefined
155
+ : children.length === 1
156
+ ? children[0]
157
+ : children;
158
+ }
159
+
160
+ it('should resolve HTML using Node streams', async () => {
161
+ function Text({children}) {
162
+ return <span>{children}</span>;
163
+ }
164
+ function HTML() {
165
+ return (
166
+ <div>
167
+ <Text>hello</Text>
168
+ <Text>world</Text>
169
+ </div>
170
+ );
171
+ }
172
+
173
+ function App() {
174
+ const model = {
175
+ html: <HTML />,
176
+ };
177
+ return model;
178
+ }
179
+
180
+ const {writable, readable} = getTestStream();
181
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
182
+ <App />,
183
+ turbopackMap,
184
+ );
185
+ pipe(writable);
186
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
187
+ const model = await response;
188
+ expect(model).toEqual({
189
+ html: (
190
+ <div>
191
+ <span>hello</span>
192
+ <span>world</span>
193
+ </div>
194
+ ),
195
+ });
196
+ });
197
+
198
+ it('should resolve the root', async () => {
199
+ // Model
200
+ function Text({children}) {
201
+ return <span>{children}</span>;
202
+ }
203
+ function HTML() {
204
+ return (
205
+ <div>
206
+ <Text>hello</Text>
207
+ <Text>world</Text>
208
+ </div>
209
+ );
210
+ }
211
+ function RootModel() {
212
+ return {
213
+ html: <HTML />,
214
+ };
215
+ }
216
+
217
+ // View
218
+ function Message({response}) {
219
+ return <section>{use(response).html}</section>;
220
+ }
221
+ function App({response}) {
222
+ return (
223
+ <Suspense fallback={<h1>Loading...</h1>}>
224
+ <Message response={response} />
225
+ </Suspense>
226
+ );
227
+ }
228
+
229
+ const {writable, readable} = getTestStream();
230
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
231
+ <RootModel />,
232
+ turbopackMap,
233
+ );
234
+ pipe(writable);
235
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
236
+
237
+ const container = document.createElement('div');
238
+ const root = ReactDOMClient.createRoot(container);
239
+ await act(() => {
240
+ root.render(<App response={response} />);
241
+ });
242
+ expect(container.innerHTML).toBe(
243
+ '<section><div><span>hello</span><span>world</span></div></section>',
244
+ );
245
+ });
246
+
247
+ it('should not get confused by $', async () => {
248
+ // Model
249
+ function RootModel() {
250
+ return {text: '$1'};
251
+ }
252
+
253
+ // View
254
+ function Message({response}) {
255
+ return <p>{use(response).text}</p>;
256
+ }
257
+ function App({response}) {
258
+ return (
259
+ <Suspense fallback={<h1>Loading...</h1>}>
260
+ <Message response={response} />
261
+ </Suspense>
262
+ );
263
+ }
264
+
265
+ const {writable, readable} = getTestStream();
266
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
267
+ <RootModel />,
268
+ turbopackMap,
269
+ );
270
+ pipe(writable);
271
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
272
+
273
+ const container = document.createElement('div');
274
+ const root = ReactDOMClient.createRoot(container);
275
+ await act(() => {
276
+ root.render(<App response={response} />);
277
+ });
278
+ expect(container.innerHTML).toBe('<p>$1</p>');
279
+ });
280
+
281
+ it('should not get confused by @', async () => {
282
+ // Model
283
+ function RootModel() {
284
+ return {text: '@div'};
285
+ }
286
+
287
+ // View
288
+ function Message({response}) {
289
+ return <p>{use(response).text}</p>;
290
+ }
291
+ function App({response}) {
292
+ return (
293
+ <Suspense fallback={<h1>Loading...</h1>}>
294
+ <Message response={response} />
295
+ </Suspense>
296
+ );
297
+ }
298
+
299
+ const {writable, readable} = getTestStream();
300
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
301
+ <RootModel />,
302
+ turbopackMap,
303
+ );
304
+ pipe(writable);
305
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
306
+
307
+ const container = document.createElement('div');
308
+ const root = ReactDOMClient.createRoot(container);
309
+ await act(() => {
310
+ root.render(<App response={response} />);
311
+ });
312
+ expect(container.innerHTML).toBe('<p>@div</p>');
313
+ });
314
+
315
+ it('should be able to esm compat test module references', async () => {
316
+ const ESMCompatModule = {
317
+ __esModule: true,
318
+ default: function ({greeting}) {
319
+ return greeting + ' World';
320
+ },
321
+ hi: 'Hello',
322
+ };
323
+
324
+ function Print({response}) {
325
+ return <p>{use(response)}</p>;
326
+ }
327
+
328
+ function App({response}) {
329
+ return (
330
+ <Suspense fallback={<h1>Loading...</h1>}>
331
+ <Print response={response} />
332
+ </Suspense>
333
+ );
334
+ }
335
+
336
+ function interopWebpack(obj) {
337
+ // Basically what Webpack's ESM interop feature testing does.
338
+ if (typeof obj === 'object' && obj.__esModule) {
339
+ return obj;
340
+ }
341
+ return Object.assign({default: obj}, obj);
342
+ }
343
+
344
+ const {default: Component, hi} = interopWebpack(
345
+ clientExports(ESMCompatModule),
346
+ );
347
+
348
+ const {writable, readable} = getTestStream();
349
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
350
+ <Component greeting={hi} />,
351
+ turbopackMap,
352
+ );
353
+ pipe(writable);
354
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
355
+
356
+ const container = document.createElement('div');
357
+ const root = ReactDOMClient.createRoot(container);
358
+ await act(() => {
359
+ root.render(<App response={response} />);
360
+ });
361
+ expect(container.innerHTML).toBe('<p>Hello World</p>');
362
+ });
363
+
364
+ it('should be able to render a named component export', async () => {
365
+ const Module = {
366
+ Component: function ({greeting}) {
367
+ return greeting + ' World';
368
+ },
369
+ };
370
+
371
+ function Print({response}) {
372
+ return <p>{use(response)}</p>;
373
+ }
374
+
375
+ function App({response}) {
376
+ return (
377
+ <Suspense fallback={<h1>Loading...</h1>}>
378
+ <Print response={response} />
379
+ </Suspense>
380
+ );
381
+ }
382
+
383
+ const {Component} = clientExports(Module);
384
+
385
+ const {writable, readable} = getTestStream();
386
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
387
+ <Component greeting={'Hello'} />,
388
+ turbopackMap,
389
+ );
390
+ pipe(writable);
391
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
392
+
393
+ const container = document.createElement('div');
394
+ const root = ReactDOMClient.createRoot(container);
395
+ await act(() => {
396
+ root.render(<App response={response} />);
397
+ });
398
+ expect(container.innerHTML).toBe('<p>Hello World</p>');
399
+ });
400
+
401
+ it('should be able to render a module split named component export', async () => {
402
+ const Module = {
403
+ // This gets split into a separate module from the original one.
404
+ split: function ({greeting}) {
405
+ return greeting + ' World';
406
+ },
407
+ };
408
+
409
+ function Print({response}) {
410
+ return <p>{use(response)}</p>;
411
+ }
412
+
413
+ function App({response}) {
414
+ return (
415
+ <Suspense fallback={<h1>Loading...</h1>}>
416
+ <Print response={response} />
417
+ </Suspense>
418
+ );
419
+ }
420
+
421
+ const {split: Component} = clientExports(Module);
422
+
423
+ const {writable, readable} = getTestStream();
424
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
425
+ <Component greeting={'Hello'} />,
426
+ turbopackMap,
427
+ );
428
+ pipe(writable);
429
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
430
+
431
+ const container = document.createElement('div');
432
+ const root = ReactDOMClient.createRoot(container);
433
+ await act(() => {
434
+ root.render(<App response={response} />);
435
+ });
436
+ expect(container.innerHTML).toBe('<p>Hello World</p>');
437
+ });
438
+
439
+ it('should unwrap async module references', async () => {
440
+ const AsyncModule = Promise.resolve(function AsyncModule({text}) {
441
+ return 'Async: ' + text;
442
+ });
443
+
444
+ const AsyncModule2 = Promise.resolve({
445
+ exportName: 'Module',
446
+ });
447
+
448
+ function Print({response}) {
449
+ return <p>{use(response)}</p>;
450
+ }
451
+
452
+ function App({response}) {
453
+ return (
454
+ <Suspense fallback={<h1>Loading...</h1>}>
455
+ <Print response={response} />
456
+ </Suspense>
457
+ );
458
+ }
459
+
460
+ const AsyncModuleRef = await clientExports(AsyncModule);
461
+ const AsyncModuleRef2 = await clientExports(AsyncModule2);
462
+
463
+ const {writable, readable} = getTestStream();
464
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
465
+ <AsyncModuleRef text={AsyncModuleRef2.exportName} />,
466
+ turbopackMap,
467
+ );
468
+ pipe(writable);
469
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
470
+
471
+ const container = document.createElement('div');
472
+ const root = ReactDOMClient.createRoot(container);
473
+ await act(() => {
474
+ root.render(<App response={response} />);
475
+ });
476
+ expect(container.innerHTML).toBe('<p>Async: Module</p>');
477
+ });
478
+
479
+ it('should unwrap async module references using use', async () => {
480
+ const AsyncModule = Promise.resolve('Async Text');
481
+
482
+ function Print({response}) {
483
+ return use(response);
484
+ }
485
+
486
+ function App({response}) {
487
+ return (
488
+ <Suspense fallback={<h1>Loading...</h1>}>
489
+ <Print response={response} />
490
+ </Suspense>
491
+ );
492
+ }
493
+
494
+ const AsyncModuleRef = clientExports(AsyncModule);
495
+
496
+ function ServerComponent() {
497
+ const text = FlightReact.use(AsyncModuleRef);
498
+ return <p>{text}</p>;
499
+ }
500
+
501
+ const {writable, readable} = getTestStream();
502
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
503
+ <ServerComponent />,
504
+ turbopackMap,
505
+ );
506
+ pipe(writable);
507
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
508
+
509
+ const container = document.createElement('div');
510
+ const root = ReactDOMClient.createRoot(container);
511
+ await act(() => {
512
+ root.render(<App response={response} />);
513
+ });
514
+ expect(container.innerHTML).toBe('<p>Async Text</p>');
515
+ });
516
+
517
+ it('should be able to import a name called "then"', async () => {
518
+ const thenExports = {
519
+ then: function then() {
520
+ return 'and then';
521
+ },
522
+ };
523
+
524
+ function Print({response}) {
525
+ return <p>{use(response)}</p>;
526
+ }
527
+
528
+ function App({response}) {
529
+ return (
530
+ <Suspense fallback={<h1>Loading...</h1>}>
531
+ <Print response={response} />
532
+ </Suspense>
533
+ );
534
+ }
535
+
536
+ const ThenRef = clientExports(thenExports).then;
537
+
538
+ const {writable, readable} = getTestStream();
539
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
540
+ <ThenRef />,
541
+ turbopackMap,
542
+ );
543
+ pipe(writable);
544
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
545
+
546
+ const container = document.createElement('div');
547
+ const root = ReactDOMClient.createRoot(container);
548
+ await act(() => {
549
+ root.render(<App response={response} />);
550
+ });
551
+ expect(container.innerHTML).toBe('<p>and then</p>');
552
+ });
553
+
554
+ it('throws when accessing a member below the client exports', () => {
555
+ const ClientModule = clientExports({
556
+ Component: {deep: 'thing'},
557
+ });
558
+ function dotting() {
559
+ return ClientModule.Component.deep;
560
+ }
561
+ expect(dotting).toThrowError(
562
+ 'Cannot access Component.deep on the server. ' +
563
+ 'You cannot dot into a client module from a server component. ' +
564
+ 'You can only pass the imported name through.',
565
+ );
566
+ });
567
+
568
+ it('does not throw when React inspects any deep props', () => {
569
+ const ClientModule = clientExports({
570
+ Component: function () {},
571
+ });
572
+ <ClientModule.Component key="this adds instrumentation" />;
573
+ });
574
+
575
+ it('throws when accessing a Context.Provider below the client exports', () => {
576
+ const Context = React.createContext();
577
+ const ClientModule = clientExports({
578
+ Context,
579
+ });
580
+ function dotting() {
581
+ return ClientModule.Context.Provider;
582
+ }
583
+ expect(dotting).toThrowError(
584
+ `Cannot render a Client Context Provider on the Server. ` +
585
+ `Instead, you can export a Client Component wrapper ` +
586
+ `that itself renders a Client Context Provider.`,
587
+ );
588
+ });
589
+
590
+ it('should progressively reveal server components', async () => {
591
+ let reportedErrors = [];
592
+
593
+ // Client Components
594
+
595
+ function MyErrorBoundary({children}) {
596
+ return (
597
+ <ErrorBoundary
598
+ fallback={e => (
599
+ <p>
600
+ {__DEV__ ? e.message + ' + ' : null}
601
+ {e.digest}
602
+ </p>
603
+ )}>
604
+ {children}
605
+ </ErrorBoundary>
606
+ );
607
+ }
608
+
609
+ // Model
610
+ function Text({children}) {
611
+ return children;
612
+ }
613
+
614
+ function makeDelayedText() {
615
+ let _resolve, _reject;
616
+ let promise = new Promise((resolve, reject) => {
617
+ _resolve = () => {
618
+ promise = null;
619
+ resolve();
620
+ };
621
+ _reject = e => {
622
+ promise = null;
623
+ reject(e);
624
+ };
625
+ });
626
+ async function DelayedText({children}) {
627
+ await promise;
628
+ return <Text>{children}</Text>;
629
+ }
630
+ return [DelayedText, _resolve, _reject];
631
+ }
632
+
633
+ const [Friends, resolveFriends] = makeDelayedText();
634
+ const [Name, resolveName] = makeDelayedText();
635
+ const [Posts, resolvePosts] = makeDelayedText();
636
+ const [Photos, resolvePhotos] = makeDelayedText();
637
+ const [Games, , rejectGames] = makeDelayedText();
638
+
639
+ // View
640
+ function ProfileDetails({avatar}) {
641
+ return (
642
+ <div>
643
+ <Name>:name:</Name>
644
+ {avatar}
645
+ </div>
646
+ );
647
+ }
648
+ function ProfileSidebar({friends}) {
649
+ return (
650
+ <div>
651
+ <Photos>:photos:</Photos>
652
+ {friends}
653
+ </div>
654
+ );
655
+ }
656
+ function ProfilePosts({posts}) {
657
+ return <div>{posts}</div>;
658
+ }
659
+ function ProfileGames({games}) {
660
+ return <div>{games}</div>;
661
+ }
662
+
663
+ const MyErrorBoundaryClient = clientExports(MyErrorBoundary);
664
+
665
+ function ProfileContent() {
666
+ return (
667
+ <>
668
+ <ProfileDetails avatar={<Text>:avatar:</Text>} />
669
+ <Suspense fallback={<p>(loading sidebar)</p>}>
670
+ <ProfileSidebar friends={<Friends>:friends:</Friends>} />
671
+ </Suspense>
672
+ <Suspense fallback={<p>(loading posts)</p>}>
673
+ <ProfilePosts posts={<Posts>:posts:</Posts>} />
674
+ </Suspense>
675
+ <MyErrorBoundaryClient>
676
+ <Suspense fallback={<p>(loading games)</p>}>
677
+ <ProfileGames games={<Games>:games:</Games>} />
678
+ </Suspense>
679
+ </MyErrorBoundaryClient>
680
+ </>
681
+ );
682
+ }
683
+
684
+ const model = {
685
+ rootContent: <ProfileContent />,
686
+ };
687
+
688
+ function ProfilePage({response}) {
689
+ return use(response).rootContent;
690
+ }
691
+
692
+ const {writable, readable} = getTestStream();
693
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
694
+ model,
695
+ turbopackMap,
696
+ {
697
+ onError(x) {
698
+ reportedErrors.push(x);
699
+ return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
700
+ },
701
+ },
702
+ );
703
+ pipe(writable);
704
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
705
+
706
+ const container = document.createElement('div');
707
+ const root = ReactDOMClient.createRoot(container);
708
+ await act(() => {
709
+ root.render(
710
+ <Suspense fallback={<p>(loading)</p>}>
711
+ <ProfilePage response={response} />
712
+ </Suspense>,
713
+ );
714
+ });
715
+ expect(container.innerHTML).toBe('<p>(loading)</p>');
716
+
717
+ // This isn't enough to show anything.
718
+ await act(() => {
719
+ resolveFriends();
720
+ });
721
+ expect(container.innerHTML).toBe('<p>(loading)</p>');
722
+
723
+ // We can now show the details. Sidebar and posts are still loading.
724
+ await act(() => {
725
+ resolveName();
726
+ });
727
+ // Advance time enough to trigger a nested fallback.
728
+ await act(() => {
729
+ jest.advanceTimersByTime(500);
730
+ });
731
+ expect(container.innerHTML).toBe(
732
+ '<div>:name::avatar:</div>' +
733
+ '<p>(loading sidebar)</p>' +
734
+ '<p>(loading posts)</p>' +
735
+ '<p>(loading games)</p>',
736
+ );
737
+
738
+ expect(reportedErrors).toEqual([]);
739
+
740
+ const theError = new Error('Game over');
741
+ // Let's *fail* loading games.
742
+ await act(async () => {
743
+ await rejectGames(theError);
744
+ await 'the inner async function';
745
+ });
746
+ const expectedGamesValue = __DEV__
747
+ ? '<p>Game over + a dev digest</p>'
748
+ : '<p>digest("Game over")</p>';
749
+ expect(container.innerHTML).toBe(
750
+ '<div>:name::avatar:</div>' +
751
+ '<p>(loading sidebar)</p>' +
752
+ '<p>(loading posts)</p>' +
753
+ expectedGamesValue,
754
+ );
755
+
756
+ expect(reportedErrors).toEqual([theError]);
757
+ reportedErrors = [];
758
+
759
+ // We can now show the sidebar.
760
+ await act(async () => {
761
+ await resolvePhotos();
762
+ await 'the inner async function';
763
+ });
764
+ expect(container.innerHTML).toBe(
765
+ '<div>:name::avatar:</div>' +
766
+ '<div>:photos::friends:</div>' +
767
+ '<p>(loading posts)</p>' +
768
+ expectedGamesValue,
769
+ );
770
+
771
+ // Show everything.
772
+ await act(async () => {
773
+ await resolvePosts();
774
+ await 'the inner async function';
775
+ });
776
+ expect(container.innerHTML).toBe(
777
+ '<div>:name::avatar:</div>' +
778
+ '<div>:photos::friends:</div>' +
779
+ '<div>:posts:</div>' +
780
+ expectedGamesValue,
781
+ );
782
+
783
+ expect(reportedErrors).toEqual([]);
784
+ });
785
+
786
+ it('should preserve state of client components on refetch', async () => {
787
+ // Client
788
+
789
+ function Page({response}) {
790
+ return use(response);
791
+ }
792
+
793
+ function Input() {
794
+ return <input />;
795
+ }
796
+
797
+ const InputClient = clientExports(Input);
798
+
799
+ // Server
800
+
801
+ function App({color}) {
802
+ // Verify both DOM and Client children.
803
+ return (
804
+ <div style={{color}}>
805
+ <input />
806
+ <InputClient />
807
+ </div>
808
+ );
809
+ }
810
+
811
+ const container = document.createElement('div');
812
+ const root = ReactDOMClient.createRoot(container);
813
+
814
+ const stream1 = getTestStream();
815
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
816
+ <App color="red" />,
817
+ turbopackMap,
818
+ );
819
+ pipe(stream1.writable);
820
+ const response1 = ReactServerDOMClient.createFromReadableStream(
821
+ stream1.readable,
822
+ );
823
+ await act(() => {
824
+ root.render(
825
+ <Suspense fallback={<p>(loading)</p>}>
826
+ <Page response={response1} />
827
+ </Suspense>,
828
+ );
829
+ });
830
+ expect(container.children.length).toBe(1);
831
+ expect(container.children[0].tagName).toBe('DIV');
832
+ expect(container.children[0].style.color).toBe('red');
833
+
834
+ // Change the DOM state for both inputs.
835
+ const inputA = container.children[0].children[0];
836
+ expect(inputA.tagName).toBe('INPUT');
837
+ inputA.value = 'hello';
838
+ const inputB = container.children[0].children[1];
839
+ expect(inputB.tagName).toBe('INPUT');
840
+ inputB.value = 'goodbye';
841
+
842
+ const stream2 = getTestStream();
843
+ const {pipe: pipe2} = ReactServerDOMServer.renderToPipeableStream(
844
+ <App color="blue" />,
845
+ turbopackMap,
846
+ );
847
+ pipe2(stream2.writable);
848
+ const response2 = ReactServerDOMClient.createFromReadableStream(
849
+ stream2.readable,
850
+ );
851
+ await act(() => {
852
+ root.render(
853
+ <Suspense fallback={<p>(loading)</p>}>
854
+ <Page response={response2} />
855
+ </Suspense>,
856
+ );
857
+ });
858
+ expect(container.children.length).toBe(1);
859
+ expect(container.children[0].tagName).toBe('DIV');
860
+ expect(container.children[0].style.color).toBe('blue');
861
+
862
+ // Verify we didn't destroy the DOM for either input.
863
+ expect(inputA === container.children[0].children[0]).toBe(true);
864
+ expect(inputA.tagName).toBe('INPUT');
865
+ expect(inputA.value).toBe('hello');
866
+ expect(inputB === container.children[0].children[1]).toBe(true);
867
+ expect(inputB.tagName).toBe('INPUT');
868
+ expect(inputB.value).toBe('goodbye');
869
+ });
870
+
871
+ it('should be able to complete after aborting and throw the reason client-side', async () => {
872
+ const reportedErrors = [];
873
+
874
+ const {writable, readable} = getTestStream();
875
+ const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
876
+ <div>
877
+ <InfiniteSuspend />
878
+ </div>,
879
+ turbopackMap,
880
+ {
881
+ onError(x) {
882
+ reportedErrors.push(x);
883
+ const message = typeof x === 'string' ? x : x.message;
884
+ return __DEV__ ? 'a dev digest' : `digest("${message}")`;
885
+ },
886
+ },
887
+ );
888
+ pipe(writable);
889
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
890
+
891
+ const container = document.createElement('div');
892
+ const root = ReactDOMClient.createRoot(container);
893
+
894
+ function App({res}) {
895
+ return use(res);
896
+ }
897
+
898
+ await act(() => {
899
+ root.render(
900
+ <ErrorBoundary
901
+ fallback={e => (
902
+ <p>
903
+ {__DEV__ ? e.message + ' + ' : null}
904
+ {e.digest}
905
+ </p>
906
+ )}>
907
+ <Suspense fallback={<p>(loading)</p>}>
908
+ <App res={response} />
909
+ </Suspense>
910
+ </ErrorBoundary>,
911
+ );
912
+ });
913
+ expect(container.innerHTML).toBe('<p>(loading)</p>');
914
+
915
+ await act(() => {
916
+ abort('for reasons');
917
+ });
918
+ if (__DEV__) {
919
+ expect(container.innerHTML).toBe(
920
+ '<p>Error: for reasons + a dev digest</p>',
921
+ );
922
+ } else {
923
+ expect(container.innerHTML).toBe('<p>digest("for reasons")</p>');
924
+ }
925
+
926
+ expect(reportedErrors).toEqual(['for reasons']);
927
+ });
928
+
929
+ it('should be able to recover from a direct reference erroring client-side', async () => {
930
+ const reportedErrors = [];
931
+
932
+ const ClientComponent = clientExports(function ({prop}) {
933
+ return 'This should never render';
934
+ });
935
+
936
+ const ClientReference = clientModuleError(new Error('module init error'));
937
+
938
+ const {writable, readable} = getTestStream();
939
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
940
+ <div>
941
+ <ClientComponent prop={ClientReference} />
942
+ </div>,
943
+ turbopackMap,
944
+ {
945
+ onError(x) {
946
+ reportedErrors.push(x);
947
+ },
948
+ },
949
+ );
950
+ pipe(writable);
951
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
952
+
953
+ const container = document.createElement('div');
954
+ const root = ReactDOMClient.createRoot(container);
955
+
956
+ function App({res}) {
957
+ return use(res);
958
+ }
959
+
960
+ await act(() => {
961
+ root.render(
962
+ <ErrorBoundary fallback={e => <p>{e.message}</p>}>
963
+ <Suspense fallback={<p>(loading)</p>}>
964
+ <App res={response} />
965
+ </Suspense>
966
+ </ErrorBoundary>,
967
+ );
968
+ });
969
+ expect(container.innerHTML).toBe('<p>module init error</p>');
970
+
971
+ expect(reportedErrors).toEqual([]);
972
+ });
973
+
974
+ it('should be able to recover from a direct reference erroring client-side async', async () => {
975
+ const reportedErrors = [];
976
+
977
+ const ClientComponent = clientExports(function ({prop}) {
978
+ return 'This should never render';
979
+ });
980
+
981
+ let rejectPromise;
982
+ const ClientReference = await clientExports(
983
+ new Promise((resolve, reject) => {
984
+ rejectPromise = reject;
985
+ }),
986
+ );
987
+
988
+ const {writable, readable} = getTestStream();
989
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
990
+ <div>
991
+ <ClientComponent prop={ClientReference} />
992
+ </div>,
993
+ turbopackMap,
994
+ {
995
+ onError(x) {
996
+ reportedErrors.push(x);
997
+ },
998
+ },
999
+ );
1000
+ pipe(writable);
1001
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
1002
+
1003
+ const container = document.createElement('div');
1004
+ const root = ReactDOMClient.createRoot(container);
1005
+
1006
+ function App({res}) {
1007
+ return use(res);
1008
+ }
1009
+
1010
+ await act(() => {
1011
+ root.render(
1012
+ <ErrorBoundary fallback={e => <p>{e.message}</p>}>
1013
+ <Suspense fallback={<p>(loading)</p>}>
1014
+ <App res={response} />
1015
+ </Suspense>
1016
+ </ErrorBoundary>,
1017
+ );
1018
+ });
1019
+
1020
+ expect(container.innerHTML).toBe('<p>(loading)</p>');
1021
+
1022
+ await act(() => {
1023
+ rejectPromise(new Error('async module init error'));
1024
+ });
1025
+
1026
+ expect(container.innerHTML).toBe('<p>async module init error</p>');
1027
+
1028
+ expect(reportedErrors).toEqual([]);
1029
+ });
1030
+
1031
+ it('should be able to recover from a direct reference erroring server-side', async () => {
1032
+ const reportedErrors = [];
1033
+
1034
+ const ClientComponent = clientExports(function ({prop}) {
1035
+ return 'This should never render';
1036
+ });
1037
+
1038
+ // We simulate a bug in the Webpack bundler which causes an error on the server.
1039
+ for (const id in turbopackMap) {
1040
+ Object.defineProperty(turbopackMap, id, {
1041
+ get: () => {
1042
+ throw new Error('bug in the bundler');
1043
+ },
1044
+ });
1045
+ }
1046
+
1047
+ const {writable, readable} = getTestStream();
1048
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1049
+ <div>
1050
+ <ClientComponent />
1051
+ </div>,
1052
+ turbopackMap,
1053
+ {
1054
+ onError(x) {
1055
+ reportedErrors.push(x.message);
1056
+ return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
1057
+ },
1058
+ },
1059
+ );
1060
+ pipe(writable);
1061
+
1062
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
1063
+
1064
+ const container = document.createElement('div');
1065
+ const root = ReactDOMClient.createRoot(container);
1066
+
1067
+ function App({res}) {
1068
+ return use(res);
1069
+ }
1070
+
1071
+ await act(() => {
1072
+ root.render(
1073
+ <ErrorBoundary
1074
+ fallback={e => (
1075
+ <p>
1076
+ {__DEV__ ? e.message + ' + ' : null}
1077
+ {e.digest}
1078
+ </p>
1079
+ )}>
1080
+ <Suspense fallback={<p>(loading)</p>}>
1081
+ <App res={response} />
1082
+ </Suspense>
1083
+ </ErrorBoundary>,
1084
+ );
1085
+ });
1086
+ if (__DEV__) {
1087
+ expect(container.innerHTML).toBe(
1088
+ '<p>bug in the bundler + a dev digest</p>',
1089
+ );
1090
+ } else {
1091
+ expect(container.innerHTML).toBe('<p>digest("bug in the bundler")</p>');
1092
+ }
1093
+
1094
+ expect(reportedErrors).toEqual(['bug in the bundler']);
1095
+ });
1096
+
1097
+ it('should pass a Promise through props and be able use() it on the client', async () => {
1098
+ async function getData() {
1099
+ return 'async hello';
1100
+ }
1101
+
1102
+ function Component({data}) {
1103
+ const text = use(data);
1104
+ return <p>{text}</p>;
1105
+ }
1106
+
1107
+ const ClientComponent = clientExports(Component);
1108
+
1109
+ function ServerComponent() {
1110
+ const data = getData(); // no await here
1111
+ return <ClientComponent data={data} />;
1112
+ }
1113
+
1114
+ function Print({response}) {
1115
+ return use(response);
1116
+ }
1117
+
1118
+ function App({response}) {
1119
+ return (
1120
+ <Suspense fallback={<h1>Loading...</h1>}>
1121
+ <Print response={response} />
1122
+ </Suspense>
1123
+ );
1124
+ }
1125
+
1126
+ const {writable, readable} = getTestStream();
1127
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1128
+ <ServerComponent />,
1129
+ turbopackMap,
1130
+ );
1131
+ pipe(writable);
1132
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
1133
+
1134
+ const container = document.createElement('div');
1135
+ const root = ReactDOMClient.createRoot(container);
1136
+ await act(() => {
1137
+ root.render(<App response={response} />);
1138
+ });
1139
+ expect(container.innerHTML).toBe('<p>async hello</p>');
1140
+ });
1141
+
1142
+ it('should throw on the client if a passed promise eventually rejects', async () => {
1143
+ const reportedErrors = [];
1144
+ const theError = new Error('Server throw');
1145
+
1146
+ async function getData() {
1147
+ throw theError;
1148
+ }
1149
+
1150
+ function Component({data}) {
1151
+ const text = use(data);
1152
+ return <p>{text}</p>;
1153
+ }
1154
+
1155
+ const ClientComponent = clientExports(Component);
1156
+
1157
+ function ServerComponent() {
1158
+ const data = getData(); // no await here
1159
+ return <ClientComponent data={data} />;
1160
+ }
1161
+
1162
+ function Await({response}) {
1163
+ return use(response);
1164
+ }
1165
+
1166
+ function App({response}) {
1167
+ return (
1168
+ <Suspense fallback={<h1>Loading...</h1>}>
1169
+ <ErrorBoundary
1170
+ fallback={e => (
1171
+ <p>
1172
+ {__DEV__ ? e.message + ' + ' : null}
1173
+ {e.digest}
1174
+ </p>
1175
+ )}>
1176
+ <Await response={response} />
1177
+ </ErrorBoundary>
1178
+ </Suspense>
1179
+ );
1180
+ }
1181
+
1182
+ const {writable, readable} = getTestStream();
1183
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1184
+ <ServerComponent />,
1185
+ turbopackMap,
1186
+ {
1187
+ onError(x) {
1188
+ reportedErrors.push(x);
1189
+ return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
1190
+ },
1191
+ },
1192
+ );
1193
+ pipe(writable);
1194
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
1195
+
1196
+ const container = document.createElement('div');
1197
+ const root = ReactDOMClient.createRoot(container);
1198
+ await act(() => {
1199
+ root.render(<App response={response} />);
1200
+ });
1201
+ expect(container.innerHTML).toBe(
1202
+ __DEV__
1203
+ ? '<p>Server throw + a dev digest</p>'
1204
+ : '<p>digest("Server throw")</p>',
1205
+ );
1206
+ expect(reportedErrors).toEqual([theError]);
1207
+ });
1208
+
1209
+ it('should support float methods when rendering in Fiber', async () => {
1210
+ function Component() {
1211
+ return <p>hello world</p>;
1212
+ }
1213
+
1214
+ const ClientComponent = clientExports(Component);
1215
+
1216
+ async function ServerComponent() {
1217
+ FlightReactDOM.prefetchDNS('d before');
1218
+ FlightReactDOM.preconnect('c before');
1219
+ FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
1220
+ FlightReactDOM.preload('l before', {as: 'style'});
1221
+ FlightReactDOM.preloadModule('lm before');
1222
+ FlightReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'});
1223
+ FlightReactDOM.preinit('i before', {as: 'script'});
1224
+ FlightReactDOM.preinitModule('m before');
1225
+ FlightReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'});
1226
+ await 1;
1227
+ FlightReactDOM.prefetchDNS('d after');
1228
+ FlightReactDOM.preconnect('c after');
1229
+ FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
1230
+ FlightReactDOM.preload('l after', {as: 'style'});
1231
+ FlightReactDOM.preloadModule('lm after');
1232
+ FlightReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'});
1233
+ FlightReactDOM.preinit('i after', {as: 'script'});
1234
+ FlightReactDOM.preinitModule('m after');
1235
+ FlightReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'});
1236
+ return <ClientComponent />;
1237
+ }
1238
+
1239
+ const {writable, readable} = getTestStream();
1240
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1241
+ <ServerComponent />,
1242
+ turbopackMap,
1243
+ );
1244
+ pipe(writable);
1245
+
1246
+ let response = null;
1247
+ function getResponse() {
1248
+ if (response === null) {
1249
+ response = ReactServerDOMClient.createFromReadableStream(readable);
1250
+ }
1251
+ return response;
1252
+ }
1253
+
1254
+ function App() {
1255
+ return getResponse();
1256
+ }
1257
+
1258
+ // We pause to allow the float call after the await point to process before the
1259
+ // HostDispatcher gets set for Fiber by createRoot. This is only needed in testing
1260
+ // because the module graphs are not different and the HostDispatcher is shared.
1261
+ // In a real environment the Fiber and Flight code would each have their own independent
1262
+ // dispatcher.
1263
+ // @TODO consider what happens when Server-Components-On-The-Client exist. we probably
1264
+ // want to use the Fiber HostDispatcher there too since it is more about the host than the runtime
1265
+ // but we need to make sure that actually makes sense
1266
+ await 1;
1267
+
1268
+ const container = document.createElement('div');
1269
+ const root = ReactDOMClient.createRoot(container);
1270
+ await act(() => {
1271
+ root.render(<App />);
1272
+ });
1273
+
1274
+ expect(getMeaningfulChildren(document)).toEqual(
1275
+ <html>
1276
+ <head>
1277
+ <link rel="dns-prefetch" href="d before" />
1278
+ <link rel="preconnect" href="c before" />
1279
+ <link rel="preconnect" href="c2 before" crossorigin="" />
1280
+ <link rel="preload" as="style" href="l before" />
1281
+ <link rel="modulepreload" href="lm before" />
1282
+ <link rel="modulepreload" href="lm2 before" crossorigin="" />
1283
+ <script async="" src="i before" />
1284
+ <script type="module" async="" src="m before" />
1285
+ <script type="module" async="" src="m2 before" crossorigin="" />
1286
+ <link rel="dns-prefetch" href="d after" />
1287
+ <link rel="preconnect" href="c after" />
1288
+ <link rel="preconnect" href="c2 after" crossorigin="" />
1289
+ <link rel="preload" as="style" href="l after" />
1290
+ <link rel="modulepreload" href="lm after" />
1291
+ <link rel="modulepreload" href="lm2 after" crossorigin="" />
1292
+ <script async="" src="i after" />
1293
+ <script type="module" async="" src="m after" />
1294
+ <script type="module" async="" src="m2 after" crossorigin="" />
1295
+ </head>
1296
+ <body />
1297
+ </html>,
1298
+ );
1299
+ expect(getMeaningfulChildren(container)).toEqual(<p>hello world</p>);
1300
+ });
1301
+
1302
+ it('should support float methods when rendering in Fizz', async () => {
1303
+ function Component() {
1304
+ return <p>hello world</p>;
1305
+ }
1306
+
1307
+ const ClientComponent = clientExports(Component);
1308
+
1309
+ async function ServerComponent() {
1310
+ FlightReactDOM.prefetchDNS('d before');
1311
+ FlightReactDOM.preconnect('c before');
1312
+ FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
1313
+ FlightReactDOM.preload('l before', {as: 'style'});
1314
+ FlightReactDOM.preloadModule('lm before');
1315
+ FlightReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'});
1316
+ FlightReactDOM.preinit('i before', {as: 'script'});
1317
+ FlightReactDOM.preinitModule('m before');
1318
+ FlightReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'});
1319
+ await 1;
1320
+ FlightReactDOM.prefetchDNS('d after');
1321
+ FlightReactDOM.preconnect('c after');
1322
+ FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
1323
+ FlightReactDOM.preload('l after', {as: 'style'});
1324
+ FlightReactDOM.preloadModule('lm after');
1325
+ FlightReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'});
1326
+ FlightReactDOM.preinit('i after', {as: 'script'});
1327
+ FlightReactDOM.preinitModule('m after');
1328
+ FlightReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'});
1329
+ return <ClientComponent />;
1330
+ }
1331
+
1332
+ const {writable: flightWritable, readable: flightReadable} =
1333
+ getTestStream();
1334
+ const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
1335
+
1336
+ // In a real environment you would want to call the render during the Fizz render.
1337
+ // The reason we cannot do this in our test is because we don't actually have two separate
1338
+ // module graphs and we are contriving the sequencing to work in a way where
1339
+ // the right HostDispatcher is in scope during the Flight Server Float calls and the
1340
+ // Flight Client hint dispatches
1341
+ const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1342
+ <ServerComponent />,
1343
+ turbopackMap,
1344
+ );
1345
+ pipe(flightWritable);
1346
+
1347
+ let response = null;
1348
+ function getResponse() {
1349
+ if (response === null) {
1350
+ response =
1351
+ ReactServerDOMClient.createFromReadableStream(flightReadable);
1352
+ }
1353
+ return response;
1354
+ }
1355
+
1356
+ function App() {
1357
+ return (
1358
+ <html>
1359
+ <body>{getResponse()}</body>
1360
+ </html>
1361
+ );
1362
+ }
1363
+
1364
+ await act(async () => {
1365
+ ReactDOMFizzServer.renderToPipeableStream(<App />).pipe(fizzWritable);
1366
+ });
1367
+
1368
+ const decoder = new TextDecoder();
1369
+ const reader = fizzReadable.getReader();
1370
+ let content = '';
1371
+ while (true) {
1372
+ const {done, value} = await reader.read();
1373
+ if (done) {
1374
+ content += decoder.decode();
1375
+ break;
1376
+ }
1377
+ content += decoder.decode(value, {stream: true});
1378
+ }
1379
+
1380
+ const doc = new JSDOM(content).window.document;
1381
+ expect(getMeaningfulChildren(doc)).toEqual(
1382
+ <html>
1383
+ <head>
1384
+ <link rel="dns-prefetch" href="d before" />
1385
+ <link rel="preconnect" href="c before" />
1386
+ <link rel="preconnect" href="c2 before" crossorigin="" />
1387
+ <link rel="dns-prefetch" href="d after" />
1388
+ <link rel="preconnect" href="c after" />
1389
+ <link rel="preconnect" href="c2 after" crossorigin="" />
1390
+ <script async="" src="i before" />
1391
+ <script type="module" async="" src="m before" />
1392
+ <script type="module" async="" src="m2 before" crossorigin="" />
1393
+ <script async="" src="i after" />
1394
+ <script type="module" async="" src="m after" />
1395
+ <script type="module" async="" src="m2 after" crossorigin="" />
1396
+ <link rel="preload" as="style" href="l before" />
1397
+ <link rel="modulepreload" href="lm before" />
1398
+ <link rel="modulepreload" href="lm2 before" crossorigin="" />
1399
+ <link rel="preload" as="style" href="l after" />
1400
+ <link rel="modulepreload" href="lm after" />
1401
+ <link rel="modulepreload" href="lm2 after" crossorigin="" />
1402
+ </head>
1403
+ <body>
1404
+ <p>hello world</p>
1405
+ </body>
1406
+ </html>,
1407
+ );
1408
+ });
1409
+
1410
+ it('supports Float hints from concurrent Flight -> Fizz renders', async () => {
1411
+ function Component() {
1412
+ return <p>hello world</p>;
1413
+ }
1414
+
1415
+ const ClientComponent = clientExports(Component);
1416
+
1417
+ async function ServerComponent1() {
1418
+ FlightReactDOM.preload('before1', {as: 'style'});
1419
+ await 1;
1420
+ FlightReactDOM.preload('after1', {as: 'style'});
1421
+ return <ClientComponent />;
1422
+ }
1423
+
1424
+ async function ServerComponent2() {
1425
+ FlightReactDOM.preload('before2', {as: 'style'});
1426
+ await 1;
1427
+ FlightReactDOM.preload('after2', {as: 'style'});
1428
+ return <ClientComponent />;
1429
+ }
1430
+
1431
+ const {writable: flightWritable1, readable: flightReadable1} =
1432
+ getTestStream();
1433
+ const {writable: flightWritable2, readable: flightReadable2} =
1434
+ getTestStream();
1435
+
1436
+ ReactServerDOMServer.renderToPipeableStream(
1437
+ <ServerComponent1 />,
1438
+ turbopackMap,
1439
+ ).pipe(flightWritable1);
1440
+
1441
+ ReactServerDOMServer.renderToPipeableStream(
1442
+ <ServerComponent2 />,
1443
+ turbopackMap,
1444
+ ).pipe(flightWritable2);
1445
+
1446
+ const responses = new Map();
1447
+ function getResponse(stream) {
1448
+ let response = responses.get(stream);
1449
+ if (!response) {
1450
+ response = ReactServerDOMClient.createFromReadableStream(stream);
1451
+ responses.set(stream, response);
1452
+ }
1453
+ return response;
1454
+ }
1455
+
1456
+ function App({stream}) {
1457
+ return (
1458
+ <html>
1459
+ <body>{getResponse(stream)}</body>
1460
+ </html>
1461
+ );
1462
+ }
1463
+
1464
+ // pausing to let Flight runtime tick. This is a test only artifact of the fact that
1465
+ // we aren't operating separate module graphs for flight and fiber. In a real app
1466
+ // each would have their own dispatcher and there would be no cross dispatching.
1467
+ await 1;
1468
+
1469
+ const {writable: fizzWritable1, readable: fizzReadable1} = getTestStream();
1470
+ const {writable: fizzWritable2, readable: fizzReadable2} = getTestStream();
1471
+ await act(async () => {
1472
+ ReactDOMFizzServer.renderToPipeableStream(
1473
+ <App stream={flightReadable1} />,
1474
+ ).pipe(fizzWritable1);
1475
+ ReactDOMFizzServer.renderToPipeableStream(
1476
+ <App stream={flightReadable2} />,
1477
+ ).pipe(fizzWritable2);
1478
+ });
1479
+
1480
+ async function read(stream) {
1481
+ const decoder = new TextDecoder();
1482
+ const reader = stream.getReader();
1483
+ let buffer = '';
1484
+ while (true) {
1485
+ const {done, value} = await reader.read();
1486
+ if (done) {
1487
+ buffer += decoder.decode();
1488
+ break;
1489
+ }
1490
+ buffer += decoder.decode(value, {stream: true});
1491
+ }
1492
+ return buffer;
1493
+ }
1494
+
1495
+ const [content1, content2] = await Promise.all([
1496
+ read(fizzReadable1),
1497
+ read(fizzReadable2),
1498
+ ]);
1499
+
1500
+ expect(content1).toEqual(
1501
+ '<!DOCTYPE html><html><head><link rel="preload" href="before1" as="style"/>' +
1502
+ '<link rel="preload" href="after1" as="style"/></head><body><p>hello world</p></body></html>',
1503
+ );
1504
+ expect(content2).toEqual(
1505
+ '<!DOCTYPE html><html><head><link rel="preload" href="before2" as="style"/>' +
1506
+ '<link rel="preload" href="after2" as="style"/></head><body><p>hello world</p></body></html>',
1507
+ );
1508
+ });
1509
+
1510
+ it('supports deduping hints by Float key', async () => {
1511
+ function Component() {
1512
+ return <p>hello world</p>;
1513
+ }
1514
+
1515
+ const ClientComponent = clientExports(Component);
1516
+
1517
+ async function ServerComponent() {
1518
+ FlightReactDOM.prefetchDNS('dns');
1519
+ FlightReactDOM.preconnect('preconnect');
1520
+ FlightReactDOM.preload('load', {as: 'style'});
1521
+ FlightReactDOM.preinit('init', {as: 'script'});
1522
+ // again but vary preconnect to demonstrate crossOrigin participates in the key
1523
+ FlightReactDOM.prefetchDNS('dns');
1524
+ FlightReactDOM.preconnect('preconnect', {crossOrigin: 'anonymous'});
1525
+ FlightReactDOM.preload('load', {as: 'style'});
1526
+ FlightReactDOM.preinit('init', {as: 'script'});
1527
+ await 1;
1528
+ // after an async point
1529
+ FlightReactDOM.prefetchDNS('dns');
1530
+ FlightReactDOM.preconnect('preconnect', {crossOrigin: 'use-credentials'});
1531
+ FlightReactDOM.preload('load', {as: 'style'});
1532
+ FlightReactDOM.preinit('init', {as: 'script'});
1533
+ return <ClientComponent />;
1534
+ }
1535
+
1536
+ const {writable, readable} = getTestStream();
1537
+
1538
+ ReactServerDOMServer.renderToPipeableStream(
1539
+ <ServerComponent />,
1540
+ turbopackMap,
1541
+ ).pipe(writable);
1542
+
1543
+ const hintRows = [];
1544
+ async function collectHints(stream) {
1545
+ const decoder = new TextDecoder();
1546
+ const reader = stream.getReader();
1547
+ let buffer = '';
1548
+ while (true) {
1549
+ const {done, value} = await reader.read();
1550
+ if (done) {
1551
+ buffer += decoder.decode();
1552
+ if (buffer.includes(':H')) {
1553
+ hintRows.push(buffer);
1554
+ }
1555
+ break;
1556
+ }
1557
+ buffer += decoder.decode(value, {stream: true});
1558
+ let line;
1559
+ while ((line = buffer.indexOf('\n')) > -1) {
1560
+ const row = buffer.slice(0, line);
1561
+ buffer = buffer.slice(line + 1);
1562
+ if (row.includes(':H')) {
1563
+ hintRows.push(row);
1564
+ }
1565
+ }
1566
+ }
1567
+ }
1568
+
1569
+ await collectHints(readable);
1570
+ expect(hintRows.length).toEqual(6);
1571
+ });
1572
+});
packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMBrowser-test.js
new
+1228
@@ -0,0 +1,1228 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+// Polyfills for test environment
13
+global.ReadableStream =
14
+ require('web-streams-polyfill/ponyfill/es6').ReadableStream;
15
+global.TextEncoder = require('util').TextEncoder;
16
+global.TextDecoder = require('util').TextDecoder;
17
+
18
+let clientExports;
19
+let serverExports;
20
+let turbopackMap;
21
+let turbopackServerMap;
22
+let act;
23
+let React;
24
+let ReactDOM;
25
+let ReactDOMClient;
26
+let ReactDOMFizzServer;
27
+let ReactServerDOMServer;
28
+let ReactServerDOMClient;
29
+let Suspense;
30
+let use;
31
+
32
+describe('ReactFlightDOMBrowser', () => {
33
+ beforeEach(() => {
34
+ jest.resetModules();
35
+
36
+ // Simulate the condition resolution
37
+ jest.mock('react-server-dom-turbopack/server', () =>
38
+ require('react-server-dom-turbopack/server.browser'),
39
+ );
40
+
41
+ act = require('internal-test-utils').act;
42
+ const TurbopackMock = require('./utils/TurbopackMock');
43
+ clientExports = TurbopackMock.clientExports;
44
+ serverExports = TurbopackMock.serverExports;
45
+ turbopackMap = TurbopackMock.turbopackMap;
46
+ turbopackServerMap = TurbopackMock.turbopackServerMap;
47
+ React = require('react');
48
+ ReactDOM = require('react-dom');
49
+ ReactDOMClient = require('react-dom/client');
50
+ ReactDOMFizzServer = require('react-dom/server.browser');
51
+ ReactServerDOMServer = require('react-server-dom-turbopack/server.browser');
52
+ ReactServerDOMClient = require('react-server-dom-turbopack/client');
53
+ Suspense = React.Suspense;
54
+ use = React.use;
55
+ });
56
+
57
+ function makeDelayedText(Model) {
58
+ let error, _resolve, _reject;
59
+ let promise = new Promise((resolve, reject) => {
60
+ _resolve = () => {
61
+ promise = null;
62
+ resolve();
63
+ };
64
+ _reject = e => {
65
+ error = e;
66
+ promise = null;
67
+ reject(e);
68
+ };
69
+ });
70
+ function DelayedText({children}, data) {
71
+ if (promise) {
72
+ throw promise;
73
+ }
74
+ if (error) {
75
+ throw error;
76
+ }
77
+ return <Model>{children}</Model>;
78
+ }
79
+ return [DelayedText, _resolve, _reject];
80
+ }
81
+
82
+ const theInfinitePromise = new Promise(() => {});
83
+ function InfiniteSuspend() {
84
+ throw theInfinitePromise;
85
+ }
86
+
87
+ function requireServerRef(ref) {
88
+ let name = '';
89
+ let resolvedModuleData = turbopackServerMap[ref];
90
+ if (resolvedModuleData) {
91
+ // The potentially aliased name.
92
+ name = resolvedModuleData.name;
93
+ } else {
94
+ // We didn't find this specific export name but we might have the * export
95
+ // which contains this name as well.
96
+ // TODO: It's unfortunate that we now have to parse this string. We should
97
+ // probably go back to encoding path and name separately on the client reference.
98
+ const idx = ref.lastIndexOf('#');
99
+ if (idx !== -1) {
100
+ name = ref.slice(idx + 1);
101
+ resolvedModuleData = turbopackServerMap[ref.slice(0, idx)];
102
+ }
103
+ if (!resolvedModuleData) {
104
+ throw new Error(
105
+ 'Could not find the module "' +
106
+ ref +
107
+ '" in the React Client Manifest. ' +
108
+ 'This is probably a bug in the React Server Components bundler.',
109
+ );
110
+ }
111
+ }
112
+ const mod = __turbopack_require__(resolvedModuleData.id);
113
+ if (name === '*') {
114
+ return mod;
115
+ }
116
+ return mod[name];
117
+ }
118
+
119
+ async function callServer(actionId, body) {
120
+ const fn = requireServerRef(actionId);
121
+ const args = await ReactServerDOMServer.decodeReply(
122
+ body,
123
+ turbopackServerMap,
124
+ );
125
+ return fn.apply(null, args);
126
+ }
127
+
128
+ it('should resolve HTML using W3C streams', async () => {
129
+ function Text({children}) {
130
+ return <span>{children}</span>;
131
+ }
132
+ function HTML() {
133
+ return (
134
+ <div>
135
+ <Text>hello</Text>
136
+ <Text>world</Text>
137
+ </div>
138
+ );
139
+ }
140
+
141
+ function App() {
142
+ const model = {
143
+ html: <HTML />,
144
+ };
145
+ return model;
146
+ }
147
+
148
+ const stream = ReactServerDOMServer.renderToReadableStream(<App />);
149
+ const response = ReactServerDOMClient.createFromReadableStream(stream);
150
+ const model = await response;
151
+ expect(model).toEqual({
152
+ html: (
153
+ <div>
154
+ <span>hello</span>
155
+ <span>world</span>
156
+ </div>
157
+ ),
158
+ });
159
+ });
160
+
161
+ it('should resolve HTML using W3C streams', async () => {
162
+ function Text({children}) {
163
+ return <span>{children}</span>;
164
+ }
165
+ function HTML() {
166
+ return (
167
+ <div>
168
+ <Text>hello</Text>
169
+ <Text>world</Text>
170
+ </div>
171
+ );
172
+ }
173
+
174
+ function App() {
175
+ const model = {
176
+ html: <HTML />,
177
+ };
178
+ return model;
179
+ }
180
+
181
+ const stream = ReactServerDOMServer.renderToReadableStream(<App />);
182
+ const response = ReactServerDOMClient.createFromReadableStream(stream);
183
+ const model = await response;
184
+ expect(model).toEqual({
185
+ html: (
186
+ <div>
187
+ <span>hello</span>
188
+ <span>world</span>
189
+ </div>
190
+ ),
191
+ });
192
+ });
193
+
194
+ it('should progressively reveal server components', async () => {
195
+ let reportedErrors = [];
196
+
197
+ // Client Components
198
+
199
+ class ErrorBoundary extends React.Component {
200
+ state = {hasError: false, error: null};
201
+ static getDerivedStateFromError(error) {
202
+ return {
203
+ hasError: true,
204
+ error,
205
+ };
206
+ }
207
+ render() {
208
+ if (this.state.hasError) {
209
+ return this.props.fallback(this.state.error);
210
+ }
211
+ return this.props.children;
212
+ }
213
+ }
214
+
215
+ let errorBoundaryFn;
216
+ if (__DEV__) {
217
+ errorBoundaryFn = e => (
218
+ <p>
219
+ {e.message} + {e.digest}
220
+ </p>
221
+ );
222
+ } else {
223
+ errorBoundaryFn = e => {
224
+ expect(e.message).toBe(
225
+ 'An error occurred in the Server Components render. The specific message is omitted in production' +
226
+ ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
227
+ ' may provide additional details about the nature of the error.',
228
+ );
229
+ return <p>{e.digest}</p>;
230
+ };
231
+ }
232
+
233
+ function MyErrorBoundary({children}) {
234
+ return (
235
+ <ErrorBoundary fallback={errorBoundaryFn}>{children}</ErrorBoundary>
236
+ );
237
+ }
238
+
239
+ // Model
240
+ function Text({children}) {
241
+ return children;
242
+ }
243
+
244
+ const [Friends, resolveFriends] = makeDelayedText(Text);
245
+ const [Name, resolveName] = makeDelayedText(Text);
246
+ const [Posts, resolvePosts] = makeDelayedText(Text);
247
+ const [Photos, resolvePhotos] = makeDelayedText(Text);
248
+ const [Games, , rejectGames] = makeDelayedText(Text);
249
+
250
+ // View
251
+ function ProfileDetails({avatar}) {
252
+ return (
253
+ <div>
254
+ <Name>:name:</Name>
255
+ {avatar}
256
+ </div>
257
+ );
258
+ }
259
+ function ProfileSidebar({friends}) {
260
+ return (
261
+ <div>
262
+ <Photos>:photos:</Photos>
263
+ {friends}
264
+ </div>
265
+ );
266
+ }
267
+ function ProfilePosts({posts}) {
268
+ return <div>{posts}</div>;
269
+ }
270
+ function ProfileGames({games}) {
271
+ return <div>{games}</div>;
272
+ }
273
+
274
+ const MyErrorBoundaryClient = clientExports(MyErrorBoundary);
275
+
276
+ function ProfileContent() {
277
+ return (
278
+ <>
279
+ <ProfileDetails avatar={<Text>:avatar:</Text>} />
280
+ <Suspense fallback={<p>(loading sidebar)</p>}>
281
+ <ProfileSidebar friends={<Friends>:friends:</Friends>} />
282
+ </Suspense>
283
+ <Suspense fallback={<p>(loading posts)</p>}>
284
+ <ProfilePosts posts={<Posts>:posts:</Posts>} />
285
+ </Suspense>
286
+ <MyErrorBoundaryClient>
287
+ <Suspense fallback={<p>(loading games)</p>}>
288
+ <ProfileGames games={<Games>:games:</Games>} />
289
+ </Suspense>
290
+ </MyErrorBoundaryClient>
291
+ </>
292
+ );
293
+ }
294
+
295
+ const model = {
296
+ rootContent: <ProfileContent />,
297
+ };
298
+
299
+ function ProfilePage({response}) {
300
+ return use(response).rootContent;
301
+ }
302
+
303
+ const stream = ReactServerDOMServer.renderToReadableStream(
304
+ model,
305
+ turbopackMap,
306
+ {
307
+ onError(x) {
308
+ reportedErrors.push(x);
309
+ return __DEV__ ? `a dev digest` : `digest("${x.message}")`;
310
+ },
311
+ },
312
+ );
313
+ const response = ReactServerDOMClient.createFromReadableStream(stream);
314
+
315
+ const container = document.createElement('div');
316
+ const root = ReactDOMClient.createRoot(container);
317
+ await act(() => {
318
+ root.render(
319
+ <Suspense fallback={<p>(loading)</p>}>
320
+ <ProfilePage response={response} />
321
+ </Suspense>,
322
+ );
323
+ });
324
+ expect(container.innerHTML).toBe('<p>(loading)</p>');
325
+
326
+ // This isn't enough to show anything.
327
+ await act(() => {
328
+ resolveFriends();
329
+ });
330
+ expect(container.innerHTML).toBe('<p>(loading)</p>');
331
+
332
+ // We can now show the details. Sidebar and posts are still loading.
333
+ await act(() => {
334
+ resolveName();
335
+ });
336
+ // Advance time enough to trigger a nested fallback.
337
+ jest.advanceTimersByTime(500);
338
+ expect(container.innerHTML).toBe(
339
+ '<div>:name::avatar:</div>' +
340
+ '<p>(loading sidebar)</p>' +
341
+ '<p>(loading posts)</p>' +
342
+ '<p>(loading games)</p>',
343
+ );
344
+
345
+ expect(reportedErrors).toEqual([]);
346
+
347
+ const theError = new Error('Game over');
348
+ // Let's *fail* loading games.
349
+ await act(() => {
350
+ rejectGames(theError);
351
+ });
352
+
353
+ const gamesExpectedValue = __DEV__
354
+ ? '<p>Game over + a dev digest</p>'
355
+ : '<p>digest("Game over")</p>';
356
+
357
+ expect(container.innerHTML).toBe(
358
+ '<div>:name::avatar:</div>' +
359
+ '<p>(loading sidebar)</p>' +
360
+ '<p>(loading posts)</p>' +
361
+ gamesExpectedValue,
362
+ );
363
+
364
+ expect(reportedErrors).toEqual([theError]);
365
+ reportedErrors = [];
366
+
367
+ // We can now show the sidebar.
368
+ await act(() => {
369
+ resolvePhotos();
370
+ });
371
+ expect(container.innerHTML).toBe(
372
+ '<div>:name::avatar:</div>' +
373
+ '<div>:photos::friends:</div>' +
374
+ '<p>(loading posts)</p>' +
375
+ gamesExpectedValue,
376
+ );
377
+
378
+ // Show everything.
379
+ await act(() => {
380
+ resolvePosts();
381
+ });
382
+ expect(container.innerHTML).toBe(
383
+ '<div>:name::avatar:</div>' +
384
+ '<div>:photos::friends:</div>' +
385
+ '<div>:posts:</div>' +
386
+ gamesExpectedValue,
387
+ );
388
+
389
+ expect(reportedErrors).toEqual([]);
390
+ });
391
+
392
+ it('should close the stream upon completion when rendering to W3C streams', async () => {
393
+ // Model
394
+ function Text({children}) {
395
+ return children;
396
+ }
397
+
398
+ const [Friends, resolveFriends] = makeDelayedText(Text);
399
+ const [Name, resolveName] = makeDelayedText(Text);
400
+ const [Posts, resolvePosts] = makeDelayedText(Text);
401
+ const [Photos, resolvePhotos] = makeDelayedText(Text);
402
+
403
+ // View
404
+ function ProfileDetails({avatar}) {
405
+ return (
406
+ <div>
407
+ <Name>:name:</Name>
408
+ {avatar}
409
+ </div>
410
+ );
411
+ }
412
+ function ProfileSidebar({friends}) {
413
+ return (
414
+ <div>
415
+ <Photos>:photos:</Photos>
416
+ {friends}
417
+ </div>
418
+ );
419
+ }
420
+ function ProfilePosts({posts}) {
421
+ return <div>{posts}</div>;
422
+ }
423
+
424
+ function ProfileContent() {
425
+ return (
426
+ <Suspense fallback="(loading everything)">
427
+ <ProfileDetails avatar={<Text>:avatar:</Text>} />
428
+ <Suspense fallback={<p>(loading sidebar)</p>}>
429
+ <ProfileSidebar friends={<Friends>:friends:</Friends>} />
430
+ </Suspense>
431
+ <Suspense fallback={<p>(loading posts)</p>}>
432
+ <ProfilePosts posts={<Posts>:posts:</Posts>} />
433
+ </Suspense>
434
+ </Suspense>
435
+ );
436
+ }
437
+
438
+ const model = {
439
+ rootContent: <ProfileContent />,
440
+ };
441
+
442
+ const stream = ReactServerDOMServer.renderToReadableStream(
443
+ model,
444
+ turbopackMap,
445
+ );
446
+
447
+ const reader = stream.getReader();
448
+ const decoder = new TextDecoder();
449
+
450
+ let flightResponse = '';
451
+ let isDone = false;
452
+
453
+ reader.read().then(function progress({done, value}) {
454
+ if (done) {
455
+ isDone = true;
456
+ return;
457
+ }
458
+
459
+ flightResponse += decoder.decode(value);
460
+
461
+ return reader.read().then(progress);
462
+ });
463
+
464
+ // Advance time enough to trigger a nested fallback.
465
+ jest.advanceTimersByTime(500);
466
+
467
+ await act(() => {});
468
+
469
+ expect(flightResponse).toContain('(loading everything)');
470
+ expect(flightResponse).toContain('(loading sidebar)');
471
+ expect(flightResponse).toContain('(loading posts)');
472
+ expect(flightResponse).not.toContain(':friends:');
473
+ expect(flightResponse).not.toContain(':name:');
474
+
475
+ await act(() => {
476
+ resolveFriends();
477
+ });
478
+
479
+ expect(flightResponse).toContain(':friends:');
480
+
481
+ await act(() => {
482
+ resolveName();
483
+ });
484
+
485
+ expect(flightResponse).toContain(':name:');
486
+
487
+ await act(() => {
488
+ resolvePhotos();
489
+ });
490
+
491
+ expect(flightResponse).toContain(':photos:');
492
+
493
+ await act(() => {
494
+ resolvePosts();
495
+ });
496
+
497
+ expect(flightResponse).toContain(':posts:');
498
+
499
+ // Final pending chunk is written; stream should be closed.
500
+ expect(isDone).toBeTruthy();
501
+ });
502
+
503
+ it('should be able to complete after aborting and throw the reason client-side', async () => {
504
+ const reportedErrors = [];
505
+
506
+ let errorBoundaryFn;
507
+ if (__DEV__) {
508
+ errorBoundaryFn = e => (
509
+ <p>
510
+ {e.message} + {e.digest}
511
+ </p>
512
+ );
513
+ } else {
514
+ errorBoundaryFn = e => {
515
+ expect(e.message).toBe(
516
+ 'An error occurred in the Server Components render. The specific message is omitted in production' +
517
+ ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
518
+ ' may provide additional details about the nature of the error.',
519
+ );
520
+ return <p>{e.digest}</p>;
521
+ };
522
+ }
523
+
524
+ class ErrorBoundary extends React.Component {
525
+ state = {hasError: false, error: null};
526
+ static getDerivedStateFromError(error) {
527
+ return {
528
+ hasError: true,
529
+ error,
530
+ };
531
+ }
532
+ render() {
533
+ if (this.state.hasError) {
534
+ return this.props.fallback(this.state.error);
535
+ }
536
+ return this.props.children;
537
+ }
538
+ }
539
+
540
+ const controller = new AbortController();
541
+ const stream = ReactServerDOMServer.renderToReadableStream(
542
+ <div>
543
+ <InfiniteSuspend />
544
+ </div>,
545
+ turbopackMap,
546
+ {
547
+ signal: controller.signal,
548
+ onError(x) {
549
+ const message = typeof x === 'string' ? x : x.message;
550
+ reportedErrors.push(x);
551
+ return __DEV__ ? 'a dev digest' : `digest("${message}")`;
552
+ },
553
+ },
554
+ );
555
+ const response = ReactServerDOMClient.createFromReadableStream(stream);
556
+
557
+ const container = document.createElement('div');
558
+ const root = ReactDOMClient.createRoot(container);
559
+
560
+ function App({res}) {
561
+ return use(res);
562
+ }
563
+
564
+ await act(() => {
565
+ root.render(
566
+ <ErrorBoundary fallback={errorBoundaryFn}>
567
+ <Suspense fallback={<p>(loading)</p>}>
568
+ <App res={response} />
569
+ </Suspense>
570
+ </ErrorBoundary>,
571
+ );
572
+ });
573
+ expect(container.innerHTML).toBe('<p>(loading)</p>');
574
+
575
+ await act(() => {
576
+ controller.abort('for reasons');
577
+ });
578
+ const expectedValue = __DEV__
579
+ ? '<p>Error: for reasons + a dev digest</p>'
580
+ : '<p>digest("for reasons")</p>';
581
+ expect(container.innerHTML).toBe(expectedValue);
582
+
583
+ expect(reportedErrors).toEqual(['for reasons']);
584
+ });
585
+
586
+ it('basic use(promise)', async () => {
587
+ function Server() {
588
+ return (
589
+ use(Promise.resolve('A')) +
590
+ use(Promise.resolve('B')) +
591
+ use(Promise.resolve('C'))
592
+ );
593
+ }
594
+
595
+ const stream = ReactServerDOMServer.renderToReadableStream(<Server />);
596
+ const response = ReactServerDOMClient.createFromReadableStream(stream);
597
+
598
+ function Client() {
599
+ return use(response);
600
+ }
601
+
602
+ const container = document.createElement('div');
603
+ const root = ReactDOMClient.createRoot(container);
604
+ await act(() => {
605
+ root.render(
606
+ <Suspense fallback="Loading...">
607
+ <Client />
608
+ </Suspense>,
609
+ );
610
+ });
611
+ expect(container.innerHTML).toBe('ABC');
612
+ });
613
+
614
+ it('basic use(context)', async () => {
615
+ const ContextA = React.createServerContext('ContextA', '');
616
+ const ContextB = React.createServerContext('ContextB', 'B');
617
+
618
+ function ServerComponent() {
619
+ return use(ContextA) + use(ContextB);
620
+ }
621
+ function Server() {
622
+ return (
623
+ <ContextA.Provider value="A">
624
+ <ServerComponent />
625
+ </ContextA.Provider>
626
+ );
627
+ }
628
+ const stream = ReactServerDOMServer.renderToReadableStream(<Server />);
629
+ const response = ReactServerDOMClient.createFromReadableStream(stream);
630
+
631
+ function Client() {
632
+ return use(response);
633
+ }
634
+
635
+ const container = document.createElement('div');
636
+ const root = ReactDOMClient.createRoot(container);
637
+ await act(() => {
638
+ // Client uses a different renderer.
639
+ // We reset _currentRenderer here to not trigger a warning about multiple
640
+ // renderers concurrently using this context
641
+ ContextA._currentRenderer = null;
642
+ root.render(<Client />);
643
+ });
644
+ expect(container.innerHTML).toBe('AB');
645
+ });
646
+
647
+ it('use(promise) in multiple components', async () => {
648
+ function Child({prefix}) {
649
+ return prefix + use(Promise.resolve('C')) + use(Promise.resolve('D'));
650
+ }
651
+
652
+ function Parent() {
653
+ return (
654
+ <Child prefix={use(Promise.resolve('A')) + use(Promise.resolve('B'))} />
655
+ );
656
+ }
657
+
658
+ const stream = ReactServerDOMServer.renderToReadableStream(<Parent />);
659
+ const response = ReactServerDOMClient.createFromReadableStream(stream);
660
+
661
+ function Client() {
662
+ return use(response);
663
+ }
664
+
665
+ const container = document.createElement('div');
666
+ const root = ReactDOMClient.createRoot(container);
667
+ await act(() => {
668
+ root.render(
669
+ <Suspense fallback="Loading...">
670
+ <Client />
671
+ </Suspense>,
672
+ );
673
+ });
674
+ expect(container.innerHTML).toBe('ABCD');
675
+ });
676
+
677
+ it('using a rejected promise will throw', async () => {
678
+ const promiseA = Promise.resolve('A');
679
+ const promiseB = Promise.reject(new Error('Oops!'));
680
+ const promiseC = Promise.resolve('C');
681
+
682
+ // Jest/Node will raise an unhandled rejected error unless we await this. It
683
+ // works fine in the browser, though.
684
+ await expect(promiseB).rejects.toThrow('Oops!');
685
+
686
+ function Server() {
687
+ return use(promiseA) + use(promiseB) + use(promiseC);
688
+ }
689
+
690
+ const reportedErrors = [];
691
+ const stream = ReactServerDOMServer.renderToReadableStream(
692
+ <Server />,
693
+ turbopackMap,
694
+ {
695
+ onError(x) {
696
+ reportedErrors.push(x);
697
+ return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
698
+ },
699
+ },
700
+ );
701
+ const response = ReactServerDOMClient.createFromReadableStream(stream);
702
+
703
+ class ErrorBoundary extends React.Component {
704
+ state = {error: null};
705
+ static getDerivedStateFromError(error) {
706
+ return {error};
707
+ }
708
+ render() {
709
+ if (this.state.error) {
710
+ return __DEV__
711
+ ? this.state.error.message + ' + ' + this.state.error.digest
712
+ : this.state.error.digest;
713
+ }
714
+ return this.props.children;
715
+ }
716
+ }
717
+
718
+ function Client() {
719
+ return use(response);
720
+ }
721
+
722
+ const container = document.createElement('div');
723
+ const root = ReactDOMClient.createRoot(container);
724
+ await act(() => {
725
+ root.render(
726
+ <ErrorBoundary>
727
+ <Client />
728
+ </ErrorBoundary>,
729
+ );
730
+ });
731
+ expect(container.innerHTML).toBe(
732
+ __DEV__ ? 'Oops! + a dev digest' : 'digest("Oops!")',
733
+ );
734
+ expect(reportedErrors.length).toBe(1);
735
+ expect(reportedErrors[0].message).toBe('Oops!');
736
+ });
737
+
738
+ it("use a promise that's already been instrumented and resolved", async () => {
739
+ const thenable = {
740
+ status: 'fulfilled',
741
+ value: 'Hi',
742
+ then() {},
743
+ };
744
+
745
+ // This will never suspend because the thenable already resolved
746
+ function Server() {
747
+ return use(thenable);
748
+ }
749
+
750
+ const stream = ReactServerDOMServer.renderToReadableStream(<Server />);
751
+ const response = ReactServerDOMClient.createFromReadableStream(stream);
752
+
753
+ function Client() {
754
+ return use(response);
755
+ }
756
+
757
+ const container = document.createElement('div');
758
+ const root = ReactDOMClient.createRoot(container);
759
+ await act(() => {
760
+ root.render(<Client />);
761
+ });
762
+ expect(container.innerHTML).toBe('Hi');
763
+ });
764
+
765
+ it('unwraps thenable that fulfills synchronously without suspending', async () => {
766
+ function Server() {
767
+ const thenable = {
768
+ then(resolve) {
769
+ // This thenable immediately resolves, synchronously, without waiting
770
+ // a microtask.
771
+ resolve('Hi');
772
+ },
773
+ };
774
+ try {
775
+ return use(thenable);
776
+ } catch {
777
+ throw new Error(
778
+ '`use` should not suspend because the thenable resolved synchronously.',
779
+ );
780
+ }
781
+ }
782
+
783
+ // Because the thenable resolves synchronously, we should be able to finish
784
+ // rendering synchronously, with no fallback.
785
+ const stream = ReactServerDOMServer.renderToReadableStream(<Server />);
786
+ const response = ReactServerDOMClient.createFromReadableStream(stream);
787
+
788
+ function Client() {
789
+ return use(response);
790
+ }
791
+
792
+ const container = document.createElement('div');
793
+ const root = ReactDOMClient.createRoot(container);
794
+ await act(() => {
795
+ root.render(<Client />);
796
+ });
797
+ expect(container.innerHTML).toBe('Hi');
798
+ });
799
+
800
+ it('can pass a higher order function by reference from server to client', async () => {
801
+ let actionProxy;
802
+
803
+ function Client({action}) {
804
+ actionProxy = action;
805
+ return 'Click Me';
806
+ }
807
+
808
+ function greet(transform, text) {
809
+ return 'Hello ' + transform(text);
810
+ }
811
+
812
+ function upper(text) {
813
+ return text.toUpperCase();
814
+ }
815
+
816
+ const ServerModuleA = serverExports({
817
+ greet,
818
+ });
819
+ const ServerModuleB = serverExports({
820
+ upper,
821
+ });
822
+ const ClientRef = clientExports(Client);
823
+
824
+ const boundFn = ServerModuleA.greet.bind(null, ServerModuleB.upper);
825
+
826
+ const stream = ReactServerDOMServer.renderToReadableStream(
827
+ <ClientRef action={boundFn} />,
828
+ turbopackMap,
829
+ );
830
+
831
+ const response = ReactServerDOMClient.createFromReadableStream(stream, {
832
+ async callServer(ref, args) {
833
+ const body = await ReactServerDOMClient.encodeReply(args);
834
+ return callServer(ref, body);
835
+ },
836
+ });
837
+
838
+ function App() {
839
+ return use(response);
840
+ }
841
+
842
+ const container = document.createElement('div');
843
+ const root = ReactDOMClient.createRoot(container);
844
+ await act(() => {
845
+ root.render(<App />);
846
+ });
847
+ expect(container.innerHTML).toBe('Click Me');
848
+ expect(typeof actionProxy).toBe('function');
849
+ expect(actionProxy).not.toBe(boundFn);
850
+
851
+ const result = await actionProxy('hi');
852
+ expect(result).toBe('Hello HI');
853
+ });
854
+
855
+ it('can call a module split server function', async () => {
856
+ let actionProxy;
857
+
858
+ function Client({action}) {
859
+ actionProxy = action;
860
+ return 'Click Me';
861
+ }
862
+
863
+ function greet(text) {
864
+ return 'Hello ' + text;
865
+ }
866
+
867
+ const ServerModule = serverExports({
868
+ // This gets split into another module
869
+ split: greet,
870
+ });
871
+ const ClientRef = clientExports(Client);
872
+
873
+ const stream = ReactServerDOMServer.renderToReadableStream(
874
+ <ClientRef action={ServerModule.split} />,
875
+ turbopackMap,
876
+ );
877
+
878
+ const response = ReactServerDOMClient.createFromReadableStream(stream, {
879
+ async callServer(ref, args) {
880
+ const body = await ReactServerDOMClient.encodeReply(args);
881
+ return callServer(ref, body);
882
+ },
883
+ });
884
+
885
+ function App() {
886
+ return use(response);
887
+ }
888
+
889
+ const container = document.createElement('div');
890
+ const root = ReactDOMClient.createRoot(container);
891
+ await act(() => {
892
+ root.render(<App />);
893
+ });
894
+ expect(container.innerHTML).toBe('Click Me');
895
+ expect(typeof actionProxy).toBe('function');
896
+
897
+ const result = await actionProxy('Split');
898
+ expect(result).toBe('Hello Split');
899
+ });
900
+
901
+ it('can pass a server function by importing from client back to server', async () => {
902
+ function greet(transform, text) {
903
+ return 'Hello ' + transform(text);
904
+ }
905
+
906
+ function upper(text) {
907
+ return text.toUpperCase();
908
+ }
909
+
910
+ const ServerModuleA = serverExports({
911
+ greet,
912
+ });
913
+ const ServerModuleB = serverExports({
914
+ upper,
915
+ });
916
+
917
+ let actionProxy;
918
+
919
+ // This is a Proxy representing ServerModuleB in the Client bundle.
920
+ const ServerModuleBImportedOnClient = {
921
+ upper: ReactServerDOMClient.createServerReference(
922
+ ServerModuleB.upper.$$id,
923
+ async function (ref, args) {
924
+ const body = await ReactServerDOMClient.encodeReply(args);
925
+ return callServer(ref, body);
926
+ },
927
+ ),
928
+ };
929
+
930
+ function Client({action}) {
931
+ // Client side pass a Server Reference into an action.
932
+ actionProxy = text => action(ServerModuleBImportedOnClient.upper, text);
933
+ return 'Click Me';
934
+ }
935
+
936
+ const ClientRef = clientExports(Client);
937
+
938
+ const stream = ReactServerDOMServer.renderToReadableStream(
939
+ <ClientRef action={ServerModuleA.greet} />,
940
+ turbopackMap,
941
+ );
942
+
943
+ const response = ReactServerDOMClient.createFromReadableStream(stream, {
944
+ async callServer(ref, args) {
945
+ const body = await ReactServerDOMClient.encodeReply(args);
946
+ return callServer(ref, body);
947
+ },
948
+ });
949
+
950
+ function App() {
951
+ return use(response);
952
+ }
953
+
954
+ const container = document.createElement('div');
955
+ const root = ReactDOMClient.createRoot(container);
956
+ await act(() => {
957
+ root.render(<App />);
958
+ });
959
+ expect(container.innerHTML).toBe('Click Me');
960
+
961
+ const result = await actionProxy('hi');
962
+ expect(result).toBe('Hello HI');
963
+ });
964
+
965
+ it('can bind arguments to a server reference', async () => {
966
+ let actionProxy;
967
+
968
+ function Client({action}) {
969
+ actionProxy = action;
970
+ return 'Click Me';
971
+ }
972
+
973
+ const greet = serverExports(function greet(a, b, c) {
974
+ return a + ' ' + b + c;
975
+ });
976
+ const ClientRef = clientExports(Client);
977
+
978
+ const stream = ReactServerDOMServer.renderToReadableStream(
979
+ <ClientRef action={greet.bind(null, 'Hello', 'World')} />,
980
+ turbopackMap,
981
+ );
982
+
983
+ const response = ReactServerDOMClient.createFromReadableStream(stream, {
984
+ async callServer(actionId, args) {
985
+ const body = await ReactServerDOMClient.encodeReply(args);
986
+ return callServer(actionId, body);
987
+ },
988
+ });
989
+
990
+ function App() {
991
+ return use(response);
992
+ }
993
+
994
+ const container = document.createElement('div');
995
+ const root = ReactDOMClient.createRoot(container);
996
+ await act(() => {
997
+ root.render(<App />);
998
+ });
999
+ expect(container.innerHTML).toBe('Click Me');
1000
+ expect(typeof actionProxy).toBe('function');
1001
+ expect(actionProxy).not.toBe(greet);
1002
+
1003
+ const result = await actionProxy('!');
1004
+ expect(result).toBe('Hello World!');
1005
+ });
1006
+
1007
+ it('propagates server reference errors to the client', async () => {
1008
+ let actionProxy;
1009
+
1010
+ function Client({action}) {
1011
+ actionProxy = action;
1012
+ return 'Click Me';
1013
+ }
1014
+
1015
+ async function send(text) {
1016
+ return Promise.reject(new Error(`Error for ${text}`));
1017
+ }
1018
+
1019
+ const ServerModule = serverExports({send});
1020
+ const ClientRef = clientExports(Client);
1021
+
1022
+ const stream = ReactServerDOMServer.renderToReadableStream(
1023
+ <ClientRef action={ServerModule.send} />,
1024
+ turbopackMap,
1025
+ );
1026
+
1027
+ const response = ReactServerDOMClient.createFromReadableStream(stream, {
1028
+ async callServer(actionId, args) {
1029
+ const body = await ReactServerDOMClient.encodeReply(args);
1030
+ return ReactServerDOMClient.createFromReadableStream(
1031
+ ReactServerDOMServer.renderToReadableStream(
1032
+ callServer(actionId, body),
1033
+ null,
1034
+ {onError: error => 'test-error-digest'},
1035
+ ),
1036
+ );
1037
+ },
1038
+ });
1039
+
1040
+ function App() {
1041
+ return use(response);
1042
+ }
1043
+
1044
+ const container = document.createElement('div');
1045
+ const root = ReactDOMClient.createRoot(container);
1046
+ await act(() => {
1047
+ root.render(<App />);
1048
+ });
1049
+
1050
+ if (__DEV__) {
1051
+ await expect(actionProxy('test')).rejects.toThrow('Error for test');
1052
+ } else {
1053
+ let thrownError;
1054
+
1055
+ try {
1056
+ await actionProxy('test');
1057
+ } catch (error) {
1058
+ thrownError = error;
1059
+ }
1060
+
1061
+ expect(thrownError).toEqual(
1062
+ new Error(
1063
+ 'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.',
1064
+ ),
1065
+ );
1066
+
1067
+ expect(thrownError.digest).toBe('test-error-digest');
1068
+ }
1069
+ });
1070
+
1071
+ it('supports Float hints before the first await in server components in Fiber', async () => {
1072
+ function Component() {
1073
+ return <p>hello world</p>;
1074
+ }
1075
+
1076
+ const ClientComponent = clientExports(Component);
1077
+
1078
+ async function ServerComponent() {
1079
+ ReactDOM.preload('before', {as: 'style'});
1080
+ await 1;
1081
+ ReactDOM.preload('after', {as: 'style'});
1082
+ return <ClientComponent />;
1083
+ }
1084
+
1085
+ const stream = ReactServerDOMServer.renderToReadableStream(
1086
+ <ServerComponent />,
1087
+ turbopackMap,
1088
+ );
1089
+
1090
+ let response = null;
1091
+ function getResponse() {
1092
+ if (response === null) {
1093
+ response = ReactServerDOMClient.createFromReadableStream(stream);
1094
+ }
1095
+ return response;
1096
+ }
1097
+
1098
+ function App() {
1099
+ return getResponse();
1100
+ }
1101
+
1102
+ // pausing to let Flight runtime tick. This is a test only artifact of the fact that
1103
+ // we aren't operating separate module graphs for flight and fiber. In a real app
1104
+ // each would have their own dispatcher and there would be no cross dispatching.
1105
+ await 1;
1106
+
1107
+ const container = document.createElement('div');
1108
+ const root = ReactDOMClient.createRoot(container);
1109
+ await act(() => {
1110
+ root.render(<App />);
1111
+ });
1112
+ expect(document.head.innerHTML).toBe(
1113
+ '<link rel="preload" href="before" as="style">',
1114
+ );
1115
+ expect(container.innerHTML).toBe('<p>hello world</p>');
1116
+ });
1117
+
1118
+ it('Does not support Float hints in server components anywhere in Fizz', async () => {
1119
+ // In environments that do not support AsyncLocalStorage the Flight client has no ability
1120
+ // to scope hint dispatching to a specific Request. In Fiber this isn't a problem because
1121
+ // the Browser scope acts like a singleton and we can dispatch away. But in Fizz we need to have
1122
+ // a reference to Resources and this is only possible during render unless you support AsyncLocalStorage.
1123
+ function Component() {
1124
+ return <p>hello world</p>;
1125
+ }
1126
+
1127
+ const ClientComponent = clientExports(Component);
1128
+
1129
+ async function ServerComponent() {
1130
+ ReactDOM.preload('before', {as: 'style'});
1131
+ await 1;
1132
+ ReactDOM.preload('after', {as: 'style'});
1133
+ return <ClientComponent />;
1134
+ }
1135
+
1136
+ const stream = ReactServerDOMServer.renderToReadableStream(
1137
+ <ServerComponent />,
1138
+ turbopackMap,
1139
+ );
1140
+
1141
+ let response = null;
1142
+ function getResponse() {
1143
+ if (response === null) {
1144
+ response = ReactServerDOMClient.createFromReadableStream(stream);
1145
+ }
1146
+ return response;
1147
+ }
1148
+
1149
+ function App() {
1150
+ return (
1151
+ <html>
1152
+ <body>{getResponse()}</body>
1153
+ </html>
1154
+ );
1155
+ }
1156
+
1157
+ // pausing to let Flight runtime tick. This is a test only artifact of the fact that
1158
+ // we aren't operating separate module graphs for flight and fiber. In a real app
1159
+ // each would have their own dispatcher and there would be no cross dispatching.
1160
+ await 1;
1161
+
1162
+ let fizzStream;
1163
+ await act(async () => {
1164
+ fizzStream = await ReactDOMFizzServer.renderToReadableStream(<App />);
1165
+ });
1166
+
1167
+ const decoder = new TextDecoder();
1168
+ const reader = fizzStream.getReader();
1169
+ let content = '';
1170
+ while (true) {
1171
+ const {done, value} = await reader.read();
1172
+ if (done) {
1173
+ content += decoder.decode();
1174
+ break;
1175
+ }
1176
+ content += decoder.decode(value, {stream: true});
1177
+ }
1178
+
1179
+ expect(content).toEqual(
1180
+ '<!DOCTYPE html><html><head>' +
1181
+ '</head><body><p>hello world</p></body></html>',
1182
+ );
1183
+ });
1184
+
1185
+ // @gate enablePostpone
1186
+ it('supports postpone in Server Components', async () => {
1187
+ function Server() {
1188
+ React.unstable_postpone('testing postpone');
1189
+ return 'Not shown';
1190
+ }
1191
+
1192
+ let postponed = null;
1193
+
1194
+ const stream = ReactServerDOMServer.renderToReadableStream(
1195
+ <Suspense fallback="Loading...">
1196
+ <Server />
1197
+ </Suspense>,
1198
+ null,
1199
+ {
1200
+ onPostpone(reason) {
1201
+ postponed = reason;
1202
+ },
1203
+ },
1204
+ );
1205
+ const response = ReactServerDOMClient.createFromReadableStream(stream);
1206
+
1207
+ function Client() {
1208
+ return use(response);
1209
+ }
1210
+
1211
+ const container = document.createElement('div');
1212
+ const root = ReactDOMClient.createRoot(container);
1213
+ await act(async () => {
1214
+ root.render(
1215
+ <div>
1216
+ Shell: <Client />
1217
+ </div>,
1218
+ );
1219
+ });
1220
+ // We should have reserved the shell already. Which means that the Server
1221
+ // Component should've been a lazy component.
1222
+ expect(container.innerHTML).toContain('Shell:');
1223
+ expect(container.innerHTML).toContain('Loading...');
1224
+ expect(container.innerHTML).not.toContain('Not shown');
1225
+
1226
+ expect(postponed).toBe('testing postpone');
1227
+ });
1228
+});
packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMEdge-test.js
new
+205
@@ -0,0 +1,205 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+// Polyfills for test environment
13
+global.ReadableStream =
14
+ require('web-streams-polyfill/ponyfill/es6').ReadableStream;
15
+global.TextEncoder = require('util').TextEncoder;
16
+global.TextDecoder = require('util').TextDecoder;
17
+
18
+// Don't wait before processing work on the server.
19
+// TODO: we can replace this with FlightServer.act().
20
+global.setTimeout = cb => cb();
21
+
22
+let clientExports;
23
+let turbopackMap;
24
+let turbopackModules;
25
+let React;
26
+let ReactDOMServer;
27
+let ReactServerDOMServer;
28
+let ReactServerDOMClient;
29
+let use;
30
+
31
+describe('ReactFlightDOMEdge', () => {
32
+ beforeEach(() => {
33
+ jest.resetModules();
34
+
35
+ // Simulate the condition resolution
36
+ jest.mock('react-server-dom-turbopack/server', () =>
37
+ require('react-server-dom-turbopack/server.edge'),
38
+ );
39
+
40
+ const TurbopackMock = require('./utils/TurbopackMock');
41
+ clientExports = TurbopackMock.clientExports;
42
+ turbopackMap = TurbopackMock.turbopackMap;
43
+ turbopackModules = TurbopackMock.turbopackModules;
44
+ React = require('react');
45
+ ReactDOMServer = require('react-dom/server.edge');
46
+ ReactServerDOMServer = require('react-server-dom-turbopack/server.edge');
47
+ ReactServerDOMClient = require('react-server-dom-turbopack/client.edge');
48
+ use = React.use;
49
+ });
50
+
51
+ function passThrough(stream) {
52
+ // Simulate more realistic network by splitting up and rejoining some chunks.
53
+ // This lets us test that we don't accidentally rely on particular bounds of the chunks.
54
+ return new ReadableStream({
55
+ async start(controller) {
56
+ const reader = stream.getReader();
57
+ let prevChunk = new Uint8Array(0);
58
+ function push() {
59
+ reader.read().then(({done, value}) => {
60
+ if (done) {
61
+ controller.enqueue(prevChunk);
62
+ controller.close();
63
+ return;
64
+ }
65
+ const chunk = new Uint8Array(prevChunk.length + value.length);
66
+ chunk.set(prevChunk, 0);
67
+ chunk.set(value, prevChunk.length);
68
+ if (chunk.length > 50) {
69
+ controller.enqueue(chunk.subarray(0, chunk.length - 50));
70
+ prevChunk = chunk.subarray(chunk.length - 50);
71
+ } else {
72
+ prevChunk = chunk;
73
+ }
74
+ push();
75
+ });
76
+ }
77
+ push();
78
+ },
79
+ });
80
+ }
81
+
82
+ async function readResult(stream) {
83
+ const reader = stream.getReader();
84
+ let result = '';
85
+ while (true) {
86
+ const {done, value} = await reader.read();
87
+ if (done) {
88
+ return result;
89
+ }
90
+ result += Buffer.from(value).toString('utf8');
91
+ }
92
+ }
93
+
94
+ it('should allow an alternative module mapping to be used for SSR', async () => {
95
+ function ClientComponent() {
96
+ return <span>Client Component</span>;
97
+ }
98
+ // The Client build may not have the same IDs as the Server bundles for the same
99
+ // component.
100
+ const ClientComponentOnTheClient = clientExports(ClientComponent);
101
+ const ClientComponentOnTheServer = clientExports(ClientComponent);
102
+
103
+ // In the SSR bundle this module won't exist. We simulate this by deleting it.
104
+ const clientId = turbopackMap[ClientComponentOnTheClient.$$id].id;
105
+ delete turbopackModules[clientId];
106
+
107
+ // Instead, we have to provide a translation from the client meta data to the SSR
108
+ // meta data.
109
+ const ssrMetadata = turbopackMap[ClientComponentOnTheServer.$$id];
110
+ const translationMap = {
111
+ [clientId]: {
112
+ '*': ssrMetadata,
113
+ },
114
+ };
115
+
116
+ function App() {
117
+ return <ClientComponentOnTheClient />;
118
+ }
119
+
120
+ const stream = ReactServerDOMServer.renderToReadableStream(
121
+ <App />,
122
+ turbopackMap,
123
+ );
124
+ const response = ReactServerDOMClient.createFromReadableStream(stream, {
125
+ ssrManifest: {
126
+ moduleMap: translationMap,
127
+ moduleLoading: null,
128
+ },
129
+ });
130
+
131
+ function ClientRoot() {
132
+ return use(response);
133
+ }
134
+
135
+ const ssrStream = await ReactDOMServer.renderToReadableStream(
136
+ <ClientRoot />,
137
+ );
138
+ const result = await readResult(ssrStream);
139
+ expect(result).toEqual('<span>Client Component</span>');
140
+ });
141
+
142
+ it('should encode long string in a compact format', async () => {
143
+ const testString = '"\n\t'.repeat(500) + '🙃';
144
+ const testString2 = 'hello'.repeat(400);
145
+
146
+ const stream = ReactServerDOMServer.renderToReadableStream({
147
+ text: testString,
148
+ text2: testString2,
149
+ });
150
+ const [stream1, stream2] = passThrough(stream).tee();
151
+
152
+ const serializedContent = await readResult(stream1);
153
+ // The content should be compact an unescaped
154
+ expect(serializedContent.length).toBeLessThan(4000);
155
+ expect(serializedContent).not.toContain('\\n');
156
+ expect(serializedContent).not.toContain('\\t');
157
+ expect(serializedContent).not.toContain('\\"');
158
+ expect(serializedContent).toContain('\t');
159
+
160
+ const result = await ReactServerDOMClient.createFromReadableStream(
161
+ stream2,
162
+ {
163
+ ssrManifest: {
164
+ moduleMap: null,
165
+ moduleLoading: null,
166
+ },
167
+ },
168
+ );
169
+ // Should still match the result when parsed
170
+ expect(result.text).toBe(testString);
171
+ expect(result.text2).toBe(testString2);
172
+ });
173
+
174
+ // @gate enableBinaryFlight
175
+ it('should be able to serialize any kind of typed array', async () => {
176
+ const buffer = new Uint8Array([
177
+ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
178
+ ]).buffer;
179
+ const buffers = [
180
+ buffer,
181
+ new Int8Array(buffer, 1),
182
+ new Uint8Array(buffer, 2),
183
+ new Uint8ClampedArray(buffer, 2),
184
+ new Int16Array(buffer, 2),
185
+ new Uint16Array(buffer, 2),
186
+ new Int32Array(buffer, 4),
187
+ new Uint32Array(buffer, 4),
188
+ new Float32Array(buffer, 4),
189
+ new Float64Array(buffer, 0),
190
+ new BigInt64Array(buffer, 0),
191
+ new BigUint64Array(buffer, 0),
192
+ new DataView(buffer, 3),
193
+ ];
194
+ const stream = passThrough(
195
+ ReactServerDOMServer.renderToReadableStream(buffers),
196
+ );
197
+ const result = await ReactServerDOMClient.createFromReadableStream(stream, {
198
+ ssrManifest: {
199
+ moduleMap: null,
200
+ moduleLoading: null,
201
+ },
202
+ });
203
+ expect(result).toEqual(buffers);
204
+ });
205
+});
packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMForm-test.js
new
+254
@@ -0,0 +1,254 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+import {insertNodesAndExecuteScripts} from 'react-dom/src/test-utils/FizzTestUtils';
13
+
14
+// Polyfills for test environment
15
+global.ReadableStream =
16
+ require('web-streams-polyfill/ponyfill/es6').ReadableStream;
17
+global.TextEncoder = require('util').TextEncoder;
18
+global.TextDecoder = require('util').TextDecoder;
19
+
20
+// Don't wait before processing work on the server.
21
+// TODO: we can replace this with FlightServer.act().
22
+global.setTimeout = cb => cb();
23
+
24
+let container;
25
+let serverExports;
26
+let turbopackServerMap;
27
+let React;
28
+let ReactDOMServer;
29
+let ReactServerDOMServer;
30
+let ReactServerDOMClient;
31
+
32
+describe('ReactFlightDOMForm', () => {
33
+ beforeEach(() => {
34
+ jest.resetModules();
35
+ // Simulate the condition resolution
36
+ jest.mock('react-server-dom-turbopack/server', () =>
37
+ require('react-server-dom-turbopack/server.edge'),
38
+ );
39
+ const TurbopackMock = require('./utils/TurbopackMock');
40
+ serverExports = TurbopackMock.serverExports;
41
+ turbopackServerMap = TurbopackMock.turbopackServerMap;
42
+ React = require('react');
43
+ ReactServerDOMServer = require('react-server-dom-turbopack/server.edge');
44
+ ReactServerDOMClient = require('react-server-dom-turbopack/client.edge');
45
+ ReactDOMServer = require('react-dom/server.edge');
46
+ container = document.createElement('div');
47
+ document.body.appendChild(container);
48
+ });
49
+
50
+ afterEach(() => {
51
+ document.body.removeChild(container);
52
+ });
53
+
54
+ async function POST(formData) {
55
+ const boundAction = await ReactServerDOMServer.decodeAction(
56
+ formData,
57
+ turbopackServerMap,
58
+ );
59
+ return boundAction();
60
+ }
61
+
62
+ function submit(submitter) {
63
+ const form = submitter.form || submitter;
64
+ if (!submitter.form) {
65
+ submitter = undefined;
66
+ }
67
+ const submitEvent = new Event('submit', {bubbles: true, cancelable: true});
68
+ submitEvent.submitter = submitter;
69
+ const returnValue = form.dispatchEvent(submitEvent);
70
+ if (!returnValue) {
71
+ return;
72
+ }
73
+ const action =
74
+ (submitter && submitter.getAttribute('formaction')) || form.action;
75
+ if (!/\s*javascript:/i.test(action)) {
76
+ const method = (submitter && submitter.formMethod) || form.method;
77
+ const encType = (submitter && submitter.formEnctype) || form.enctype;
78
+ if (method === 'post' && encType === 'multipart/form-data') {
79
+ let formData;
80
+ if (submitter) {
81
+ const temp = document.createElement('input');
82
+ temp.name = submitter.name;
83
+ temp.value = submitter.value;
84
+ submitter.parentNode.insertBefore(temp, submitter);
85
+ formData = new FormData(form);
86
+ temp.parentNode.removeChild(temp);
87
+ } else {
88
+ formData = new FormData(form);
89
+ }
90
+ return POST(formData);
91
+ }
92
+ throw new Error('Navigate to: ' + action);
93
+ }
94
+ }
95
+
96
+ async function readIntoContainer(stream) {
97
+ const reader = stream.getReader();
98
+ let result = '';
99
+ while (true) {
100
+ const {done, value} = await reader.read();
101
+ if (done) {
102
+ break;
103
+ }
104
+ result += Buffer.from(value).toString('utf8');
105
+ }
106
+ const temp = document.createElement('div');
107
+ temp.innerHTML = result;
108
+ insertNodesAndExecuteScripts(temp, container, null);
109
+ }
110
+
111
+ // @gate enableFormActions
112
+ it('can submit a passed server action without hydrating it', async () => {
113
+ let foo = null;
114
+
115
+ const serverAction = serverExports(function action(formData) {
116
+ foo = formData.get('foo');
117
+ return 'hello';
118
+ });
119
+ function App() {
120
+ return (
121
+ <form action={serverAction}>
122
+ <input type="text" name="foo" defaultValue="bar" />
123
+ </form>
124
+ );
125
+ }
126
+ const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
127
+ const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
128
+ ssrManifest: {
129
+ moduleMap: null,
130
+ moduleLoading: null,
131
+ },
132
+ });
133
+ const ssrStream = await ReactDOMServer.renderToReadableStream(response);
134
+ await readIntoContainer(ssrStream);
135
+
136
+ const form = container.firstChild;
137
+
138
+ expect(foo).toBe(null);
139
+
140
+ const result = await submit(form);
141
+
142
+ expect(result).toBe('hello');
143
+ expect(foo).toBe('bar');
144
+ });
145
+
146
+ // @gate enableFormActions
147
+ it('can submit an imported server action without hydrating it', async () => {
148
+ let foo = null;
149
+
150
+ const ServerModule = serverExports(function action(formData) {
151
+ foo = formData.get('foo');
152
+ return 'hi';
153
+ });
154
+ const serverAction = ReactServerDOMClient.createServerReference(
155
+ ServerModule.$$id,
156
+ );
157
+ function App() {
158
+ return (
159
+ <form action={serverAction}>
160
+ <input type="text" name="foo" defaultValue="bar" />
161
+ </form>
162
+ );
163
+ }
164
+
165
+ const ssrStream = await ReactDOMServer.renderToReadableStream(<App />);
166
+ await readIntoContainer(ssrStream);
167
+
168
+ const form = container.firstChild;
169
+
170
+ expect(foo).toBe(null);
171
+
172
+ const result = await submit(form);
173
+
174
+ expect(result).toBe('hi');
175
+
176
+ expect(foo).toBe('bar');
177
+ });
178
+
179
+ // @gate enableFormActions
180
+ it('can submit a complex closure server action without hydrating it', async () => {
181
+ let foo = null;
182
+
183
+ const serverAction = serverExports(function action(bound, formData) {
184
+ foo = formData.get('foo') + bound.complex;
185
+ return 'hello';
186
+ });
187
+ function App() {
188
+ return (
189
+ <form action={serverAction.bind(null, {complex: 'object'})}>
190
+ <input type="text" name="foo" defaultValue="bar" />
191
+ </form>
192
+ );
193
+ }
194
+ const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
195
+ const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
196
+ ssrManifest: {
197
+ moduleMap: null,
198
+ moduleLoading: null,
199
+ },
200
+ });
201
+ const ssrStream = await ReactDOMServer.renderToReadableStream(response);
202
+ await readIntoContainer(ssrStream);
203
+
204
+ const form = container.firstChild;
205
+
206
+ expect(foo).toBe(null);
207
+
208
+ const result = await submit(form);
209
+
210
+ expect(result).toBe('hello');
211
+ expect(foo).toBe('barobject');
212
+ });
213
+
214
+ // @gate enableFormActions
215
+ it('can submit a multiple complex closure server action without hydrating it', async () => {
216
+ let foo = null;
217
+
218
+ const serverAction = serverExports(function action(bound, formData) {
219
+ foo = formData.get('foo') + bound.complex;
220
+ return 'hello' + bound.complex;
221
+ });
222
+ function App() {
223
+ return (
224
+ <form action={serverAction.bind(null, {complex: 'a'})}>
225
+ <input type="text" name="foo" defaultValue="bar" />
226
+ <button formAction={serverAction.bind(null, {complex: 'b'})} />
227
+ <button formAction={serverAction.bind(null, {complex: 'c'})} />
228
+ <input
229
+ type="submit"
230
+ formAction={serverAction.bind(null, {complex: 'd'})}
231
+ />
232
+ </form>
233
+ );
234
+ }
235
+ const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
236
+ const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
237
+ ssrManifest: {
238
+ moduleMap: null,
239
+ moduleLoading: null,
240
+ },
241
+ });
242
+ const ssrStream = await ReactDOMServer.renderToReadableStream(response);
243
+ await readIntoContainer(ssrStream);
244
+
245
+ const form = container.firstChild;
246
+
247
+ expect(foo).toBe(null);
248
+
249
+ const result = await submit(form.getElementsByTagName('button')[1]);
250
+
251
+ expect(result).toBe('helloc');
252
+ expect(foo).toBe('barc');
253
+ });
254
+});
packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMNode-test.js
new
+256
@@ -0,0 +1,256 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+// Don't wait before processing work on the server.
13
+// TODO: we can replace this with FlightServer.act().
14
+global.setImmediate = cb => cb();
15
+
16
+let clientExports;
17
+let turbopackMap;
18
+let turbopackModules;
19
+let turbopackModuleLoading;
20
+let React;
21
+let ReactDOMServer;
22
+let ReactServerDOMServer;
23
+let ReactServerDOMClient;
24
+let Stream;
25
+let use;
26
+
27
+describe('ReactFlightDOMNode', () => {
28
+ beforeEach(() => {
29
+ jest.resetModules();
30
+
31
+ // Simulate the condition resolution
32
+ jest.mock('react', () => require('react/react.shared-subset'));
33
+ jest.mock('react-server-dom-turbopack/server', () =>
34
+ require('react-server-dom-turbopack/server.node'),
35
+ );
36
+ ReactServerDOMServer = require('react-server-dom-turbopack/server');
37
+
38
+ const TurbopackMock = require('./utils/TurbopackMock');
39
+ clientExports = TurbopackMock.clientExports;
40
+ turbopackMap = TurbopackMock.turbopackMap;
41
+ turbopackModules = TurbopackMock.turbopackModules;
42
+ turbopackModuleLoading = TurbopackMock.moduleLoading;
43
+
44
+ jest.resetModules();
45
+ jest.unmock('react');
46
+ jest.unmock('react-server-dom-turbopack/server');
47
+ jest.mock('react-server-dom-turbopack/client', () =>
48
+ require('react-server-dom-turbopack/client.node'),
49
+ );
50
+
51
+ React = require('react');
52
+ ReactDOMServer = require('react-dom/server.node');
53
+ ReactServerDOMClient = require('react-server-dom-turbopack/client');
54
+ Stream = require('stream');
55
+ use = React.use;
56
+ });
57
+
58
+ function readResult(stream) {
59
+ return new Promise((resolve, reject) => {
60
+ let buffer = '';
61
+ const writable = new Stream.PassThrough();
62
+ writable.setEncoding('utf8');
63
+ writable.on('data', chunk => {
64
+ buffer += chunk;
65
+ });
66
+ writable.on('error', error => {
67
+ reject(error);
68
+ });
69
+ writable.on('end', () => {
70
+ resolve(buffer);
71
+ });
72
+ stream.pipe(writable);
73
+ });
74
+ }
75
+
76
+ it('should allow an alternative module mapping to be used for SSR', async () => {
77
+ function ClientComponent() {
78
+ return <span>Client Component</span>;
79
+ }
80
+ // The Client build may not have the same IDs as the Server bundles for the same
81
+ // component.
82
+ const ClientComponentOnTheClient = clientExports(
83
+ ClientComponent,
84
+ 'path/to/chunk.js',
85
+ );
86
+ const ClientComponentOnTheServer = clientExports(ClientComponent);
87
+
88
+ // In the SSR bundle this module won't exist. We simulate this by deleting it.
89
+ const clientId = turbopackMap[ClientComponentOnTheClient.$$id].id;
90
+ delete turbopackModules[clientId];
91
+
92
+ // Instead, we have to provide a translation from the client meta data to the SSR
93
+ // meta data.
94
+ const ssrMetadata = turbopackMap[ClientComponentOnTheServer.$$id];
95
+ const translationMap = {
96
+ [clientId]: {
97
+ '*': ssrMetadata,
98
+ },
99
+ };
100
+
101
+ function App() {
102
+ return <ClientComponentOnTheClient />;
103
+ }
104
+
105
+ const stream = ReactServerDOMServer.renderToPipeableStream(
106
+ <App />,
107
+ turbopackMap,
108
+ );
109
+ const readable = new Stream.PassThrough();
110
+
111
+ stream.pipe(readable);
112
+
113
+ let response;
114
+ function ClientRoot() {
115
+ if (!response) {
116
+ response = ReactServerDOMClient.createFromNodeStream(readable, {
117
+ moduleMap: translationMap,
118
+ moduleLoading: turbopackModuleLoading,
119
+ });
120
+ }
121
+ return use(response);
122
+ }
123
+
124
+ const ssrStream = await ReactDOMServer.renderToPipeableStream(
125
+ <ClientRoot />,
126
+ );
127
+ const result = await readResult(ssrStream);
128
+ expect(result).toEqual(
129
+ '<script src="/prefix/path/to/chunk.js" async=""></script><span>Client Component</span>',
130
+ );
131
+ });
132
+
133
+ it('should encode long string in a compact format', async () => {
134
+ const testString = '"\n\t'.repeat(500) + '🙃';
135
+
136
+ const stream = ReactServerDOMServer.renderToPipeableStream({
137
+ text: testString,
138
+ });
139
+
140
+ const readable = new Stream.PassThrough();
141
+
142
+ const stringResult = readResult(readable);
143
+ const parsedResult = ReactServerDOMClient.createFromNodeStream(readable, {
144
+ moduleMap: turbopackMap,
145
+ moduleLoading: turbopackModuleLoading,
146
+ });
147
+
148
+ stream.pipe(readable);
149
+
150
+ const serializedContent = await stringResult;
151
+ // The content should be compact an unescaped
152
+ expect(serializedContent.length).toBeLessThan(2000);
153
+ expect(serializedContent).not.toContain('\\n');
154
+ expect(serializedContent).not.toContain('\\t');
155
+ expect(serializedContent).not.toContain('\\"');
156
+ expect(serializedContent).toContain('\t');
157
+
158
+ const result = await parsedResult;
159
+ // Should still match the result when parsed
160
+ expect(result.text).toBe(testString);
161
+ });
162
+
163
+ // @gate enableBinaryFlight
164
+ it('should be able to serialize any kind of typed array', async () => {
165
+ const buffer = new Uint8Array([
166
+ 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
167
+ ]).buffer;
168
+ const buffers = [
169
+ buffer,
170
+ new Int8Array(buffer, 1),
171
+ new Uint8Array(buffer, 2),
172
+ new Uint8ClampedArray(buffer, 2),
173
+ new Int16Array(buffer, 2),
174
+ new Uint16Array(buffer, 2),
175
+ new Int32Array(buffer, 4),
176
+ new Uint32Array(buffer, 4),
177
+ new Float32Array(buffer, 4),
178
+ new Float64Array(buffer, 0),
179
+ new BigInt64Array(buffer, 0),
180
+ new BigUint64Array(buffer, 0),
181
+ new DataView(buffer, 3),
182
+ ];
183
+ const stream = ReactServerDOMServer.renderToPipeableStream(buffers);
184
+ const readable = new Stream.PassThrough();
185
+ const promise = ReactServerDOMClient.createFromNodeStream(readable, {
186
+ moduleMap: turbopackMap,
187
+ moduleLoading: turbopackModuleLoading,
188
+ });
189
+ stream.pipe(readable);
190
+ const result = await promise;
191
+ expect(result).toEqual(buffers);
192
+ });
193
+
194
+ it('should allow accept a nonce option for Flight preinitialized scripts', async () => {
195
+ function ClientComponent() {
196
+ return <span>Client Component</span>;
197
+ }
198
+ // The Client build may not have the same IDs as the Server bundles for the same
199
+ // component.
200
+ const ClientComponentOnTheClient = clientExports(
201
+ ClientComponent,
202
+ 'path/to/chunk.js',
203
+ );
204
+ const ClientComponentOnTheServer = clientExports(ClientComponent);
205
+
206
+ // In the SSR bundle this module won't exist. We simulate this by deleting it.
207
+ const clientId = turbopackMap[ClientComponentOnTheClient.$$id].id;
208
+ delete turbopackModules[clientId];
209
+
210
+ // Instead, we have to provide a translation from the client meta data to the SSR
211
+ // meta data.
212
+ const ssrMetadata = turbopackMap[ClientComponentOnTheServer.$$id];
213
+ const translationMap = {
214
+ [clientId]: {
215
+ '*': ssrMetadata,
216
+ },
217
+ };
218
+ const ssrManifest = {
219
+ moduleMap: translationMap,
220
+ moduleLoading: turbopackModuleLoading,
221
+ };
222
+
223
+ function App() {
224
+ return <ClientComponentOnTheClient />;
225
+ }
226
+
227
+ const stream = ReactServerDOMServer.renderToPipeableStream(
228
+ <App />,
229
+ turbopackMap,
230
+ );
231
+ const readable = new Stream.PassThrough();
232
+ let response;
233
+
234
+ stream.pipe(readable);
235
+
236
+ function ClientRoot() {
237
+ if (response) return use(response);
238
+ response = ReactServerDOMClient.createFromNodeStream(
239
+ readable,
240
+ ssrManifest,
241
+ {
242
+ nonce: 'r4nd0m',
243
+ },
244
+ );
245
+ return use(response);
246
+ }
247
+
248
+ const ssrStream = await ReactDOMServer.renderToPipeableStream(
249
+ <ClientRoot />,
250
+ );
251
+ const result = await readResult(ssrStream);
252
+ expect(result).toEqual(
253
+ '<script src="/prefix/path/to/chunk.js" async="" nonce="r4nd0m"></script><span>Client Component</span>',
254
+ );
255
+ });
256
+});
packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMReply-test.js
new
+232
@@ -0,0 +1,232 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+// Polyfills for test environment
13
+global.ReadableStream =
14
+ require('web-streams-polyfill/ponyfill/es6').ReadableStream;
15
+global.TextEncoder = require('util').TextEncoder;
16
+global.TextDecoder = require('util').TextDecoder;
17
+
18
+// let serverExports;
19
+let turbopackServerMap;
20
+let ReactServerDOMServer;
21
+let ReactServerDOMClient;
22
+
23
+describe('ReactFlightDOMReply', () => {
24
+ beforeEach(() => {
25
+ jest.resetModules();
26
+ // Simulate the condition resolution
27
+ jest.mock('react-server-dom-turbopack/server', () =>
28
+ require('react-server-dom-turbopack/server.browser'),
29
+ );
30
+ const TurbopackMock = require('./utils/TurbopackMock');
31
+ // serverExports = TurbopackMock.serverExports;
32
+ turbopackServerMap = TurbopackMock.turbopackServerMap;
33
+ ReactServerDOMServer = require('react-server-dom-turbopack/server.browser');
34
+ ReactServerDOMClient = require('react-server-dom-turbopack/client');
35
+ });
36
+
37
+ // This method should exist on File but is not implemented in JSDOM
38
+ async function arrayBuffer(file) {
39
+ return new Promise((resolve, reject) => {
40
+ const reader = new FileReader();
41
+ reader.onload = function () {
42
+ return resolve(reader.result);
43
+ };
44
+ reader.onerror = function () {
45
+ return reject(reader.error);
46
+ };
47
+ reader.readAsArrayBuffer(file);
48
+ });
49
+ }
50
+
51
+ it('can pass undefined as a reply', async () => {
52
+ const body = await ReactServerDOMClient.encodeReply(undefined);
53
+ const missing = await ReactServerDOMServer.decodeReply(
54
+ body,
55
+ turbopackServerMap,
56
+ );
57
+ expect(missing).toBe(undefined);
58
+
59
+ const body2 = await ReactServerDOMClient.encodeReply({
60
+ array: [undefined, null, undefined],
61
+ prop: undefined,
62
+ });
63
+ const object = await ReactServerDOMServer.decodeReply(
64
+ body2,
65
+ turbopackServerMap,
66
+ );
67
+ expect(object.array.length).toBe(3);
68
+ expect(object.array[0]).toBe(undefined);
69
+ expect(object.array[1]).toBe(null);
70
+ expect(object.array[3]).toBe(undefined);
71
+ expect(object.prop).toBe(undefined);
72
+ // These should really be true but our deserialization doesn't currently deal with it.
73
+ expect('3' in object.array).toBe(false);
74
+ expect('prop' in object).toBe(false);
75
+ });
76
+
77
+ it('can pass an iterable as a reply', async () => {
78
+ const body = await ReactServerDOMClient.encodeReply({
79
+ [Symbol.iterator]: function* () {
80
+ yield 'A';
81
+ yield 'B';
82
+ yield 'C';
83
+ },
84
+ });
85
+ const iterable = await ReactServerDOMServer.decodeReply(
86
+ body,
87
+ turbopackServerMap,
88
+ );
89
+ const items = [];
90
+ // eslint-disable-next-line no-for-of-loops/no-for-of-loops
91
+ for (const item of iterable) {
92
+ items.push(item);
93
+ }
94
+ expect(items).toEqual(['A', 'B', 'C']);
95
+ });
96
+
97
+ it('can pass weird numbers as a reply', async () => {
98
+ const nums = [0, -0, Infinity, -Infinity, NaN];
99
+ const body = await ReactServerDOMClient.encodeReply(nums);
100
+ const nums2 = await ReactServerDOMServer.decodeReply(
101
+ body,
102
+ turbopackServerMap,
103
+ );
104
+
105
+ expect(nums).toEqual(nums2);
106
+ expect(nums.every((n, i) => Object.is(n, nums2[i]))).toBe(true);
107
+ });
108
+
109
+ it('can pass a BigInt as a reply', async () => {
110
+ const body = await ReactServerDOMClient.encodeReply(90071992547409910000n);
111
+ const n = await ReactServerDOMServer.decodeReply(body, turbopackServerMap);
112
+
113
+ expect(n).toEqual(90071992547409910000n);
114
+ });
115
+
116
+ it('can pass FormData as a reply', async () => {
117
+ const formData = new FormData();
118
+ formData.set('hello', 'world');
119
+ formData.append('list', '1');
120
+ formData.append('list', '2');
121
+ formData.append('list', '3');
122
+ const typedArray = new Uint8Array([0, 1, 2, 3]);
123
+ const blob = new Blob([typedArray]);
124
+ formData.append('blob', blob, 'filename.blob');
125
+
126
+ const body = await ReactServerDOMClient.encodeReply(formData);
127
+ const formData2 = await ReactServerDOMServer.decodeReply(
128
+ body,
129
+ turbopackServerMap,
130
+ );
131
+
132
+ expect(formData2).not.toBe(formData);
133
+ expect(Array.from(formData2).length).toBe(5);
134
+ expect(formData2.get('hello')).toBe('world');
135
+ expect(formData2.getAll('list')).toEqual(['1', '2', '3']);
136
+ const blob2 = formData.get('blob');
137
+ expect(blob2.size).toBe(4);
138
+ expect(blob2.name).toBe('filename.blob');
139
+ expect(blob2.type).toBe('');
140
+ const typedArray2 = new Uint8Array(await arrayBuffer(blob2));
141
+ expect(typedArray2).toEqual(typedArray);
142
+ });
143
+
144
+ it('can pass multiple Files in FormData', async () => {
145
+ const typedArrayA = new Uint8Array([0, 1, 2, 3]);
146
+ const typedArrayB = new Uint8Array([4, 5]);
147
+ const blobA = new Blob([typedArrayA]);
148
+ const blobB = new Blob([typedArrayB]);
149
+ const formData = new FormData();
150
+ formData.append('filelist', 'string');
151
+ formData.append('filelist', blobA);
152
+ formData.append('filelist', blobB);
153
+
154
+ const body = await ReactServerDOMClient.encodeReply(formData);
155
+ const formData2 = await ReactServerDOMServer.decodeReply(
156
+ body,
157
+ turbopackServerMap,
158
+ );
159
+
160
+ const filelist2 = formData2.getAll('filelist');
161
+ expect(filelist2.length).toBe(3);
162
+ expect(filelist2[0]).toBe('string');
163
+ const blobA2 = filelist2[1];
164
+ expect(blobA2.size).toBe(4);
165
+ expect(blobA2.name).toBe('blob');
166
+ expect(blobA2.type).toBe('');
167
+ const typedArrayA2 = new Uint8Array(await arrayBuffer(blobA2));
168
+ expect(typedArrayA2).toEqual(typedArrayA);
169
+ const blobB2 = filelist2[2];
170
+ expect(blobB2.size).toBe(2);
171
+ expect(blobB2.name).toBe('blob');
172
+ expect(blobB2.type).toBe('');
173
+ const typedArrayB2 = new Uint8Array(await arrayBuffer(blobB2));
174
+ expect(typedArrayB2).toEqual(typedArrayB);
175
+ });
176
+
177
+ it('can pass two independent FormData with same keys', async () => {
178
+ const formDataA = new FormData();
179
+ formDataA.set('greeting', 'hello');
180
+ const formDataB = new FormData();
181
+ formDataB.set('greeting', 'hi');
182
+
183
+ const body = await ReactServerDOMClient.encodeReply({
184
+ a: formDataA,
185
+ b: formDataB,
186
+ });
187
+ const {a: formDataA2, b: formDataB2} =
188
+ await ReactServerDOMServer.decodeReply(body, turbopackServerMap);
189
+
190
+ expect(Array.from(formDataA2).length).toBe(1);
191
+ expect(Array.from(formDataB2).length).toBe(1);
192
+ expect(formDataA2.get('greeting')).toBe('hello');
193
+ expect(formDataB2.get('greeting')).toBe('hi');
194
+ });
195
+
196
+ it('can pass a Date as a reply', async () => {
197
+ const d = new Date(1234567890123);
198
+ const body = await ReactServerDOMClient.encodeReply(d);
199
+ const d2 = await ReactServerDOMServer.decodeReply(body, turbopackServerMap);
200
+
201
+ expect(d).toEqual(d2);
202
+ expect(d % 1000).toEqual(123); // double-check the milliseconds made it through
203
+ });
204
+
205
+ it('can pass a Map as a reply', async () => {
206
+ const objKey = {obj: 'key'};
207
+ const m = new Map([
208
+ ['hi', {greet: 'world'}],
209
+ [objKey, 123],
210
+ ]);
211
+ const body = await ReactServerDOMClient.encodeReply(m);
212
+ const m2 = await ReactServerDOMServer.decodeReply(body, turbopackServerMap);
213
+
214
+ expect(m2 instanceof Map).toBe(true);
215
+ expect(m2.size).toBe(2);
216
+ expect(m2.get('hi').greet).toBe('world');
217
+ expect(m2).toEqual(m);
218
+ });
219
+
220
+ it('can pass a Set as a reply', async () => {
221
+ const objKey = {obj: 'key'};
222
+ const s = new Set(['hi', objKey]);
223
+
224
+ const body = await ReactServerDOMClient.encodeReply(s);
225
+ const s2 = await ReactServerDOMServer.decodeReply(body, turbopackServerMap);
226
+
227
+ expect(s2 instanceof Set).toBe(true);
228
+ expect(s2.size).toBe(2);
229
+ expect(s2.has('hi')).toBe(true);
230
+ expect(s2).toEqual(s);
231
+ });
232
+});
packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js
new
+148
@@ -0,0 +1,148 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+'use strict';
9
+
10
+const url = require('url');
11
+const Module = require('module');
12
+
13
+let turbopackModuleIdx = 0;
14
+const turbopackServerModules = {};
15
+const turbopackClientModules = {};
16
+const turbopackErroredModules = {};
17
+const turbopackServerMap = {};
18
+const turbopackClientMap = {};
19
+global.__turbopack_require__ = function (id) {
20
+ if (turbopackErroredModules[id]) {
21
+ throw turbopackErroredModules[id];
22
+ }
23
+ return turbopackClientModules[id] || turbopackServerModules[id];
24
+};
25
+
26
+const previousCompile = Module.prototype._compile;
27
+
28
+const register = require('react-server-dom-turbopack/node-register');
29
+// Register node compile
30
+register();
31
+
32
+const nodeCompile = Module.prototype._compile;
33
+
34
+if (previousCompile === nodeCompile) {
35
+ throw new Error(
36
+ 'Expected the Node loader to register the _compile extension',
37
+ );
38
+}
39
+
40
+Module.prototype._compile = previousCompile;
41
+
42
+exports.turbopackMap = turbopackClientMap;
43
+exports.turbopackModules = turbopackClientModules;
44
+exports.turbopackServerMap = turbopackServerMap;
45
+exports.moduleLoading = {
46
+ prefix: '/prefix/',
47
+};
48
+
49
+exports.clientModuleError = function clientModuleError(moduleError) {
50
+ const idx = '' + turbopackModuleIdx++;
51
+ turbopackErroredModules[idx] = moduleError;
52
+ const path = url.pathToFileURL(idx).href;
53
+ turbopackClientMap[path] = {
54
+ id: idx,
55
+ chunks: [],
56
+ name: '*',
57
+ };
58
+ const mod = {exports: {}};
59
+ nodeCompile.call(mod, '"use client"', idx);
60
+ return mod.exports;
61
+};
62
+
63
+exports.clientExports = function clientExports(moduleExports, chunkUrl) {
64
+ const chunks = [];
65
+ if (chunkUrl !== undefined) {
66
+ chunks.push(chunkUrl);
67
+ }
68
+ const idx = '' + turbopackModuleIdx++;
69
+ turbopackClientModules[idx] = moduleExports;
70
+ const path = url.pathToFileURL(idx).href;
71
+ turbopackClientMap[path] = {
72
+ id: idx,
73
+ chunks,
74
+ name: '*',
75
+ };
76
+ // We only add this if this test is testing ESM compat.
77
+ if ('__esModule' in moduleExports) {
78
+ turbopackClientMap[path + '#'] = {
79
+ id: idx,
80
+ chunks,
81
+ name: '',
82
+ };
83
+ }
84
+ if (typeof moduleExports.then === 'function') {
85
+ moduleExports.then(
86
+ asyncModuleExports => {
87
+ for (const name in asyncModuleExports) {
88
+ turbopackClientMap[path + '#' + name] = {
89
+ id: idx,
90
+ chunks,
91
+ name: name,
92
+ };
93
+ }
94
+ },
95
+ () => {},
96
+ );
97
+ }
98
+ if ('split' in moduleExports) {
99
+ // If we're testing module splitting, we encode this name in a separate module id.
100
+ const splitIdx = '' + turbopackModuleIdx++;
101
+ turbopackClientModules[splitIdx] = {
102
+ s: moduleExports.split,
103
+ };
104
+ turbopackClientMap[path + '#split'] = {
105
+ id: splitIdx,
106
+ chunks,
107
+ name: 's',
108
+ };
109
+ }
110
+ const mod = {exports: {}};
111
+ nodeCompile.call(mod, '"use client"', idx);
112
+ return mod.exports;
113
+};
114
+
115
+// This tests server to server references. There's another case of client to server references.
116
+exports.serverExports = function serverExports(moduleExports) {
117
+ const idx = '' + turbopackModuleIdx++;
118
+ turbopackServerModules[idx] = moduleExports;
119
+ const path = url.pathToFileURL(idx).href;
120
+ turbopackServerMap[path] = {
121
+ id: idx,
122
+ chunks: [],
123
+ name: '*',
124
+ };
125
+ // We only add this if this test is testing ESM compat.
126
+ if ('__esModule' in moduleExports) {
127
+ turbopackServerMap[path + '#'] = {
128
+ id: idx,
129
+ chunks: [],
130
+ name: '',
131
+ };
132
+ }
133
+ if ('split' in moduleExports) {
134
+ // If we're testing module splitting, we encode this name in a separate module id.
135
+ const splitIdx = '' + turbopackModuleIdx++;
136
+ turbopackServerModules[splitIdx] = {
137
+ s: moduleExports.split,
138
+ };
139
+ turbopackServerMap[path + '#split'] = {
140
+ id: splitIdx,
141
+ chunks: [],
142
+ name: 's',
143
+ };
144
+ }
145
+ const mod = {exports: moduleExports};
146
+ nodeCompile.call(mod, '"use server"', idx);
147
+ return mod.exports;
148
+};
packages/react-server-dom-turbopack/src/shared/ReactFlightImportMetadata.js
new
+39
@@ -0,0 +1,39 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export type ImportManifestEntry = {
11
+ id: string,
12
+ // chunks is an array of filenames
13
+ chunks: Array<string>,
14
+ name: string,
15
+};
16
+
17
+// This is the parsed shape of the wire format which is why it is
18
+// condensed to only the essentialy information
19
+export type ImportMetadata =
20
+ | [
21
+ /* id */ string,
22
+ /* chunk filenames */ Array<string>,
23
+ /* name */ string,
24
+ /* async */ 1,
25
+ ]
26
+ | [/* id */ string, /* chunk filenames */ Array<string>, /* name */ string];
27
+
28
+export const ID = 0;
29
+export const CHUNKS = 1;
30
+export const NAME = 2;
31
+// export const ASYNC = 3;
32
+
33
+// This logic is correct because currently only include the 4th tuple member
34
+// when the module is async. If that changes we will need to actually assert
35
+// the value is true. We don't index into the 4th slot because flow does not
36
+// like the potential out of bounds access
37
+export function isAsyncImport(metadata: ImportMetadata): boolean {
38
+ return metadata.length === 4;
39
+}
packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-turbopack.js
new
+16
@@ -0,0 +1,16 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {Request} from 'react-server/src/ReactFlightServer';
11
+
12
+export * from 'react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler';
13
+export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
14
+
15
+export const supportsRequestStorage = false;
16
+export const requestStorage: AsyncLocalStorage<Request> = (null: any);
packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-turbopack.js
new
+18
@@ -0,0 +1,18 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+import type {Request} from 'react-server/src/ReactFlightServer';
10
+
11
+export * from 'react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler';
12
+export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
13
+
14
+// For now, we get this from the global scope, but this will likely move to a module.
15
+export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
16
+export const requestStorage: AsyncLocalStorage<Request> = supportsRequestStorage
17
+ ? new AsyncLocalStorage()
18
+ : (null: any);
packages/react-server/src/forks/ReactFlightServerConfig.dom-node-turbopack.js
new
+19
@@ -0,0 +1,19 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import {AsyncLocalStorage} from 'async_hooks';
11
+
12
+import type {Request} from 'react-server/src/ReactFlightServer';
13
+
14
+export * from 'react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler';
15
+export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
16
+
17
+export const supportsRequestStorage = true;
18
+export const requestStorage: AsyncLocalStorage<Request> =
19
+ new AsyncLocalStorage();
scripts/flow/environment.js
+5
@@ -78,6 +78,11 @@ declare var __webpack_require__: ((id: string) => any) & {
78
u: string => string,
79
};
80
81
+declare function __turbopack_load__(id: string): Promise<mixed>;
82
+declare var __turbopack_require__: ((id: string) => any) & {
83
+ u: string => string,
84
+};
85
+
86
declare module 'fs/promises' {
87
declare var access: (path: string, mode?: number) => Promise<void>;
88
declare var lstat: (
scripts/rollup/bundles.js
+103
@@ -432,6 +432,109 @@ const bundles = [
432
externals: ['url', 'module', 'react-server-dom-webpack/server'],
433
},
434
435
+ /******* React Server DOM Turbopack Server *******/
436
+ {
437
+ bundleTypes: [NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD],
438
+ moduleType: RENDERER,
439
+ entry: 'react-server-dom-turbopack/server.browser',
440
+ global: 'ReactServerDOMServer',
441
+ minifyWithProdErrorCodes: false,
442
+ wrapWithModuleBoundaries: false,
443
+ externals: ['react', 'react-dom'],
444
+ },
445
+ {
446
+ bundleTypes: [NODE_DEV, NODE_PROD],
447
+ moduleType: RENDERER,
448
+ entry: 'react-server-dom-turbopack/server.node',
449
+ global: 'ReactServerDOMServer',
450
+ minifyWithProdErrorCodes: false,
451
+ wrapWithModuleBoundaries: false,
452
+ externals: ['react', 'util', 'async_hooks', 'react-dom'],
453
+ },
454
+ {
455
+ bundleTypes: [NODE_DEV, NODE_PROD],
456
+ moduleType: RENDERER,
457
+ entry: 'react-server-dom-turbopack/server.node.unbundled',
458
+ global: 'ReactServerDOMServer',
459
+ minifyWithProdErrorCodes: false,
460
+ wrapWithModuleBoundaries: false,
461
+ externals: ['react', 'util', 'async_hooks', 'react-dom'],
462
+ },
463
+ {
464
+ bundleTypes: [NODE_DEV, NODE_PROD],
465
+ moduleType: RENDERER,
466
+ entry: 'react-server-dom-turbopack/server.edge',
467
+ global: 'ReactServerDOMServer',
468
+ minifyWithProdErrorCodes: false,
469
+ wrapWithModuleBoundaries: false,
470
+ externals: ['react', 'util', 'async_hooks', 'react-dom'],
471
+ },
472
+
473
+ /******* React Server DOM Turbopack Client *******/
474
+ {
475
+ bundleTypes: [NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD],
476
+ moduleType: RENDERER,
477
+ entry: 'react-server-dom-turbopack/client.browser',
478
+ global: 'ReactServerDOMClient',
479
+ minifyWithProdErrorCodes: false,
480
+ wrapWithModuleBoundaries: false,
481
+ externals: ['react', 'react-dom'],
482
+ },
483
+ {
484
+ bundleTypes: [NODE_DEV, NODE_PROD],
485
+ moduleType: RENDERER,
486
+ entry: 'react-server-dom-turbopack/client.node',
487
+ global: 'ReactServerDOMClient',
488
+ minifyWithProdErrorCodes: false,
489
+ wrapWithModuleBoundaries: false,
490
+ externals: ['react', 'react-dom', 'util'],
491
+ },
492
+ {
493
+ bundleTypes: [NODE_DEV, NODE_PROD],
494
+ moduleType: RENDERER,
495
+ entry: 'react-server-dom-turbopack/client.node.unbundled',
496
+ global: 'ReactServerDOMClient',
497
+ minifyWithProdErrorCodes: false,
498
+ wrapWithModuleBoundaries: false,
499
+ externals: ['react', 'react-dom', 'util'],
500
+ },
501
+ {
502
+ bundleTypes: [NODE_DEV, NODE_PROD],
503
+ moduleType: RENDERER,
504
+ entry: 'react-server-dom-turbopack/client.edge',
505
+ global: 'ReactServerDOMClient',
506
+ minifyWithProdErrorCodes: false,
507
+ wrapWithModuleBoundaries: false,
508
+ externals: ['react', 'react-dom'],
509
+ },
510
+
511
+ /******* React Server DOM Turbopack Plugin *******/
512
+ // There is no plugin the moment because Turbopack
513
+ // does not expose a plugin interface yet.
514
+
515
+ /******* React Server DOM Turbopack Node.js Loader *******/
516
+ {
517
+ bundleTypes: [ESM_PROD],
518
+ moduleType: RENDERER_UTILS,
519
+ entry: 'react-server-dom-turbopack/node-loader',
520
+ global: 'ReactServerTurbopackNodeLoader',
521
+ minifyWithProdErrorCodes: false,
522
+ wrapWithModuleBoundaries: false,
523
+ externals: ['acorn'],
524
+ },
525
+
526
+ /******* React Server DOM Turbopack Node.js CommonJS Loader *******/
527
+ {
528
+ bundleTypes: [NODE_ES2015],
529
+ moduleType: RENDERER_UTILS,
530
+ entry: 'react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister',
531
+ name: 'react-server-dom-turbopack-node-register',
532
+ global: 'ReactFlightWebpackNodeRegister',
533
+ minifyWithProdErrorCodes: false,
534
+ wrapWithModuleBoundaries: false,
535
+ externals: ['url', 'module', 'react-server-dom-turbopack/server'],
536
+ },
537
+
538
/******* React Server DOM ESM Server *******/
539
{
540
bundleTypes: [NODE_DEV, NODE_PROD],
scripts/rollup/validate/eslintrc.cjs.js
+4
@@ -56,6 +56,10 @@ module.exports = {
56
__webpack_chunk_load__: 'readonly',
57
__webpack_require__: 'readonly',
58
59
+ // Flight Turbopack
60
+ __turbopack_load__: 'readonly',
61
+ __turbopack_require__: 'readonly',
62
+
63
// jest
64
expect: 'readonly',
65
jest: 'readonly',
scripts/rollup/validate/eslintrc.cjs2015.js
+4
@@ -56,6 +56,10 @@ module.exports = {
56
__webpack_chunk_load__: 'readonly',
57
__webpack_require__: 'readonly',
58
59
+ // Flight Turbopack
60
+ __turbopack_load__: 'readonly',
61
+ __turbopack_require__: 'readonly',
62
+
63
// jest
64
expect: 'readonly',
65
jest: 'readonly',
scripts/rollup/validate/eslintrc.esm.js
+4
@@ -56,6 +56,10 @@ module.exports = {
56
__webpack_chunk_load__: 'readonly',
57
__webpack_require__: 'readonly',
58
59
+ // Flight Turbopack
60
+ __turbopack_load__: 'readonly',
61
+ __turbopack_require__: 'readonly',
62
+
63
// jest
64
expect: 'readonly',
65
jest: 'readonly',
scripts/rollup/validate/eslintrc.umd.js
+4
@@ -61,6 +61,10 @@ module.exports = {
61
__webpack_chunk_load__: 'readonly',
62
__webpack_require__: 'readonly',
63
64
+ // Flight Turbopack
65
+ __turbopack_load__: 'readonly',
66
+ __turbopack_require__: 'readonly',
67
+
68
// jest
69
jest: 'readonly',
70
scripts/shared/inlinedHostConfigs.js
+155
-21
@@ -49,6 +49,115 @@ module.exports = [
49
isFlowTyped: true,
50
isServerSupported: true,
51
},
52
+ {
53
+ shortName: 'dom-node-webpack',
54
+ entryPoints: [
55
+ 'react-server-dom-webpack/server.node',
56
+ 'react-server-dom-webpack/client.node',
57
+ ],
58
+ paths: [
59
+ 'react-dom',
60
+ 'react-dom-bindings',
61
+ 'react-dom/client',
62
+ 'react-dom/server',
63
+ 'react-dom/server.node',
64
+ 'react-dom/static',
65
+ 'react-dom/static.node',
66
+ 'react-dom/src/server/react-dom-server.node',
67
+ 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node
68
+ 'react-dom/src/server/ReactDOMFizzStaticNode.js',
69
+ 'react-server-dom-webpack',
70
+ 'react-server-dom-webpack/client.node',
71
+ 'react-server-dom-webpack/server',
72
+ 'react-server-dom-webpack/server.node',
73
+ 'react-server-dom-webpack/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node
74
+ 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js',
75
+ 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer.js',
76
+ 'react-server-dom-webpack/node-register',
77
+ 'react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js',
78
+ 'react-devtools',
79
+ 'react-devtools-core',
80
+ 'react-devtools-shell',
81
+ 'react-devtools-shared',
82
+ 'react-interactions',
83
+ 'shared/ReactDOMSharedInternals',
84
+ ],
85
+ isFlowTyped: true,
86
+ isServerSupported: true,
87
+ },
88
+ {
89
+ shortName: 'dom-node-turbopack',
90
+ entryPoints: [
91
+ 'react-server-dom-turbopack/server.node.unbundled',
92
+ 'react-server-dom-turbopack/client.node.unbundled',
93
+ ],
94
+ paths: [
95
+ 'react-dom',
96
+ 'react-dom-bindings',
97
+ 'react-dom/client',
98
+ 'react-dom/server',
99
+ 'react-dom/server.node',
100
+ 'react-dom/static',
101
+ 'react-dom/static.node',
102
+ 'react-dom/src/server/react-dom-server.node',
103
+ 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node
104
+ 'react-dom/src/server/ReactDOMFizzStaticNode.js',
105
+ 'react-server-dom-turbopack',
106
+ 'react-server-dom-turbopack/client.node.unbundled',
107
+ 'react-server-dom-turbopack/server',
108
+ 'react-server-dom-turbopack/server.node.unbundled',
109
+ 'react-server-dom-turbopack/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node.unbundled
110
+ 'react-server-dom-turbopack/src/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node.unbundled
111
+ 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js',
112
+ 'react-server-dom-turbopack/node-register',
113
+ 'react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js',
114
+ 'react-devtools',
115
+ 'react-devtools-core',
116
+ 'react-devtools-shell',
117
+ 'react-devtools-shared',
118
+ 'react-interactions',
119
+ 'shared/ReactDOMSharedInternals',
120
+ ],
121
+ isFlowTyped: true,
122
+ isServerSupported: true,
123
+ },
124
+ {
125
+ shortName: 'dom-node-turbopack-bundled',
126
+ entryPoints: [
127
+ 'react-server-dom-turbopack/server.node',
128
+ 'react-server-dom-turbopack/client.node',
129
+ ],
130
+ paths: [
131
+ 'react-dom',
132
+ 'react-dom-bindings',
133
+ 'react-dom/client',
134
+ 'react-dom/server',
135
+ 'react-dom/server.node',
136
+ 'react-dom/static',
137
+ 'react-dom/static.node',
138
+ 'react-dom/src/server/react-dom-server.node',
139
+ 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node
140
+ 'react-dom/src/server/ReactDOMFizzStaticNode.js',
141
+ 'react-server-dom-turbopack',
142
+ 'react-server-dom-turbopack/client.node',
143
+ 'react-server-dom-turbopack/server',
144
+ 'react-server-dom-turbopack/server.node',
145
+ 'react-server-dom-turbopack/src/ReactFlightDOMServerNode.js', // react-server-dom-turbopack/server.node
146
+ 'react-server-dom-turbopack/src/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node
147
+ 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js',
148
+ 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js',
149
+ 'react-server-dom-turbopack/node-register',
150
+ 'react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js',
151
+ 'react-devtools',
152
+ 'react-devtools-core',
153
+ 'react-devtools-shell',
154
+ 'react-devtools-shared',
155
+ 'react-interactions',
156
+ 'shared/ReactDOMSharedInternals',
157
+ ],
158
+ isFlowTyped: true,
159
+ isServerSupported: true,
160
+ },
161
{
162
shortName: 'dom-bun',
163
entryPoints: ['react-dom', 'react-dom/src/server/react-dom-server.bun.js'],
@@ -127,6 +236,36 @@ module.exports = [
236
isFlowTyped: true,
237
isServerSupported: true,
238
},
239
+ {
240
+ shortName: 'dom-browser-turbopack',
241
+ entryPoints: [
242
+ 'react-server-dom-turbopack/client.browser',
243
+ 'react-server-dom-turbopack/server.browser',
244
+ ],
245
+ paths: [
246
+ 'react-dom',
247
+ 'react-dom/client',
248
+ 'react-dom/server',
249
+ 'react-dom/server.node',
250
+ 'react-dom-bindings',
251
+ 'react-server-dom-turbopack',
252
+ 'react-server-dom-turbopack/client',
253
+ 'react-server-dom-turbopack/client.browser',
254
+ 'react-server-dom-turbopack/server.browser',
255
+ 'react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js', // react-server-dom-turbopack/client.browser
256
+ 'react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js', // react-server-dom-turbopack/server.browser
257
+ 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js',
258
+ 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser.js',
259
+ 'react-devtools',
260
+ 'react-devtools-core',
261
+ 'react-devtools-shell',
262
+ 'react-devtools-shared',
263
+ 'react-interactions',
264
+ 'shared/ReactDOMSharedInternals',
265
+ ],
266
+ isFlowTyped: true,
267
+ isServerSupported: true,
268
+ },
269
{
270
shortName: 'dom-edge-webpack',
271
entryPoints: [
@@ -163,38 +302,33 @@ module.exports = [
302
isServerSupported: true,
303
},
304
{
166
- shortName: 'dom-node-webpack',
305
+ shortName: 'dom-edge-turbopack',
306
entryPoints: [
168
- 'react-server-dom-webpack/server.node',
169
- 'react-server-dom-webpack/client.node',
307
+ 'react-server-dom-turbopack/server.edge',
308
+ 'react-server-dom-turbopack/client.edge',
309
],
310
paths: [
311
'react-dom',
312
'react-dom/src/ReactDOMSharedSubset.js',
313
'react-dom-bindings',
314
'react-dom/client',
176
- 'react-dom/server',
177
- 'react-dom/server.node',
178
- 'react-dom/static',
179
- 'react-dom/static.node',
180
- 'react-dom/src/server/react-dom-server.node',
181
- 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node
182
- 'react-dom/src/server/ReactDOMFizzStaticNode.js',
183
- 'react-server-dom-webpack',
184
- 'react-server-dom-webpack/client.node',
185
- 'react-server-dom-webpack/server',
186
- 'react-server-dom-webpack/server.node',
187
- 'react-server-dom-webpack/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node
188
- 'react-server-dom-webpack/src/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node
189
- 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js',
190
- 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer.js',
191
- 'react-server-dom-webpack/node-register',
192
- 'react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js',
315
+ 'react-dom/server.edge',
316
+ 'react-dom/static.edge',
317
+ 'react-dom/unstable_testing',
318
+ 'react-dom/src/server/react-dom-server.edge',
319
+ 'react-dom/src/server/ReactDOMFizzServerEdge.js', // react-dom/server.edge
320
+ 'react-dom/src/server/ReactDOMFizzStaticEdge.js',
321
+ 'react-server-dom-turbopack',
322
+ 'react-server-dom-turbopack/client.edge',
323
+ 'react-server-dom-turbopack/server.edge',
324
+ 'react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js', // react-server-dom-webpack/client.edge
325
+ 'react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js', // react-server-dom-webpack/server.edge
326
+ 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js',
327
+ 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js',
328
'react-devtools',
329
'react-devtools-core',
330
'react-devtools-shell',
331
'react-devtools-shared',
197
- 'react-interactions',
332
'shared/ReactDOMSharedInternals',
333
],
334
isFlowTyped: true,