@samitouri / QOS-React-2 / commits / f81c0f1ed9

[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,