diff --git a/.eslintrc.js b/.eslintrc.js index 878dd08fda653..4d53738e281ae 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -326,6 +326,7 @@ module.exports = { 'packages/react-refresh/**/*.js', 'packages/react-server-dom-esm/**/*.js', 'packages/react-server-dom-webpack/**/*.js', + 'packages/react-server-dom-turbopack/**/*.js', 'packages/react-test-renderer/**/*.js', 'packages/react-debug-tools/**/*.js', 'packages/react-devtools-extensions/**/*.js', @@ -427,6 +428,13 @@ module.exports = { __webpack_require__: 'readonly', }, }, + { + files: ['packages/react-server-dom-turbopack/**/*.js'], + globals: { + __turbopack_load__: 'readonly', + __turbopack_require__: 'readonly', + }, + }, { files: ['packages/scheduler/**/*.js'], globals: { diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js new file mode 100644 index 0000000000000..9cb36f0674d02 --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from 'react-client/src/ReactFlightClientConfigBrowser'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser'; +export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; +export const usedWithSSR = false; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js new file mode 100644 index 0000000000000..3c6949554e361 --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from 'react-client/src/ReactFlightClientConfigBrowser'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer'; +export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; +export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js new file mode 100644 index 0000000000000..30e737d14f668 --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from 'react-client/src/ReactFlightClientConfigNode'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer'; +export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; +export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js new file mode 100644 index 0000000000000..ad8f3608102cf --- /dev/null +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js @@ -0,0 +1,14 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from 'react-client/src/ReactFlightClientConfigNode'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode'; +export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer'; +export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; +export const usedWithSSR = true; diff --git a/packages/react-server-dom-turbopack/README.md b/packages/react-server-dom-turbopack/README.md new file mode 100644 index 0000000000000..3d183a8a4a2fd --- /dev/null +++ b/packages/react-server-dom-turbopack/README.md @@ -0,0 +1,5 @@ +# react-server-dom-turbopack + +Experimental React Flight bindings for DOM using Turbopack. + +**Use it at your own risk.** diff --git a/packages/react-server-dom-turbopack/client.browser.js b/packages/react-server-dom-turbopack/client.browser.js new file mode 100644 index 0000000000000..7d26c2771e50a --- /dev/null +++ b/packages/react-server-dom-turbopack/client.browser.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './src/ReactFlightDOMClientBrowser'; diff --git a/packages/react-server-dom-turbopack/client.edge.js b/packages/react-server-dom-turbopack/client.edge.js new file mode 100644 index 0000000000000..fadceeaf8443a --- /dev/null +++ b/packages/react-server-dom-turbopack/client.edge.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './src/ReactFlightDOMClientEdge'; diff --git a/packages/react-server-dom-turbopack/client.js b/packages/react-server-dom-turbopack/client.js new file mode 100644 index 0000000000000..2dad5bb513872 --- /dev/null +++ b/packages/react-server-dom-turbopack/client.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './client.browser'; diff --git a/packages/react-server-dom-turbopack/client.node.js b/packages/react-server-dom-turbopack/client.node.js new file mode 100644 index 0000000000000..4f435353a20f0 --- /dev/null +++ b/packages/react-server-dom-turbopack/client.node.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './src/ReactFlightDOMClientNode'; diff --git a/packages/react-server-dom-turbopack/client.node.unbundled.js b/packages/react-server-dom-turbopack/client.node.unbundled.js new file mode 100644 index 0000000000000..4f435353a20f0 --- /dev/null +++ b/packages/react-server-dom-turbopack/client.node.unbundled.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './src/ReactFlightDOMClientNode'; diff --git a/packages/react-server-dom-turbopack/esm/package.json b/packages/react-server-dom-turbopack/esm/package.json new file mode 100644 index 0000000000000..3dbc1ca591c05 --- /dev/null +++ b/packages/react-server-dom-turbopack/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/react-server-dom-turbopack/esm/react-server-dom-turbopack-node-loader.production.min.js b/packages/react-server-dom-turbopack/esm/react-server-dom-turbopack-node-loader.production.min.js new file mode 100644 index 0000000000000..ef6486656cafe --- /dev/null +++ b/packages/react-server-dom-turbopack/esm/react-server-dom-turbopack-node-loader.production.min.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from '../src/ReactFlightTurbopackNodeLoader.js'; diff --git a/packages/react-server-dom-turbopack/index.js b/packages/react-server-dom-turbopack/index.js new file mode 100644 index 0000000000000..348324f0de86d --- /dev/null +++ b/packages/react-server-dom-turbopack/index.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +throw new Error('Use react-server-dom-turbopack/client instead.'); diff --git a/packages/react-server-dom-turbopack/node-register.js b/packages/react-server-dom-turbopack/node-register.js new file mode 100644 index 0000000000000..0d399f3842731 --- /dev/null +++ b/packages/react-server-dom-turbopack/node-register.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +module.exports = require('./src/ReactFlightTurbopackNodeRegister'); diff --git a/packages/react-server-dom-turbopack/npm/client.browser.js b/packages/react-server-dom-turbopack/npm/client.browser.js new file mode 100644 index 0000000000000..5cd0ada188456 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/client.browser.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-turbopack-client.browser.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-turbopack-client.browser.development.js'); +} diff --git a/packages/react-server-dom-turbopack/npm/client.edge.js b/packages/react-server-dom-turbopack/npm/client.edge.js new file mode 100644 index 0000000000000..3499ce22c2a39 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/client.edge.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-turbopack-client.edge.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-turbopack-client.edge.development.js'); +} diff --git a/packages/react-server-dom-turbopack/npm/client.js b/packages/react-server-dom-turbopack/npm/client.js new file mode 100644 index 0000000000000..89d93a7a7920f --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/client.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./client.browser'); diff --git a/packages/react-server-dom-turbopack/npm/client.node.js b/packages/react-server-dom-turbopack/npm/client.node.js new file mode 100644 index 0000000000000..c346d351d344e --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/client.node.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-turbopack-client.node.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-turbopack-client.node.development.js'); +} diff --git a/packages/react-server-dom-turbopack/npm/client.node.unbundled.js b/packages/react-server-dom-turbopack/npm/client.node.unbundled.js new file mode 100644 index 0000000000000..9b15ea16d8a33 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/client.node.unbundled.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-turbopack-client.node.unbundled.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-turbopack-client.node.unbundled.development.js'); +} diff --git a/packages/react-server-dom-turbopack/npm/esm/package.json b/packages/react-server-dom-turbopack/npm/esm/package.json new file mode 100644 index 0000000000000..3dbc1ca591c05 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/react-server-dom-turbopack/npm/index.js b/packages/react-server-dom-turbopack/npm/index.js new file mode 100644 index 0000000000000..53e8a98128cdb --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/index.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +'use strict'; + +throw new Error('Use react-server-dom-turbopack/client instead.'); diff --git a/packages/react-server-dom-turbopack/npm/node-register.js b/packages/react-server-dom-turbopack/npm/node-register.js new file mode 100644 index 0000000000000..7506743f033fa --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/node-register.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./cjs/react-server-dom-turbopack-node-register.js'); diff --git a/packages/react-server-dom-turbopack/npm/server.browser.js b/packages/react-server-dom-turbopack/npm/server.browser.js new file mode 100644 index 0000000000000..05c0b03496bda --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/server.browser.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-turbopack-server.browser.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-turbopack-server.browser.development.js'); +} diff --git a/packages/react-server-dom-turbopack/npm/server.edge.js b/packages/react-server-dom-turbopack/npm/server.edge.js new file mode 100644 index 0000000000000..b09cb0b82282d --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/server.edge.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-turbopack-server.edge.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-turbopack-server.edge.development.js'); +} diff --git a/packages/react-server-dom-turbopack/npm/server.js b/packages/react-server-dom-turbopack/npm/server.js new file mode 100644 index 0000000000000..13a632e641179 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/server.js @@ -0,0 +1,6 @@ +'use strict'; + +throw new Error( + 'The React Server Writer cannot be used outside a react-server environment. ' + + 'You must configure Node.js using the `--conditions react-server` flag.' +); diff --git a/packages/react-server-dom-turbopack/npm/server.node.js b/packages/react-server-dom-turbopack/npm/server.node.js new file mode 100644 index 0000000000000..59635310eb2d1 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/server.node.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-turbopack-server.node.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-turbopack-server.node.development.js'); +} diff --git a/packages/react-server-dom-turbopack/npm/server.node.unbundled.js b/packages/react-server-dom-turbopack/npm/server.node.unbundled.js new file mode 100644 index 0000000000000..4f8856e4303a8 --- /dev/null +++ b/packages/react-server-dom-turbopack/npm/server.node.unbundled.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-turbopack-server.node.unbundled.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-turbopack-server.node.unbundled.development.js'); +} diff --git a/packages/react-server-dom-turbopack/package.json b/packages/react-server-dom-turbopack/package.json new file mode 100644 index 0000000000000..36e8765617368 --- /dev/null +++ b/packages/react-server-dom-turbopack/package.json @@ -0,0 +1,95 @@ +{ + "name": "react-server-dom-turbopack", + "description": "React Server Components bindings for DOM using Turbopack. This is intended to be integrated into meta-frameworks. It is not intended to be imported directly.", + "version": "18.2.0", + "keywords": [ + "react" + ], + "homepage": "https://reactjs.org/", + "bugs": "https://github.com/facebook/react/issues", + "license": "MIT", + "files": [ + "LICENSE", + "README.md", + "index.js", + "client.js", + "client.browser.js", + "client.edge.js", + "client.node.js", + "client.node.unbundled.js", + "server.js", + "server.browser.js", + "server.edge.js", + "server.node.js", + "server.node.unbundled.js", + "node-register.js", + "cjs/", + "umd/", + "esm/" + ], + "exports": { + ".": "./index.js", + "./client": { + "workerd": "./client.edge.js", + "deno": "./client.edge.js", + "worker": "./client.edge.js", + "node": { + "turbopack": "./client.node.js", + "webpack": "./client.node.js", + "default": "./client.node.unbundled.js" + }, + "edge-light": "./client.edge.js", + "browser": "./client.browser.js", + "default": "./client.browser.js" + }, + "./client.browser": "./client.browser.js", + "./client.edge": "./client.edge.js", + "./client.node": "./client.node.js", + "./client.node.unbundled": "./client.node.unbundled.js", + "./server": { + "react-server": { + "workerd": "./server.edge.js", + "deno": "./server.browser.js", + "node": { + "turbopack": "./server.node.js", + "webpack": "./server.node.js", + "default": "./server.node.unbundled.js" + }, + "edge-light": "./server.edge.js", + "browser": "./server.browser.js" + }, + "default": "./server.js" + }, + "./server.browser": "./server.browser.js", + "./server.edge": "./server.edge.js", + "./server.node": "./server.node.js", + "./server.node.unbundled": "./server.node.unbundled.js", + "./node-loader": "./esm/react-server-dom-turbopack-node-loader.production.min.js", + "./node-register": "./node-register.js", + "./src/*": "./src/*.js", + "./package.json": "./package.json" + }, + "main": "index.js", + "repository": { + "type" : "git", + "url" : "https://github.com/facebook/react.git", + "directory": "packages/react-server-dom-turbopack" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "dependencies": { + "acorn-loose": "^8.3.0", + "neo-async": "^2.6.1", + "loose-envify": "^1.1.0" + }, + "browserify": { + "transform": [ + "loose-envify" + ] + } +} diff --git a/packages/react-server-dom-turbopack/server.browser.js b/packages/react-server-dom-turbopack/server.browser.js new file mode 100644 index 0000000000000..41a9fb5c44968 --- /dev/null +++ b/packages/react-server-dom-turbopack/server.browser.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './src/ReactFlightDOMServerBrowser'; diff --git a/packages/react-server-dom-turbopack/server.edge.js b/packages/react-server-dom-turbopack/server.edge.js new file mode 100644 index 0000000000000..98f975cb4706f --- /dev/null +++ b/packages/react-server-dom-turbopack/server.edge.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './src/ReactFlightDOMServerEdge'; diff --git a/packages/react-server-dom-turbopack/server.js b/packages/react-server-dom-turbopack/server.js new file mode 100644 index 0000000000000..83d8b8a017ff2 --- /dev/null +++ b/packages/react-server-dom-turbopack/server.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +throw new Error( + 'The React Server cannot be used outside a react-server environment. ' + + 'You must configure Node.js using the `--conditions react-server` flag.', +); diff --git a/packages/react-server-dom-turbopack/server.node.js b/packages/react-server-dom-turbopack/server.node.js new file mode 100644 index 0000000000000..7726b9bb929d4 --- /dev/null +++ b/packages/react-server-dom-turbopack/server.node.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './src/ReactFlightDOMServerNode'; diff --git a/packages/react-server-dom-turbopack/server.node.unbundled.js b/packages/react-server-dom-turbopack/server.node.unbundled.js new file mode 100644 index 0000000000000..7726b9bb929d4 --- /dev/null +++ b/packages/react-server-dom-turbopack/server.node.unbundled.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './src/ReactFlightDOMServerNode'; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js new file mode 100644 index 0000000000000..b2bf9b8ae0b53 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js @@ -0,0 +1,162 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { + Thenable, + FulfilledThenable, + RejectedThenable, +} from 'shared/ReactTypes'; + +import type {ImportMetadata} from './shared/ReactFlightImportMetadata'; +import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig'; + +import { + ID, + CHUNKS, + NAME, + isAsyncImport, +} from './shared/ReactFlightImportMetadata'; +import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig'; + +export type SSRModuleMap = { + [clientId: string]: { + [clientExportName: string]: ClientReference, + }, +}; + +export type ServerManifest = void; + +export type ServerReferenceId = string; + +export opaque type ClientReferenceMetadata = ImportMetadata; + +// eslint-disable-next-line no-unused-vars +export opaque type ClientReference = { + specifier: string, + name: string, + async?: boolean, +}; + +// The reason this function needs to defined here in this file instead of just +// being exported directly from the WebpackDestination... file is because the +// ClientReferenceMetadata is opaque and we can't unwrap it there. +// This should get inlined and we could also just implement an unwrapping function +// though that risks it getting used in places it shouldn't be. This is unfortunate +// but currently it seems to be the best option we have. +export function prepareDestinationForModule( + moduleLoading: ModuleLoading, + nonce: ?string, + metadata: ClientReferenceMetadata, +) { + prepareDestinationWithChunks(moduleLoading, metadata[CHUNKS], nonce); +} + +export function resolveClientReference( + bundlerConfig: SSRModuleMap, + metadata: ClientReferenceMetadata, +): ClientReference { + const moduleExports = bundlerConfig[metadata[ID]]; + let resolvedModuleData = moduleExports[metadata[NAME]]; + let name; + if (resolvedModuleData) { + // The potentially aliased name. + name = resolvedModuleData.name; + } else { + // If we don't have this specific name, we might have the full module. + resolvedModuleData = moduleExports['*']; + if (!resolvedModuleData) { + throw new Error( + 'Could not find the module "' + + metadata[ID] + + '" in the React SSR Manifest. ' + + 'This is probably a bug in the React Server Components bundler.', + ); + } + name = metadata[NAME]; + } + return { + specifier: resolvedModuleData.specifier, + name: name, + async: isAsyncImport(metadata), + }; +} + +export function resolveServerReference( + bundlerConfig: ServerManifest, + id: ServerReferenceId, +): ClientReference { + const idx = id.lastIndexOf('#'); + const specifier = id.slice(0, idx); + const name = id.slice(idx + 1); + return {specifier, name}; +} + +const asyncModuleCache: Map> = new Map(); + +export function preloadModule( + metadata: ClientReference, +): null | Thenable { + const existingPromise = asyncModuleCache.get(metadata.specifier); + if (existingPromise) { + if (existingPromise.status === 'fulfilled') { + return null; + } + return existingPromise; + } else { + // $FlowFixMe[unsupported-syntax] + let modulePromise: Promise = import(metadata.specifier); + if (metadata.async) { + // If the module is async, it must have been a CJS module. + // CJS modules are accessed through the default export in + // Node.js so we have to get the default export to get the + // full module exports. + modulePromise = modulePromise.then(function (value) { + return (value: any).default; + }); + } + modulePromise.then( + value => { + const fulfilledThenable: FulfilledThenable = + (modulePromise: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = value; + }, + reason => { + const rejectedThenable: RejectedThenable = (modulePromise: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = reason; + }, + ); + asyncModuleCache.set(metadata.specifier, modulePromise); + return modulePromise; + } +} + +export function requireModule(metadata: ClientReference): T { + let moduleExports; + // We assume that preloadModule has been called before, which + // should have added something to the module cache. + const promise: any = asyncModuleCache.get(metadata.specifier); + if (promise.status === 'fulfilled') { + moduleExports = promise.value; + } else { + throw promise.reason; + } + if (metadata.name === '*') { + // This is a placeholder value that represents that the caller imported this + // as a CommonJS module as is. + return moduleExports; + } + if (metadata.name === '') { + // This is a placeholder value that represents that the caller accessed the + // default property of this if it was an ESM interop module. + return moduleExports.default; + } + return moduleExports[metadata.name]; +} diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js new file mode 100644 index 0000000000000..b20ad69a408d6 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js @@ -0,0 +1,232 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { + Thenable, + FulfilledThenable, + RejectedThenable, +} from 'shared/ReactTypes'; + +import type { + ImportMetadata, + ImportManifestEntry, +} from './shared/ReactFlightImportMetadata'; +import type {ModuleLoading} from 'react-client/src/ReactFlightClientConfig'; + +import { + ID, + CHUNKS, + NAME, + isAsyncImport, +} from './shared/ReactFlightImportMetadata'; + +import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig'; + +import {loadChunk} from 'react-client/src/ReactFlightClientConfig'; + +export type SSRModuleMap = null | { + [clientId: string]: { + [clientExportName: string]: ClientReferenceManifestEntry, + }, +}; + +export type ServerManifest = { + [id: string]: ImportManifestEntry, +}; + +export type ServerReferenceId = string; + +export opaque type ClientReferenceManifestEntry = ImportManifestEntry; +export opaque type ClientReferenceMetadata = ImportMetadata; + +// eslint-disable-next-line no-unused-vars +export opaque type ClientReference = ClientReferenceMetadata; + +// The reason this function needs to defined here in this file instead of just +// being exported directly from the TurbopackDestination... file is because the +// ClientReferenceMetadata is opaque and we can't unwrap it there. +// This should get inlined and we could also just implement an unwrapping function +// though that risks it getting used in places it shouldn't be. This is unfortunate +// but currently it seems to be the best option we have. +export function prepareDestinationForModule( + moduleLoading: ModuleLoading, + nonce: ?string, + metadata: ClientReferenceMetadata, +) { + prepareDestinationWithChunks(moduleLoading, metadata[CHUNKS], nonce); +} + +export function resolveClientReference( + bundlerConfig: SSRModuleMap, + metadata: ClientReferenceMetadata, +): ClientReference { + if (bundlerConfig) { + const moduleExports = bundlerConfig[metadata[ID]]; + let resolvedModuleData = moduleExports[metadata[NAME]]; + let name; + if (resolvedModuleData) { + // The potentially aliased name. + name = resolvedModuleData.name; + } else { + // If we don't have this specific name, we might have the full module. + resolvedModuleData = moduleExports['*']; + if (!resolvedModuleData) { + throw new Error( + 'Could not find the module "' + + metadata[ID] + + '" in the React SSR Manifest. ' + + 'This is probably a bug in the React Server Components bundler.', + ); + } + name = metadata[NAME]; + } + if (isAsyncImport(metadata)) { + return [ + resolvedModuleData.id, + resolvedModuleData.chunks, + name, + 1 /* async */, + ]; + } else { + return [resolvedModuleData.id, resolvedModuleData.chunks, name]; + } + } + return metadata; +} + +export function resolveServerReference( + bundlerConfig: ServerManifest, + id: ServerReferenceId, +): ClientReference { + let name = ''; + let resolvedModuleData = bundlerConfig[id]; + if (resolvedModuleData) { + // The potentially aliased name. + name = resolvedModuleData.name; + } else { + // We didn't find this specific export name but we might have the * export + // which contains this name as well. + // TODO: It's unfortunate that we now have to parse this string. We should + // probably go back to encoding path and name separately on the client reference. + const idx = id.lastIndexOf('#'); + if (idx !== -1) { + name = id.slice(idx + 1); + resolvedModuleData = bundlerConfig[id.slice(0, idx)]; + } + if (!resolvedModuleData) { + throw new Error( + 'Could not find the module "' + + id + + '" in the React Server Manifest. ' + + 'This is probably a bug in the React Server Components bundler.', + ); + } + } + // TODO: This needs to return async: true if it's an async module. + return [resolvedModuleData.id, resolvedModuleData.chunks, name]; +} + +// The chunk cache contains all the chunks we've preloaded so far. +// If they're still pending they're a thenable. This map also exists +// in Turbopack but unfortunately it's not exposed so we have to +// replicate it in user space. null means that it has already loaded. +const chunkCache: Map> = new Map(); + +function requireAsyncModule(id: string): null | Thenable { + // We've already loaded all the chunks. We can require the module. + const promise = __turbopack_require__(id); + if (typeof promise.then !== 'function') { + // This wasn't a promise after all. + return null; + } else if (promise.status === 'fulfilled') { + // This module was already resolved earlier. + return null; + } else { + // Instrument the Promise to stash the result. + promise.then( + value => { + const fulfilledThenable: FulfilledThenable = (promise: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = value; + }, + reason => { + const rejectedThenable: RejectedThenable = (promise: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = reason; + }, + ); + return promise; + } +} + +function ignoreReject() { + // We rely on rejected promises to be handled by another listener. +} +// Start preloading the modules since we might need them soon. +// This function doesn't suspend. +export function preloadModule( + metadata: ClientReference, +): null | Thenable { + const chunks = metadata[CHUNKS]; + const promises = []; + for (let i = 0; i < chunks.length; i++) { + const chunkFilename = chunks[i]; + const entry = chunkCache.get(chunkFilename); + if (entry === undefined) { + const thenable = loadChunk(chunkFilename); + promises.push(thenable); + // $FlowFixMe[method-unbinding] + const resolve = chunkCache.set.bind(chunkCache, chunkFilename, null); + thenable.then(resolve, ignoreReject); + chunkCache.set(chunkFilename, thenable); + } else if (entry !== null) { + promises.push(entry); + } + } + if (isAsyncImport(metadata)) { + if (promises.length === 0) { + return requireAsyncModule(metadata[ID]); + } else { + return Promise.all(promises).then(() => { + return requireAsyncModule(metadata[ID]); + }); + } + } else if (promises.length > 0) { + return Promise.all(promises); + } else { + return null; + } +} + +// Actually require the module or suspend if it's not yet ready. +// Increase priority if necessary. +export function requireModule(metadata: ClientReference): T { + let moduleExports = __turbopack_require__(metadata[ID]); + if (isAsyncImport(metadata)) { + if (typeof moduleExports.then !== 'function') { + // This wasn't a promise after all. + } else if (moduleExports.status === 'fulfilled') { + // This Promise should've been instrumented by preloadModule. + moduleExports = moduleExports.value; + } else { + throw moduleExports.reason; + } + } + if (metadata[NAME] === '*') { + // This is a placeholder value that represents that the caller imported this + // as a CommonJS module as is. + return moduleExports; + } + if (metadata[NAME] === '') { + // This is a placeholder value that represents that the caller accessed the + // default property of this if it was an ESM interop module. + return moduleExports.__esModule ? moduleExports.default : moduleExports; + } + return moduleExports[metadata[NAME]]; +} diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser.js new file mode 100644 index 0000000000000..c418f51fa80ee --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export function loadChunk(filename: string): Promise { + return __turbopack_load__(filename); +} diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js new file mode 100644 index 0000000000000..c418f51fa80ee --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export function loadChunk(filename: string): Promise { + return __turbopack_load__(filename); +} diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser.js new file mode 100644 index 0000000000000..60b9e87dbea3e --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type ModuleLoading = null; + +export function prepareDestinationWithChunks( + moduleLoading: ModuleLoading, + chunks: mixed, + nonce: ?string, +) { + // In the browser we don't need to prepare our destination since the browser is the Destination +} diff --git a/packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer.js b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer.js new file mode 100644 index 0000000000000..d68f0802aa5f4 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {preinitScriptForSSR} from 'react-client/src/ReactFlightClientConfig'; + +export type ModuleLoading = null | { + prefix: string, + crossOrigin?: 'use-credentials' | '', +}; + +export function prepareDestinationWithChunks( + moduleLoading: ModuleLoading, + // Chunks are single-indexed filenames + chunks: Array, + nonce: ?string, +) { + if (moduleLoading !== null) { + for (let i = 0; i < chunks.length; i++) { + preinitScriptForSSR( + moduleLoading.prefix + chunks[i], + nonce, + moduleLoading.crossOrigin, + ); + } + } +} diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js new file mode 100644 index 0000000000000..64e6b3886adf7 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js @@ -0,0 +1,111 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Thenable} from 'shared/ReactTypes.js'; + +import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient'; + +import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient'; + +import { + createResponse, + getRoot, + reportGlobalError, + processBinaryChunk, + close, +} from 'react-client/src/ReactFlightClient'; + +import { + processReply, + createServerReference, +} from 'react-client/src/ReactFlightReplyClient'; + +type CallServerCallback = (string, args: A) => Promise; + +export type Options = { + callServer?: CallServerCallback, +}; + +function createResponseFromOptions(options: void | Options) { + return createResponse( + null, + null, + options && options.callServer ? options.callServer : undefined, + undefined, // nonce + ); +} + +function startReadingFromStream( + response: FlightResponse, + stream: ReadableStream, +): void { + const reader = stream.getReader(); + function progress({ + done, + value, + }: { + done: boolean, + value: ?any, + ... + }): void | Promise { + if (done) { + close(response); + return; + } + const buffer: Uint8Array = (value: any); + processBinaryChunk(response, buffer); + return reader.read().then(progress).catch(error); + } + function error(e: any) { + reportGlobalError(response, e); + } + reader.read().then(progress).catch(error); +} + +function createFromReadableStream( + stream: ReadableStream, + options?: Options, +): Thenable { + const response: FlightResponse = createResponseFromOptions(options); + startReadingFromStream(response, stream); + return getRoot(response); +} + +function createFromFetch( + promiseForResponse: Promise, + options?: Options, +): Thenable { + const response: FlightResponse = createResponseFromOptions(options); + promiseForResponse.then( + function (r) { + startReadingFromStream(response, (r.body: any)); + }, + function (e) { + reportGlobalError(response, e); + }, + ); + return getRoot(response); +} + +function encodeReply( + value: ReactServerValue, +): Promise< + string | URLSearchParams | FormData, +> /* We don't use URLSearchParams yet but maybe */ { + return new Promise((resolve, reject) => { + processReply(value, '', resolve, reject); + }); +} + +export { + createFromFetch, + createFromReadableStream, + encodeReply, + createServerReference, +}; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js new file mode 100644 index 0000000000000..3b2f7aeea044e --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js @@ -0,0 +1,115 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Thenable} from 'shared/ReactTypes.js'; + +import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient'; + +import type { + SSRModuleMap, + ModuleLoading, +} from 'react-client/src/ReactFlightClientConfig'; + +type SSRManifest = { + moduleMap: SSRModuleMap, + moduleLoading: ModuleLoading, +}; + +import { + createResponse, + getRoot, + reportGlobalError, + processBinaryChunk, + close, +} from 'react-client/src/ReactFlightClient'; + +import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient'; + +function noServerCall() { + throw new Error( + 'Server Functions cannot be called during initial render. ' + + 'This would create a fetch waterfall. Try to use a Server Component ' + + 'to pass data to Client Components instead.', + ); +} + +export function createServerReference, T>( + id: any, + callServer: any, +): (...A) => Promise { + return createServerReferenceImpl(id, noServerCall); +} + +export type Options = { + ssrManifest: SSRManifest, + nonce?: string, +}; + +function createResponseFromOptions(options: Options) { + return createResponse( + options.ssrManifest.moduleMap, + options.ssrManifest.moduleLoading, + noServerCall, + typeof options.nonce === 'string' ? options.nonce : undefined, + ); +} + +function startReadingFromStream( + response: FlightResponse, + stream: ReadableStream, +): void { + const reader = stream.getReader(); + function progress({ + done, + value, + }: { + done: boolean, + value: ?any, + ... + }): void | Promise { + if (done) { + close(response); + return; + } + const buffer: Uint8Array = (value: any); + processBinaryChunk(response, buffer); + return reader.read().then(progress).catch(error); + } + function error(e: any) { + reportGlobalError(response, e); + } + reader.read().then(progress).catch(error); +} + +function createFromReadableStream( + stream: ReadableStream, + options: Options, +): Thenable { + const response: FlightResponse = createResponseFromOptions(options); + startReadingFromStream(response, stream); + return getRoot(response); +} + +function createFromFetch( + promiseForResponse: Promise, + options: Options, +): Thenable { + const response: FlightResponse = createResponseFromOptions(options); + promiseForResponse.then( + function (r) { + startReadingFromStream(response, (r.body: any)); + }, + function (e) { + reportGlobalError(response, e); + }, + ); + return getRoot(response); +} + +export {createFromFetch, createFromReadableStream}; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js new file mode 100644 index 0000000000000..730bc8d61354b --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js @@ -0,0 +1,75 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Thenable} from 'shared/ReactTypes.js'; + +import type {Response} from 'react-client/src/ReactFlightClient'; + +import type { + SSRModuleMap, + ModuleLoading, +} from 'react-client/src/ReactFlightClientConfig'; + +type SSRManifest = { + moduleMap: SSRModuleMap, + moduleLoading: ModuleLoading, +}; + +import type {Readable} from 'stream'; + +import { + createResponse, + getRoot, + reportGlobalError, + processBinaryChunk, + close, +} from 'react-client/src/ReactFlightClient'; + +import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient'; + +function noServerCall() { + throw new Error( + 'Server Functions cannot be called during initial render. ' + + 'This would create a fetch waterfall. Try to use a Server Component ' + + 'to pass data to Client Components instead.', + ); +} +export type Options = { + nonce?: string, +}; + +export function createServerReference, T>( + id: any, + callServer: any, +): (...A) => Promise { + return createServerReferenceImpl(id, noServerCall); +} + +function createFromNodeStream( + stream: Readable, + ssrManifest: SSRManifest, + options?: Options, +): Thenable { + const response: Response = createResponse( + ssrManifest.moduleMap, + ssrManifest.moduleLoading, + noServerCall, + options && typeof options.nonce === 'string' ? options.nonce : undefined, + ); + stream.on('data', chunk => { + processBinaryChunk(response, chunk); + }); + stream.on('error', error => { + reportGlobalError(response, error); + }); + stream.on('end', () => close(response)); + return getRoot(response); +} + +export {createFromNodeStream}; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js new file mode 100644 index 0000000000000..412e697af3ab9 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js @@ -0,0 +1,100 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactClientValue} from 'react-server/src/ReactFlightServer'; +import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes'; +import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler'; +import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; + +import { + createRequest, + startWork, + startFlowing, + abort, +} from 'react-server/src/ReactFlightServer'; + +import { + createResponse, + close, + getRoot, +} from 'react-server/src/ReactFlightReplyServer'; + +import {decodeAction} from 'react-server/src/ReactFlightActionServer'; + +export { + registerServerReference, + registerClientReference, + createClientModuleProxy, +} from './ReactFlightTurbopackReferences'; + +type Options = { + identifierPrefix?: string, + signal?: AbortSignal, + context?: Array<[string, ServerContextJSONValue]>, + onError?: (error: mixed) => void, + onPostpone?: (reason: string) => void, +}; + +function renderToReadableStream( + model: ReactClientValue, + turbopackMap: ClientManifest, + options?: Options, +): ReadableStream { + const request = createRequest( + model, + turbopackMap, + options ? options.onError : undefined, + options ? options.context : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort(request, (signal: any).reason); + } else { + const listener = () => { + abort(request, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + startWork(request); + }, + pull: (controller): ?Promise => { + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => {}, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + return stream; +} + +function decodeReply( + body: string | FormData, + turbopackMap: ServerManifest, +): Thenable { + if (typeof body === 'string') { + const form = new FormData(); + form.append('0', body); + body = form; + } + const response = createResponse(turbopackMap, '', body); + close(response); + return getRoot(response); +} + +export {renderToReadableStream, decodeReply, decodeAction}; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js new file mode 100644 index 0000000000000..412e697af3ab9 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js @@ -0,0 +1,100 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactClientValue} from 'react-server/src/ReactFlightServer'; +import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes'; +import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler'; +import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; + +import { + createRequest, + startWork, + startFlowing, + abort, +} from 'react-server/src/ReactFlightServer'; + +import { + createResponse, + close, + getRoot, +} from 'react-server/src/ReactFlightReplyServer'; + +import {decodeAction} from 'react-server/src/ReactFlightActionServer'; + +export { + registerServerReference, + registerClientReference, + createClientModuleProxy, +} from './ReactFlightTurbopackReferences'; + +type Options = { + identifierPrefix?: string, + signal?: AbortSignal, + context?: Array<[string, ServerContextJSONValue]>, + onError?: (error: mixed) => void, + onPostpone?: (reason: string) => void, +}; + +function renderToReadableStream( + model: ReactClientValue, + turbopackMap: ClientManifest, + options?: Options, +): ReadableStream { + const request = createRequest( + model, + turbopackMap, + options ? options.onError : undefined, + options ? options.context : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort(request, (signal: any).reason); + } else { + const listener = () => { + abort(request, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + startWork(request); + }, + pull: (controller): ?Promise => { + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => {}, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + return stream; +} + +function decodeReply( + body: string | FormData, + turbopackMap: ServerManifest, +): Thenable { + if (typeof body === 'string') { + const form = new FormData(); + form.append('0', body); + body = form; + } + const response = createResponse(turbopackMap, '', body); + close(response); + return getRoot(response); +} + +export {renderToReadableStream, decodeReply, decodeAction}; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMServerNode.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerNode.js new file mode 100644 index 0000000000000..4cfe7a4044d61 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMServerNode.js @@ -0,0 +1,170 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { + Request, + ReactClientValue, +} from 'react-server/src/ReactFlightServer'; +import type {Destination} from 'react-server/src/ReactServerStreamConfigNode'; +import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler'; +import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; +import type {Busboy} from 'busboy'; +import type {Writable} from 'stream'; +import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes'; + +import { + createRequest, + startWork, + startFlowing, + abort, +} from 'react-server/src/ReactFlightServer'; + +import { + createResponse, + reportGlobalError, + close, + resolveField, + resolveFileInfo, + resolveFileChunk, + resolveFileComplete, + getRoot, +} from 'react-server/src/ReactFlightReplyServer'; + +import {decodeAction} from 'react-server/src/ReactFlightActionServer'; + +export { + registerServerReference, + registerClientReference, + createClientModuleProxy, +} from './ReactFlightTurbopackReferences'; + +function createDrainHandler(destination: Destination, request: Request) { + return () => startFlowing(request, destination); +} + +type Options = { + onError?: (error: mixed) => void, + onPostpone?: (reason: string) => void, + context?: Array<[string, ServerContextJSONValue]>, + identifierPrefix?: string, +}; + +type PipeableStream = { + abort(reason: mixed): void, + pipe(destination: T): T, +}; + +function renderToPipeableStream( + model: ReactClientValue, + turbopackMap: ClientManifest, + options?: Options, +): PipeableStream { + const request = createRequest( + model, + turbopackMap, + options ? options.onError : undefined, + options ? options.context : undefined, + options ? options.identifierPrefix : undefined, + options ? options.onPostpone : undefined, + ); + let hasStartedFlowing = false; + startWork(request); + return { + pipe(destination: T): T { + if (hasStartedFlowing) { + throw new Error( + 'React currently only supports piping to one writable stream.', + ); + } + hasStartedFlowing = true; + startFlowing(request, destination); + destination.on('drain', createDrainHandler(destination, request)); + return destination; + }, + abort(reason: mixed) { + abort(request, reason); + }, + }; +} + +function decodeReplyFromBusboy( + busboyStream: Busboy, + turbopackMap: ServerManifest, +): Thenable { + const response = createResponse(turbopackMap, ''); + let pendingFiles = 0; + const queuedFields: Array = []; + busboyStream.on('field', (name, value) => { + if (pendingFiles > 0) { + // Because the 'end' event fires two microtasks after the next 'field' + // we would resolve files and fields out of order. To handle this properly + // we queue any fields we receive until the previous file is done. + queuedFields.push(name, value); + } else { + resolveField(response, name, value); + } + }); + busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => { + if (encoding.toLowerCase() === 'base64') { + throw new Error( + "React doesn't accept base64 encoded file uploads because we don't expect " + + "form data passed from a browser to ever encode data that way. If that's " + + 'the wrong assumption, we can easily fix it.', + ); + } + pendingFiles++; + const file = resolveFileInfo(response, name, filename, mimeType); + value.on('data', chunk => { + resolveFileChunk(response, file, chunk); + }); + value.on('end', () => { + resolveFileComplete(response, name, file); + pendingFiles--; + if (pendingFiles === 0) { + // Release any queued fields + for (let i = 0; i < queuedFields.length; i += 2) { + resolveField(response, queuedFields[i], queuedFields[i + 1]); + } + queuedFields.length = 0; + } + }); + }); + busboyStream.on('finish', () => { + close(response); + }); + busboyStream.on('error', err => { + reportGlobalError( + response, + // $FlowFixMe[incompatible-call] types Error and mixed are incompatible + err, + ); + }); + return getRoot(response); +} + +function decodeReply( + body: string | FormData, + turbopackMap: ServerManifest, +): Thenable { + if (typeof body === 'string') { + const form = new FormData(); + form.append('0', body); + body = form; + } + const response = createResponse(turbopackMap, '', body); + close(response); + return getRoot(response); +} + +export { + renderToPipeableStream, + decodeReplyFromBusboy, + decodeReply, + decodeAction, +}; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler.js b/packages/react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler.js new file mode 100644 index 0000000000000..73a2819a8cebd --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler.js @@ -0,0 +1,93 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactClientValue} from 'react-server/src/ReactFlightServer'; +import type { + ImportMetadata, + ImportManifestEntry, +} from './shared/ReactFlightImportMetadata'; + +import type { + ClientReference, + ServerReference, +} from './ReactFlightTurbopackReferences'; + +export type {ClientReference, ServerReference}; + +export type ClientManifest = { + [id: string]: ClientReferenceManifestEntry, +}; + +export type ServerReferenceId = string; + +export type ClientReferenceMetadata = ImportMetadata; +export opaque type ClientReferenceManifestEntry = ImportManifestEntry; + +export type ClientReferenceKey = string; + +export { + isClientReference, + isServerReference, +} from './ReactFlightTurbopackReferences'; + +export function getClientReferenceKey( + reference: ClientReference, +): ClientReferenceKey { + return reference.$$async ? reference.$$id + '#async' : reference.$$id; +} + +export function resolveClientReferenceMetadata( + config: ClientManifest, + clientReference: ClientReference, +): ClientReferenceMetadata { + const modulePath = clientReference.$$id; + let name = ''; + let resolvedModuleData = config[modulePath]; + if (resolvedModuleData) { + // The potentially aliased name. + name = resolvedModuleData.name; + } else { + // We didn't find this specific export name but we might have the * export + // which contains this name as well. + // TODO: It's unfortunate that we now have to parse this string. We should + // probably go back to encoding path and name separately on the client reference. + const idx = modulePath.lastIndexOf('#'); + if (idx !== -1) { + name = modulePath.slice(idx + 1); + resolvedModuleData = config[modulePath.slice(0, idx)]; + } + if (!resolvedModuleData) { + throw new Error( + 'Could not find the module "' + + modulePath + + '" in the React Client Manifest. ' + + 'This is probably a bug in the React Server Components bundler.', + ); + } + } + if (clientReference.$$async === true) { + return [resolvedModuleData.id, resolvedModuleData.chunks, name, 1]; + } else { + return [resolvedModuleData.id, resolvedModuleData.chunks, name]; + } +} + +export function getServerReferenceId( + config: ClientManifest, + serverReference: ServerReference, +): ServerReferenceId { + return serverReference.$$id; +} + +export function getServerReferenceBoundArguments( + config: ClientManifest, + serverReference: ServerReference, +): null | Array { + return serverReference.$$bound; +} diff --git a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js new file mode 100644 index 0000000000000..066825857f44e --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeLoader.js @@ -0,0 +1,483 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as acorn from 'acorn-loose'; + +type ResolveContext = { + conditions: Array, + parentURL: string | void, +}; + +type ResolveFunction = ( + string, + ResolveContext, + ResolveFunction, +) => {url: string} | Promise<{url: string}>; + +type GetSourceContext = { + format: string, +}; + +type GetSourceFunction = ( + string, + GetSourceContext, + GetSourceFunction, +) => Promise<{source: Source}>; + +type TransformSourceContext = { + format: string, + url: string, +}; + +type TransformSourceFunction = ( + Source, + TransformSourceContext, + TransformSourceFunction, +) => Promise<{source: Source}>; + +type LoadContext = { + conditions: Array, + format: string | null | void, + importAssertions: Object, +}; + +type LoadFunction = ( + string, + LoadContext, + LoadFunction, +) => Promise<{format: string, shortCircuit?: boolean, source: Source}>; + +type Source = string | ArrayBuffer | Uint8Array; + +let warnedAboutConditionsFlag = false; + +let stashedGetSource: null | GetSourceFunction = null; +let stashedResolve: null | ResolveFunction = null; + +export async function resolve( + specifier: string, + context: ResolveContext, + defaultResolve: ResolveFunction, +): Promise<{url: string}> { + // We stash this in case we end up needing to resolve export * statements later. + stashedResolve = defaultResolve; + + if (!context.conditions.includes('react-server')) { + context = { + ...context, + conditions: [...context.conditions, 'react-server'], + }; + if (!warnedAboutConditionsFlag) { + warnedAboutConditionsFlag = true; + // eslint-disable-next-line react-internal/no-production-logging + console.warn( + 'You did not run Node.js with the `--conditions react-server` flag. ' + + 'Any "react-server" override will only work with ESM imports.', + ); + } + } + return await defaultResolve(specifier, context, defaultResolve); +} + +export async function getSource( + url: string, + context: GetSourceContext, + defaultGetSource: GetSourceFunction, +): Promise<{source: Source}> { + // We stash this in case we end up needing to resolve export * statements later. + stashedGetSource = defaultGetSource; + return defaultGetSource(url, context, defaultGetSource); +} + +function addLocalExportedNames(names: Map, node: any) { + switch (node.type) { + case 'Identifier': + names.set(node.name, node.name); + return; + case 'ObjectPattern': + for (let i = 0; i < node.properties.length; i++) + addLocalExportedNames(names, node.properties[i]); + return; + case 'ArrayPattern': + for (let i = 0; i < node.elements.length; i++) { + const element = node.elements[i]; + if (element) addLocalExportedNames(names, element); + } + return; + case 'Property': + addLocalExportedNames(names, node.value); + return; + case 'AssignmentPattern': + addLocalExportedNames(names, node.left); + return; + case 'RestElement': + addLocalExportedNames(names, node.argument); + return; + case 'ParenthesizedExpression': + addLocalExportedNames(names, node.expression); + return; + } +} + +function transformServerModule( + source: string, + body: any, + url: string, + loader: LoadFunction, +): string { + // If the same local name is exported more than once, we only need one of the names. + const localNames: Map = new Map(); + const localTypes: Map = new Map(); + + for (let i = 0; i < body.length; i++) { + const node = body[i]; + switch (node.type) { + case 'ExportAllDeclaration': + // If export * is used, the other file needs to explicitly opt into "use server" too. + break; + case 'ExportDefaultDeclaration': + if (node.declaration.type === 'Identifier') { + localNames.set(node.declaration.name, 'default'); + } else if (node.declaration.type === 'FunctionDeclaration') { + if (node.declaration.id) { + localNames.set(node.declaration.id.name, 'default'); + localTypes.set(node.declaration.id.name, 'function'); + } else { + // TODO: This needs to be rewritten inline because it doesn't have a local name. + } + } + continue; + case 'ExportNamedDeclaration': + if (node.declaration) { + if (node.declaration.type === 'VariableDeclaration') { + const declarations = node.declaration.declarations; + for (let j = 0; j < declarations.length; j++) { + addLocalExportedNames(localNames, declarations[j].id); + } + } else { + const name = node.declaration.id.name; + localNames.set(name, name); + if (node.declaration.type === 'FunctionDeclaration') { + localTypes.set(name, 'function'); + } + } + } + if (node.specifiers) { + const specifiers = node.specifiers; + for (let j = 0; j < specifiers.length; j++) { + const specifier = specifiers[j]; + localNames.set(specifier.local.name, specifier.exported.name); + } + } + continue; + } + } + if (localNames.size === 0) { + return source; + } + let newSrc = source + '\n\n;'; + newSrc += + 'import {registerServerReference} from "react-server-dom-turbopack/server";\n'; + localNames.forEach(function (exported, local) { + if (localTypes.get(local) !== 'function') { + // We first check if the export is a function and if so annotate it. + newSrc += 'if (typeof ' + local + ' === "function") '; + } + newSrc += 'registerServerReference(' + local + ','; + newSrc += JSON.stringify(url) + ','; + newSrc += JSON.stringify(exported) + ');\n'; + }); + return newSrc; +} + +function addExportNames(names: Array, node: any) { + switch (node.type) { + case 'Identifier': + names.push(node.name); + return; + case 'ObjectPattern': + for (let i = 0; i < node.properties.length; i++) + addExportNames(names, node.properties[i]); + return; + case 'ArrayPattern': + for (let i = 0; i < node.elements.length; i++) { + const element = node.elements[i]; + if (element) addExportNames(names, element); + } + return; + case 'Property': + addExportNames(names, node.value); + return; + case 'AssignmentPattern': + addExportNames(names, node.left); + return; + case 'RestElement': + addExportNames(names, node.argument); + return; + case 'ParenthesizedExpression': + addExportNames(names, node.expression); + return; + } +} + +function resolveClientImport( + specifier: string, + parentURL: string, +): {url: string} | Promise<{url: string}> { + // Resolve an import specifier as if it was loaded by the client. This doesn't use + // the overrides that this loader does but instead reverts to the default. + // This resolution algorithm will not necessarily have the same configuration + // as the actual client loader. It should mostly work and if it doesn't you can + // always convert to explicit exported names instead. + const conditions = ['node', 'import']; + if (stashedResolve === null) { + throw new Error( + 'Expected resolve to have been called before transformSource', + ); + } + return stashedResolve(specifier, {conditions, parentURL}, stashedResolve); +} + +async function parseExportNamesInto( + body: any, + names: Array, + parentURL: string, + loader: LoadFunction, +): Promise { + for (let i = 0; i < body.length; i++) { + const node = body[i]; + switch (node.type) { + case 'ExportAllDeclaration': + if (node.exported) { + addExportNames(names, node.exported); + continue; + } else { + const {url} = await resolveClientImport(node.source.value, parentURL); + const {source} = await loader( + url, + {format: 'module', conditions: [], importAssertions: {}}, + loader, + ); + if (typeof source !== 'string') { + throw new Error('Expected the transformed source to be a string.'); + } + let childBody; + try { + childBody = acorn.parse(source, { + ecmaVersion: '2024', + sourceType: 'module', + }).body; + } catch (x) { + // eslint-disable-next-line react-internal/no-production-logging + console.error('Error parsing %s %s', url, x.message); + continue; + } + await parseExportNamesInto(childBody, names, url, loader); + continue; + } + case 'ExportDefaultDeclaration': + names.push('default'); + continue; + case 'ExportNamedDeclaration': + if (node.declaration) { + if (node.declaration.type === 'VariableDeclaration') { + const declarations = node.declaration.declarations; + for (let j = 0; j < declarations.length; j++) { + addExportNames(names, declarations[j].id); + } + } else { + addExportNames(names, node.declaration.id); + } + } + if (node.specifiers) { + const specifiers = node.specifiers; + for (let j = 0; j < specifiers.length; j++) { + addExportNames(names, specifiers[j].exported); + } + } + continue; + } + } +} + +async function transformClientModule( + body: any, + url: string, + loader: LoadFunction, +): Promise { + const names: Array = []; + + await parseExportNamesInto(body, names, url, loader); + + if (names.length === 0) { + return ''; + } + + let newSrc = + 'import {registerClientReference} from "react-server-dom-turbopack/server";\n'; + for (let i = 0; i < names.length; i++) { + const name = names[i]; + if (name === 'default') { + newSrc += 'export default '; + newSrc += 'registerClientReference(function() {'; + newSrc += + 'throw new Error(' + + JSON.stringify( + `Attempted to call the default export of ${url} from the server` + + `but it's on the client. It's not possible to invoke a client function from ` + + `the server, it can only be rendered as a Component or passed to props of a` + + `Client Component.`, + ) + + ');'; + } else { + newSrc += 'export const ' + name + ' = '; + newSrc += 'registerClientReference(function() {'; + newSrc += + 'throw new Error(' + + JSON.stringify( + `Attempted to call ${name}() from the server but ${name} is on the client. ` + + `It's not possible to invoke a client function from the server, it can ` + + `only be rendered as a Component or passed to props of a Client Component.`, + ) + + ');'; + } + newSrc += '},'; + newSrc += JSON.stringify(url) + ','; + newSrc += JSON.stringify(name) + ');\n'; + } + return newSrc; +} + +async function loadClientImport( + url: string, + defaultTransformSource: TransformSourceFunction, +): Promise<{format: string, shortCircuit?: boolean, source: Source}> { + if (stashedGetSource === null) { + throw new Error( + 'Expected getSource to have been called before transformSource', + ); + } + // TODO: Validate that this is another module by calling getFormat. + const {source} = await stashedGetSource( + url, + {format: 'module'}, + stashedGetSource, + ); + const result = await defaultTransformSource( + source, + {format: 'module', url}, + defaultTransformSource, + ); + return {format: 'module', source: result.source}; +} + +async function transformModuleIfNeeded( + source: string, + url: string, + loader: LoadFunction, +): Promise { + // Do a quick check for the exact string. If it doesn't exist, don't + // bother parsing. + if ( + source.indexOf('use client') === -1 && + source.indexOf('use server') === -1 + ) { + return source; + } + + let body; + try { + body = acorn.parse(source, { + ecmaVersion: '2024', + sourceType: 'module', + }).body; + } catch (x) { + // eslint-disable-next-line react-internal/no-production-logging + console.error('Error parsing %s %s', url, x.message); + return source; + } + + let useClient = false; + let useServer = false; + for (let i = 0; i < body.length; i++) { + const node = body[i]; + if (node.type !== 'ExpressionStatement' || !node.directive) { + break; + } + if (node.directive === 'use client') { + useClient = true; + } + if (node.directive === 'use server') { + useServer = true; + } + } + + if (!useClient && !useServer) { + return source; + } + + if (useClient && useServer) { + throw new Error( + 'Cannot have both "use client" and "use server" directives in the same file.', + ); + } + + if (useClient) { + return transformClientModule(body, url, loader); + } + + return transformServerModule(source, body, url, loader); +} + +export async function transformSource( + source: Source, + context: TransformSourceContext, + defaultTransformSource: TransformSourceFunction, +): Promise<{source: Source}> { + const transformed = await defaultTransformSource( + source, + context, + defaultTransformSource, + ); + if (context.format === 'module') { + const transformedSource = transformed.source; + if (typeof transformedSource !== 'string') { + throw new Error('Expected source to have been transformed to a string.'); + } + const newSrc = await transformModuleIfNeeded( + transformedSource, + context.url, + (url: string, ctx: LoadContext, defaultLoad: LoadFunction) => { + return loadClientImport(url, defaultTransformSource); + }, + ); + return {source: newSrc}; + } + return transformed; +} + +export async function load( + url: string, + context: LoadContext, + defaultLoad: LoadFunction, +): Promise<{format: string, shortCircuit?: boolean, source: Source}> { + const result = await defaultLoad(url, context, defaultLoad); + if (result.format === 'module') { + if (typeof result.source !== 'string') { + throw new Error('Expected source to have been loaded into a string.'); + } + const newSrc = await transformModuleIfNeeded( + result.source, + url, + defaultLoad, + ); + return {format: 'module', source: newSrc}; + } + return result; +} diff --git a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js new file mode 100644 index 0000000000000..68c692530d6c5 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js @@ -0,0 +1,110 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +const acorn = require('acorn-loose'); + +const url = require('url'); + +const Module = require('module'); + +module.exports = function register() { + const Server: any = require('react-server-dom-turbopack/server'); + const registerServerReference = Server.registerServerReference; + const createClientModuleProxy = Server.createClientModuleProxy; + + // $FlowFixMe[prop-missing] found when upgrading Flow + const originalCompile = Module.prototype._compile; + + // $FlowFixMe[prop-missing] found when upgrading Flow + Module.prototype._compile = function ( + this: any, + content: string, + filename: string, + ): void { + // Do a quick check for the exact string. If it doesn't exist, don't + // bother parsing. + if ( + content.indexOf('use client') === -1 && + content.indexOf('use server') === -1 + ) { + return originalCompile.apply(this, arguments); + } + + let body; + try { + body = acorn.parse(content, { + ecmaVersion: '2024', + sourceType: 'source', + }).body; + } catch (x) { + // eslint-disable-next-line react-internal/no-production-logging + console.error('Error parsing %s %s', url, x.message); + return originalCompile.apply(this, arguments); + } + + let useClient = false; + let useServer = false; + for (let i = 0; i < body.length; i++) { + const node = body[i]; + if (node.type !== 'ExpressionStatement' || !node.directive) { + break; + } + if (node.directive === 'use client') { + useClient = true; + } + if (node.directive === 'use server') { + useServer = true; + } + } + + if (!useClient && !useServer) { + return originalCompile.apply(this, arguments); + } + + if (useClient && useServer) { + throw new Error( + 'Cannot have both "use client" and "use server" directives in the same file.', + ); + } + + if (useClient) { + const moduleId: string = (url.pathToFileURL(filename).href: any); + this.exports = createClientModuleProxy(moduleId); + } + + if (useServer) { + originalCompile.apply(this, arguments); + + const moduleId: string = (url.pathToFileURL(filename).href: any); + + const exports = this.exports; + + // This module is imported server to server, but opts in to exposing functions by + // reference. If there are any functions in the export. + if (typeof exports === 'function') { + // The module exports a function directly, + registerServerReference( + (exports: any), + moduleId, + // Represents the whole Module object instead of a particular import. + null, + ); + } else { + const keys = Object.keys(exports); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = exports[keys[i]]; + if (typeof value === 'function') { + registerServerReference((value: any), moduleId, key); + } + } + } + } + }; +}; diff --git a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js new file mode 100644 index 0000000000000..9df0e43bd75e1 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js @@ -0,0 +1,256 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactClientValue} from 'react-server/src/ReactFlightServer'; + +export type ServerReference = T & { + $$typeof: symbol, + $$id: string, + $$bound: null | Array, +}; + +// eslint-disable-next-line no-unused-vars +export type ClientReference = { + $$typeof: symbol, + $$id: string, + $$async: boolean, +}; + +const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference'); +const SERVER_REFERENCE_TAG = Symbol.for('react.server.reference'); + +export function isClientReference(reference: Object): boolean { + return reference.$$typeof === CLIENT_REFERENCE_TAG; +} + +export function isServerReference(reference: Object): boolean { + return reference.$$typeof === SERVER_REFERENCE_TAG; +} + +export function registerClientReference( + proxyImplementation: any, + id: string, + exportName: string, +): ClientReference { + return registerClientReferenceImpl( + proxyImplementation, + id + '#' + exportName, + false, + ); +} + +function registerClientReferenceImpl( + proxyImplementation: any, + id: string, + async: boolean, +): ClientReference { + return Object.defineProperties(proxyImplementation, { + $$typeof: {value: CLIENT_REFERENCE_TAG}, + $$id: {value: id}, + $$async: {value: async}, + }); +} + +// $FlowFixMe[method-unbinding] +const FunctionBind = Function.prototype.bind; +// $FlowFixMe[method-unbinding] +const ArraySlice = Array.prototype.slice; +function bind(this: ServerReference) { + // $FlowFixMe[unsupported-syntax] + const newFn = FunctionBind.apply(this, arguments); + if (this.$$typeof === SERVER_REFERENCE_TAG) { + const args = ArraySlice.call(arguments, 1); + newFn.$$typeof = SERVER_REFERENCE_TAG; + newFn.$$id = this.$$id; + newFn.$$bound = this.$$bound ? this.$$bound.concat(args) : args; + } + return newFn; +} + +export function registerServerReference( + reference: ServerReference, + id: string, + exportName: null | string, +): ServerReference { + return Object.defineProperties((reference: any), { + $$typeof: {value: SERVER_REFERENCE_TAG}, + $$id: {value: exportName === null ? id : id + '#' + exportName}, + $$bound: {value: null}, + bind: {value: bind}, + }); +} + +const PROMISE_PROTOTYPE = Promise.prototype; + +const deepProxyHandlers = { + get: function (target: Function, name: string, receiver: Proxy) { + switch (name) { + // These names are read by the Flight runtime if you end up using the exports object. + case '$$typeof': + // These names are a little too common. We should probably have a way to + // have the Flight runtime extract the inner target instead. + return target.$$typeof; + case '$$id': + return target.$$id; + case '$$async': + return target.$$async; + case 'name': + return target.name; + case 'displayName': + return undefined; + // We need to special case this because createElement reads it if we pass this + // reference. + case 'defaultProps': + return undefined; + // Avoid this attempting to be serialized. + case 'toJSON': + return undefined; + case Symbol.toPrimitive: + // $FlowFixMe[prop-missing] + return Object.prototype[Symbol.toPrimitive]; + case 'Provider': + throw new Error( + `Cannot render a Client Context Provider on the Server. ` + + `Instead, you can export a Client Component wrapper ` + + `that itself renders a Client Context Provider.`, + ); + } + // eslint-disable-next-line react-internal/safe-string-coercion + const expression = String(target.name) + '.' + String(name); + throw new Error( + `Cannot access ${expression} on the server. ` + + 'You cannot dot into a client module from a server component. ' + + 'You can only pass the imported name through.', + ); + }, + set: function () { + throw new Error('Cannot assign to a client module from a server module.'); + }, +}; + +const proxyHandlers = { + get: function ( + target: Function, + name: string, + receiver: Proxy, + ): $FlowFixMe { + switch (name) { + // These names are read by the Flight runtime if you end up using the exports object. + case '$$typeof': + return target.$$typeof; + case '$$id': + return target.$$id; + case '$$async': + return target.$$async; + case 'name': + return target.name; + // We need to special case this because createElement reads it if we pass this + // reference. + case 'defaultProps': + return undefined; + // Avoid this attempting to be serialized. + case 'toJSON': + return undefined; + case Symbol.toPrimitive: + // $FlowFixMe[prop-missing] + return Object.prototype[Symbol.toPrimitive]; + case '__esModule': + // Something is conditionally checking which export to use. We'll pretend to be + // an ESM compat module but then we'll check again on the client. + const moduleId = target.$$id; + target.default = registerClientReferenceImpl( + (function () { + throw new Error( + `Attempted to call the default export of ${moduleId} from the server ` + + `but it's on the client. It's not possible to invoke a client function from ` + + `the server, it can only be rendered as a Component or passed to props of a ` + + `Client Component.`, + ); + }: any), + target.$$id + '#', + target.$$async, + ); + return true; + case 'then': + if (target.then) { + // Use a cached value + return target.then; + } + if (!target.$$async) { + // If this module is expected to return a Promise (such as an AsyncModule) then + // we should resolve that with a client reference that unwraps the Promise on + // the client. + + const clientReference: ClientReference = + registerClientReferenceImpl(({}: any), target.$$id, true); + const proxy = new Proxy(clientReference, proxyHandlers); + + // Treat this as a resolved Promise for React's use() + target.status = 'fulfilled'; + target.value = proxy; + + const then = (target.then = registerClientReferenceImpl( + (function then(resolve, reject: any) { + // Expose to React. + return Promise.resolve(resolve(proxy)); + }: any), + // If this is not used as a Promise but is treated as a reference to a `.then` + // export then we should treat it as a reference to that name. + target.$$id + '#then', + false, + )); + return then; + } else { + // Since typeof .then === 'function' is a feature test we'd continue recursing + // indefinitely if we return a function. Instead, we return an object reference + // if we check further. + return undefined; + } + } + let cachedReference = target[name]; + if (!cachedReference) { + const reference: ClientReference = registerClientReferenceImpl( + (function () { + throw new Error( + // eslint-disable-next-line react-internal/safe-string-coercion + `Attempted to call ${String(name)}() from the server but ${String( + name, + )} is on the client. ` + + `It's not possible to invoke a client function from the server, it can ` + + `only be rendered as a Component or passed to props of a Client Component.`, + ); + }: any), + target.$$id + '#' + name, + target.$$async, + ); + Object.defineProperty((reference: any), 'name', {value: name}); + cachedReference = target[name] = new Proxy(reference, deepProxyHandlers); + } + return cachedReference; + }, + getPrototypeOf(target: Function): Object { + // Pretend to be a Promise in case anyone asks. + return PROMISE_PROTOTYPE; + }, + set: function (): empty { + throw new Error('Cannot assign to a client module from a server module.'); + }, +}; + +export function createClientModuleProxy( + moduleId: string, +): ClientReference { + const clientReference: ClientReference = registerClientReferenceImpl( + ({}: any), + // Represents the whole Module object instead of a particular import. + moduleId, + false, + ); + return new Proxy(clientReference, proxyHandlers); +} diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js new file mode 100644 index 0000000000000..dc57bd0e94255 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOM-test.js @@ -0,0 +1,1572 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +// Polyfills for test environment +global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; +global.TextEncoder = require('util').TextEncoder; +global.TextDecoder = require('util').TextDecoder; + +// Don't wait before processing work on the server. +// TODO: we can replace this with FlightServer.act(). +global.setImmediate = cb => cb(); + +let act; +let use; +let clientExports; +let clientModuleError; +let turbopackMap; +let Stream; +let FlightReact; +let React; +let FlightReactDOM; +let ReactDOMClient; +let ReactServerDOMServer; +let ReactServerDOMClient; +let ReactDOMFizzServer; +let Suspense; +let ErrorBoundary; +let JSDOM; + +describe('ReactFlightDOM', () => { + beforeEach(() => { + // For this first reset we are going to load the dom-node version of react-server-dom-turbopack/server + // This can be thought of as essentially being the React Server Components scope with react-server + // condition + jest.resetModules(); + + JSDOM = require('jsdom').JSDOM; + + // Simulate the condition resolution + jest.mock('react-server-dom-turbopack/server', () => + require('react-server-dom-turbopack/server.node.unbundled'), + ); + jest.mock('react', () => require('react/react.shared-subset')); + + const TurbopackMock = require('./utils/TurbopackMock'); + clientExports = TurbopackMock.clientExports; + clientModuleError = TurbopackMock.clientModuleError; + turbopackMap = TurbopackMock.turbopackMap; + + ReactServerDOMServer = require('react-server-dom-turbopack/server'); + FlightReact = require('react'); + FlightReactDOM = require('react-dom'); + + // This reset is to load modules for the SSR/Browser scope. + jest.resetModules(); + jest.unmock('react'); + act = require('internal-test-utils').act; + Stream = require('stream'); + React = require('react'); + use = React.use; + Suspense = React.Suspense; + ReactDOMClient = require('react-dom/client'); + ReactDOMFizzServer = require('react-dom/server.node'); + ReactServerDOMClient = require('react-server-dom-turbopack/client'); + + ErrorBoundary = class extends React.Component { + state = {hasError: false, error: null}; + static getDerivedStateFromError(error) { + return { + hasError: true, + error, + }; + } + render() { + if (this.state.hasError) { + return this.props.fallback(this.state.error); + } + return this.props.children; + } + }; + }); + + function getTestStream() { + const writable = new Stream.PassThrough(); + const readable = new ReadableStream({ + start(controller) { + writable.on('data', chunk => { + controller.enqueue(chunk); + }); + writable.on('end', () => { + controller.close(); + }); + }, + }); + return { + readable, + writable, + }; + } + + const theInfinitePromise = new Promise(() => {}); + function InfiniteSuspend() { + throw theInfinitePromise; + } + + function getMeaningfulChildren(element) { + const children = []; + let node = element.firstChild; + while (node) { + if (node.nodeType === 1) { + if ( + // some tags are ambiguous and might be hidden because they look like non-meaningful children + // so we have a global override where if this data attribute is included we also include the node + node.hasAttribute('data-meaningful') || + (node.tagName === 'SCRIPT' && + node.hasAttribute('src') && + node.hasAttribute('async')) || + (node.tagName !== 'SCRIPT' && + node.tagName !== 'TEMPLATE' && + node.tagName !== 'template' && + !node.hasAttribute('hidden') && + !node.hasAttribute('aria-hidden')) + ) { + const props = {}; + const attributes = node.attributes; + for (let i = 0; i < attributes.length; i++) { + if ( + attributes[i].name === 'id' && + attributes[i].value.includes(':') + ) { + // We assume this is a React added ID that's a non-visual implementation detail. + continue; + } + props[attributes[i].name] = attributes[i].value; + } + props.children = getMeaningfulChildren(node); + children.push(React.createElement(node.tagName.toLowerCase(), props)); + } + } else if (node.nodeType === 3) { + children.push(node.data); + } + node = node.nextSibling; + } + return children.length === 0 + ? undefined + : children.length === 1 + ? children[0] + : children; + } + + it('should resolve HTML using Node streams', async () => { + function Text({children}) { + return {children}; + } + function HTML() { + return ( +
+ hello + world +
+ ); + } + + function App() { + const model = { + html: , + }; + return model; + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + const model = await response; + expect(model).toEqual({ + html: ( +
+ hello + world +
+ ), + }); + }); + + it('should resolve the root', async () => { + // Model + function Text({children}) { + return {children}; + } + function HTML() { + return ( +
+ hello + world +
+ ); + } + function RootModel() { + return { + html: , + }; + } + + // View + function Message({response}) { + return
{use(response).html}
; + } + function App({response}) { + return ( + Loading...}> + + + ); + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe( + '
helloworld
', + ); + }); + + it('should not get confused by $', async () => { + // Model + function RootModel() { + return {text: '$1'}; + } + + // View + function Message({response}) { + return

{use(response).text}

; + } + function App({response}) { + return ( + Loading...}> + + + ); + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

$1

'); + }); + + it('should not get confused by @', async () => { + // Model + function RootModel() { + return {text: '@div'}; + } + + // View + function Message({response}) { + return

{use(response).text}

; + } + function App({response}) { + return ( + Loading...}> + + + ); + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

@div

'); + }); + + it('should be able to esm compat test module references', async () => { + const ESMCompatModule = { + __esModule: true, + default: function ({greeting}) { + return greeting + ' World'; + }, + hi: 'Hello', + }; + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + function interopWebpack(obj) { + // Basically what Webpack's ESM interop feature testing does. + if (typeof obj === 'object' && obj.__esModule) { + return obj; + } + return Object.assign({default: obj}, obj); + } + + const {default: Component, hi} = interopWebpack( + clientExports(ESMCompatModule), + ); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

Hello World

'); + }); + + it('should be able to render a named component export', async () => { + const Module = { + Component: function ({greeting}) { + return greeting + ' World'; + }, + }; + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const {Component} = clientExports(Module); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

Hello World

'); + }); + + it('should be able to render a module split named component export', async () => { + const Module = { + // This gets split into a separate module from the original one. + split: function ({greeting}) { + return greeting + ' World'; + }, + }; + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const {split: Component} = clientExports(Module); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

Hello World

'); + }); + + it('should unwrap async module references', async () => { + const AsyncModule = Promise.resolve(function AsyncModule({text}) { + return 'Async: ' + text; + }); + + const AsyncModule2 = Promise.resolve({ + exportName: 'Module', + }); + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const AsyncModuleRef = await clientExports(AsyncModule); + const AsyncModuleRef2 = await clientExports(AsyncModule2); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

Async: Module

'); + }); + + it('should unwrap async module references using use', async () => { + const AsyncModule = Promise.resolve('Async Text'); + + function Print({response}) { + return use(response); + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const AsyncModuleRef = clientExports(AsyncModule); + + function ServerComponent() { + const text = FlightReact.use(AsyncModuleRef); + return

{text}

; + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

Async Text

'); + }); + + it('should be able to import a name called "then"', async () => { + const thenExports = { + then: function then() { + return 'and then'; + }, + }; + + function Print({response}) { + return

{use(response)}

; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const ThenRef = clientExports(thenExports).then; + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

and then

'); + }); + + it('throws when accessing a member below the client exports', () => { + const ClientModule = clientExports({ + Component: {deep: 'thing'}, + }); + function dotting() { + return ClientModule.Component.deep; + } + expect(dotting).toThrowError( + 'Cannot access Component.deep on the server. ' + + 'You cannot dot into a client module from a server component. ' + + 'You can only pass the imported name through.', + ); + }); + + it('does not throw when React inspects any deep props', () => { + const ClientModule = clientExports({ + Component: function () {}, + }); + ; + }); + + it('throws when accessing a Context.Provider below the client exports', () => { + const Context = React.createContext(); + const ClientModule = clientExports({ + Context, + }); + function dotting() { + return ClientModule.Context.Provider; + } + expect(dotting).toThrowError( + `Cannot render a Client Context Provider on the Server. ` + + `Instead, you can export a Client Component wrapper ` + + `that itself renders a Client Context Provider.`, + ); + }); + + it('should progressively reveal server components', async () => { + let reportedErrors = []; + + // Client Components + + function MyErrorBoundary({children}) { + return ( + ( +

+ {__DEV__ ? e.message + ' + ' : null} + {e.digest} +

+ )}> + {children} +
+ ); + } + + // Model + function Text({children}) { + return children; + } + + function makeDelayedText() { + let _resolve, _reject; + let promise = new Promise((resolve, reject) => { + _resolve = () => { + promise = null; + resolve(); + }; + _reject = e => { + promise = null; + reject(e); + }; + }); + async function DelayedText({children}) { + await promise; + return {children}; + } + return [DelayedText, _resolve, _reject]; + } + + const [Friends, resolveFriends] = makeDelayedText(); + const [Name, resolveName] = makeDelayedText(); + const [Posts, resolvePosts] = makeDelayedText(); + const [Photos, resolvePhotos] = makeDelayedText(); + const [Games, , rejectGames] = makeDelayedText(); + + // View + function ProfileDetails({avatar}) { + return ( +
+ :name: + {avatar} +
+ ); + } + function ProfileSidebar({friends}) { + return ( +
+ :photos: + {friends} +
+ ); + } + function ProfilePosts({posts}) { + return
{posts}
; + } + function ProfileGames({games}) { + return
{games}
; + } + + const MyErrorBoundaryClient = clientExports(MyErrorBoundary); + + function ProfileContent() { + return ( + <> + :avatar:} /> + (loading sidebar)

}> + :friends:} /> +
+ (loading posts)

}> + :posts:} /> +
+ + (loading games)

}> + :games:} /> +
+
+ + ); + } + + const model = { + rootContent: , + }; + + function ProfilePage({response}) { + return use(response).rootContent; + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + model, + turbopackMap, + { + onError(x) { + reportedErrors.push(x); + return __DEV__ ? 'a dev digest' : `digest("${x.message}")`; + }, + }, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + (loading)

}> + +
, + ); + }); + expect(container.innerHTML).toBe('

(loading)

'); + + // This isn't enough to show anything. + await act(() => { + resolveFriends(); + }); + expect(container.innerHTML).toBe('

(loading)

'); + + // We can now show the details. Sidebar and posts are still loading. + await act(() => { + resolveName(); + }); + // Advance time enough to trigger a nested fallback. + await act(() => { + jest.advanceTimersByTime(500); + }); + expect(container.innerHTML).toBe( + '
:name::avatar:
' + + '

(loading sidebar)

' + + '

(loading posts)

' + + '

(loading games)

', + ); + + expect(reportedErrors).toEqual([]); + + const theError = new Error('Game over'); + // Let's *fail* loading games. + await act(async () => { + await rejectGames(theError); + await 'the inner async function'; + }); + const expectedGamesValue = __DEV__ + ? '

Game over + a dev digest

' + : '

digest("Game over")

'; + expect(container.innerHTML).toBe( + '
:name::avatar:
' + + '

(loading sidebar)

' + + '

(loading posts)

' + + expectedGamesValue, + ); + + expect(reportedErrors).toEqual([theError]); + reportedErrors = []; + + // We can now show the sidebar. + await act(async () => { + await resolvePhotos(); + await 'the inner async function'; + }); + expect(container.innerHTML).toBe( + '
:name::avatar:
' + + '
:photos::friends:
' + + '

(loading posts)

' + + expectedGamesValue, + ); + + // Show everything. + await act(async () => { + await resolvePosts(); + await 'the inner async function'; + }); + expect(container.innerHTML).toBe( + '
:name::avatar:
' + + '
:photos::friends:
' + + '
:posts:
' + + expectedGamesValue, + ); + + expect(reportedErrors).toEqual([]); + }); + + it('should preserve state of client components on refetch', async () => { + // Client + + function Page({response}) { + return use(response); + } + + function Input() { + return ; + } + + const InputClient = clientExports(Input); + + // Server + + function App({color}) { + // Verify both DOM and Client children. + return ( +
+ + +
+ ); + } + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + const stream1 = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + pipe(stream1.writable); + const response1 = ReactServerDOMClient.createFromReadableStream( + stream1.readable, + ); + await act(() => { + root.render( + (loading)

}> + +
, + ); + }); + expect(container.children.length).toBe(1); + expect(container.children[0].tagName).toBe('DIV'); + expect(container.children[0].style.color).toBe('red'); + + // Change the DOM state for both inputs. + const inputA = container.children[0].children[0]; + expect(inputA.tagName).toBe('INPUT'); + inputA.value = 'hello'; + const inputB = container.children[0].children[1]; + expect(inputB.tagName).toBe('INPUT'); + inputB.value = 'goodbye'; + + const stream2 = getTestStream(); + const {pipe: pipe2} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + pipe2(stream2.writable); + const response2 = ReactServerDOMClient.createFromReadableStream( + stream2.readable, + ); + await act(() => { + root.render( + (loading)

}> + +
, + ); + }); + expect(container.children.length).toBe(1); + expect(container.children[0].tagName).toBe('DIV'); + expect(container.children[0].style.color).toBe('blue'); + + // Verify we didn't destroy the DOM for either input. + expect(inputA === container.children[0].children[0]).toBe(true); + expect(inputA.tagName).toBe('INPUT'); + expect(inputA.value).toBe('hello'); + expect(inputB === container.children[0].children[1]).toBe(true); + expect(inputB.tagName).toBe('INPUT'); + expect(inputB.value).toBe('goodbye'); + }); + + it('should be able to complete after aborting and throw the reason client-side', async () => { + const reportedErrors = []; + + const {writable, readable} = getTestStream(); + const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream( +
+ +
, + turbopackMap, + { + onError(x) { + reportedErrors.push(x); + const message = typeof x === 'string' ? x : x.message; + return __DEV__ ? 'a dev digest' : `digest("${message}")`; + }, + }, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + function App({res}) { + return use(res); + } + + await act(() => { + root.render( + ( +

+ {__DEV__ ? e.message + ' + ' : null} + {e.digest} +

+ )}> + (loading)

}> + +
+
, + ); + }); + expect(container.innerHTML).toBe('

(loading)

'); + + await act(() => { + abort('for reasons'); + }); + if (__DEV__) { + expect(container.innerHTML).toBe( + '

Error: for reasons + a dev digest

', + ); + } else { + expect(container.innerHTML).toBe('

digest("for reasons")

'); + } + + expect(reportedErrors).toEqual(['for reasons']); + }); + + it('should be able to recover from a direct reference erroring client-side', async () => { + const reportedErrors = []; + + const ClientComponent = clientExports(function ({prop}) { + return 'This should never render'; + }); + + const ClientReference = clientModuleError(new Error('module init error')); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( +
+ +
, + turbopackMap, + { + onError(x) { + reportedErrors.push(x); + }, + }, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + function App({res}) { + return use(res); + } + + await act(() => { + root.render( +

{e.message}

}> + (loading)

}> + +
+
, + ); + }); + expect(container.innerHTML).toBe('

module init error

'); + + expect(reportedErrors).toEqual([]); + }); + + it('should be able to recover from a direct reference erroring client-side async', async () => { + const reportedErrors = []; + + const ClientComponent = clientExports(function ({prop}) { + return 'This should never render'; + }); + + let rejectPromise; + const ClientReference = await clientExports( + new Promise((resolve, reject) => { + rejectPromise = reject; + }), + ); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( +
+ +
, + turbopackMap, + { + onError(x) { + reportedErrors.push(x); + }, + }, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + function App({res}) { + return use(res); + } + + await act(() => { + root.render( +

{e.message}

}> + (loading)

}> + +
+
, + ); + }); + + expect(container.innerHTML).toBe('

(loading)

'); + + await act(() => { + rejectPromise(new Error('async module init error')); + }); + + expect(container.innerHTML).toBe('

async module init error

'); + + expect(reportedErrors).toEqual([]); + }); + + it('should be able to recover from a direct reference erroring server-side', async () => { + const reportedErrors = []; + + const ClientComponent = clientExports(function ({prop}) { + return 'This should never render'; + }); + + // We simulate a bug in the Webpack bundler which causes an error on the server. + for (const id in turbopackMap) { + Object.defineProperty(turbopackMap, id, { + get: () => { + throw new Error('bug in the bundler'); + }, + }); + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( +
+ +
, + turbopackMap, + { + onError(x) { + reportedErrors.push(x.message); + return __DEV__ ? 'a dev digest' : `digest("${x.message}")`; + }, + }, + ); + pipe(writable); + + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + function App({res}) { + return use(res); + } + + await act(() => { + root.render( + ( +

+ {__DEV__ ? e.message + ' + ' : null} + {e.digest} +

+ )}> + (loading)

}> + +
+
, + ); + }); + if (__DEV__) { + expect(container.innerHTML).toBe( + '

bug in the bundler + a dev digest

', + ); + } else { + expect(container.innerHTML).toBe('

digest("bug in the bundler")

'); + } + + expect(reportedErrors).toEqual(['bug in the bundler']); + }); + + it('should pass a Promise through props and be able use() it on the client', async () => { + async function getData() { + return 'async hello'; + } + + function Component({data}) { + const text = use(data); + return

{text}

; + } + + const ClientComponent = clientExports(Component); + + function ServerComponent() { + const data = getData(); // no await here + return ; + } + + function Print({response}) { + return use(response); + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

async hello

'); + }); + + it('should throw on the client if a passed promise eventually rejects', async () => { + const reportedErrors = []; + const theError = new Error('Server throw'); + + async function getData() { + throw theError; + } + + function Component({data}) { + const text = use(data); + return

{text}

; + } + + const ClientComponent = clientExports(Component); + + function ServerComponent() { + const data = getData(); // no await here + return ; + } + + function Await({response}) { + return use(response); + } + + function App({response}) { + return ( + Loading...}> + ( +

+ {__DEV__ ? e.message + ' + ' : null} + {e.digest} +

+ )}> + +
+
+ ); + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + { + onError(x) { + reportedErrors.push(x); + return __DEV__ ? 'a dev digest' : `digest("${x.message}")`; + }, + }, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe( + __DEV__ + ? '

Server throw + a dev digest

' + : '

digest("Server throw")

', + ); + expect(reportedErrors).toEqual([theError]); + }); + + it('should support float methods when rendering in Fiber', async () => { + function Component() { + return

hello world

; + } + + const ClientComponent = clientExports(Component); + + async function ServerComponent() { + FlightReactDOM.prefetchDNS('d before'); + FlightReactDOM.preconnect('c before'); + FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'}); + FlightReactDOM.preload('l before', {as: 'style'}); + FlightReactDOM.preloadModule('lm before'); + FlightReactDOM.preloadModule('lm2 before', {crossOrigin: 'anonymous'}); + FlightReactDOM.preinit('i before', {as: 'script'}); + FlightReactDOM.preinitModule('m before'); + FlightReactDOM.preinitModule('m2 before', {crossOrigin: 'anonymous'}); + await 1; + FlightReactDOM.prefetchDNS('d after'); + FlightReactDOM.preconnect('c after'); + FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'}); + FlightReactDOM.preload('l after', {as: 'style'}); + FlightReactDOM.preloadModule('lm after'); + FlightReactDOM.preloadModule('lm2 after', {crossOrigin: 'anonymous'}); + FlightReactDOM.preinit('i after', {as: 'script'}); + FlightReactDOM.preinitModule('m after'); + FlightReactDOM.preinitModule('m2 after', {crossOrigin: 'anonymous'}); + return ; + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + pipe(writable); + + let response = null; + function getResponse() { + if (response === null) { + response = ReactServerDOMClient.createFromReadableStream(readable); + } + return response; + } + + function App() { + return getResponse(); + } + + // We pause to allow the float call after the await point to process before the + // HostDispatcher gets set for Fiber by createRoot. This is only needed in testing + // because the module graphs are not different and the HostDispatcher is shared. + // In a real environment the Fiber and Flight code would each have their own independent + // dispatcher. + // @TODO consider what happens when Server-Components-On-The-Client exist. we probably + // want to use the Fiber HostDispatcher there too since it is more about the host than the runtime + // but we need to make sure that actually makes sense + await 1; + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + + + Client Component', + ); + }); + + it('should encode long string in a compact format', async () => { + const testString = '"\n\t'.repeat(500) + '🙃'; + + const stream = ReactServerDOMServer.renderToPipeableStream({ + text: testString, + }); + + const readable = new Stream.PassThrough(); + + const stringResult = readResult(readable); + const parsedResult = ReactServerDOMClient.createFromNodeStream(readable, { + moduleMap: turbopackMap, + moduleLoading: turbopackModuleLoading, + }); + + stream.pipe(readable); + + const serializedContent = await stringResult; + // The content should be compact an unescaped + expect(serializedContent.length).toBeLessThan(2000); + expect(serializedContent).not.toContain('\\n'); + expect(serializedContent).not.toContain('\\t'); + expect(serializedContent).not.toContain('\\"'); + expect(serializedContent).toContain('\t'); + + const result = await parsedResult; + // Should still match the result when parsed + expect(result.text).toBe(testString); + }); + + // @gate enableBinaryFlight + it('should be able to serialize any kind of typed array', async () => { + const buffer = new Uint8Array([ + 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, + ]).buffer; + const buffers = [ + buffer, + new Int8Array(buffer, 1), + new Uint8Array(buffer, 2), + new Uint8ClampedArray(buffer, 2), + new Int16Array(buffer, 2), + new Uint16Array(buffer, 2), + new Int32Array(buffer, 4), + new Uint32Array(buffer, 4), + new Float32Array(buffer, 4), + new Float64Array(buffer, 0), + new BigInt64Array(buffer, 0), + new BigUint64Array(buffer, 0), + new DataView(buffer, 3), + ]; + const stream = ReactServerDOMServer.renderToPipeableStream(buffers); + const readable = new Stream.PassThrough(); + const promise = ReactServerDOMClient.createFromNodeStream(readable, { + moduleMap: turbopackMap, + moduleLoading: turbopackModuleLoading, + }); + stream.pipe(readable); + const result = await promise; + expect(result).toEqual(buffers); + }); + + it('should allow accept a nonce option for Flight preinitialized scripts', async () => { + function ClientComponent() { + return Client Component; + } + // The Client build may not have the same IDs as the Server bundles for the same + // component. + const ClientComponentOnTheClient = clientExports( + ClientComponent, + 'path/to/chunk.js', + ); + const ClientComponentOnTheServer = clientExports(ClientComponent); + + // In the SSR bundle this module won't exist. We simulate this by deleting it. + const clientId = turbopackMap[ClientComponentOnTheClient.$$id].id; + delete turbopackModules[clientId]; + + // Instead, we have to provide a translation from the client meta data to the SSR + // meta data. + const ssrMetadata = turbopackMap[ClientComponentOnTheServer.$$id]; + const translationMap = { + [clientId]: { + '*': ssrMetadata, + }, + }; + const ssrManifest = { + moduleMap: translationMap, + moduleLoading: turbopackModuleLoading, + }; + + function App() { + return ; + } + + const stream = ReactServerDOMServer.renderToPipeableStream( + , + turbopackMap, + ); + const readable = new Stream.PassThrough(); + let response; + + stream.pipe(readable); + + function ClientRoot() { + if (response) return use(response); + response = ReactServerDOMClient.createFromNodeStream( + readable, + ssrManifest, + { + nonce: 'r4nd0m', + }, + ); + return use(response); + } + + const ssrStream = await ReactDOMServer.renderToPipeableStream( + , + ); + const result = await readResult(ssrStream); + expect(result).toEqual( + 'Client Component', + ); + }); +}); diff --git a/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMReply-test.js b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMReply-test.js new file mode 100644 index 0000000000000..d6c4c318d7b38 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/__tests__/ReactFlightDOMReply-test.js @@ -0,0 +1,232 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +// Polyfills for test environment +global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; +global.TextEncoder = require('util').TextEncoder; +global.TextDecoder = require('util').TextDecoder; + +// let serverExports; +let turbopackServerMap; +let ReactServerDOMServer; +let ReactServerDOMClient; + +describe('ReactFlightDOMReply', () => { + beforeEach(() => { + jest.resetModules(); + // Simulate the condition resolution + jest.mock('react-server-dom-turbopack/server', () => + require('react-server-dom-turbopack/server.browser'), + ); + const TurbopackMock = require('./utils/TurbopackMock'); + // serverExports = TurbopackMock.serverExports; + turbopackServerMap = TurbopackMock.turbopackServerMap; + ReactServerDOMServer = require('react-server-dom-turbopack/server.browser'); + ReactServerDOMClient = require('react-server-dom-turbopack/client'); + }); + + // This method should exist on File but is not implemented in JSDOM + async function arrayBuffer(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = function () { + return resolve(reader.result); + }; + reader.onerror = function () { + return reject(reader.error); + }; + reader.readAsArrayBuffer(file); + }); + } + + it('can pass undefined as a reply', async () => { + const body = await ReactServerDOMClient.encodeReply(undefined); + const missing = await ReactServerDOMServer.decodeReply( + body, + turbopackServerMap, + ); + expect(missing).toBe(undefined); + + const body2 = await ReactServerDOMClient.encodeReply({ + array: [undefined, null, undefined], + prop: undefined, + }); + const object = await ReactServerDOMServer.decodeReply( + body2, + turbopackServerMap, + ); + expect(object.array.length).toBe(3); + expect(object.array[0]).toBe(undefined); + expect(object.array[1]).toBe(null); + expect(object.array[3]).toBe(undefined); + expect(object.prop).toBe(undefined); + // These should really be true but our deserialization doesn't currently deal with it. + expect('3' in object.array).toBe(false); + expect('prop' in object).toBe(false); + }); + + it('can pass an iterable as a reply', async () => { + const body = await ReactServerDOMClient.encodeReply({ + [Symbol.iterator]: function* () { + yield 'A'; + yield 'B'; + yield 'C'; + }, + }); + const iterable = await ReactServerDOMServer.decodeReply( + body, + turbopackServerMap, + ); + const items = []; + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const item of iterable) { + items.push(item); + } + expect(items).toEqual(['A', 'B', 'C']); + }); + + it('can pass weird numbers as a reply', async () => { + const nums = [0, -0, Infinity, -Infinity, NaN]; + const body = await ReactServerDOMClient.encodeReply(nums); + const nums2 = await ReactServerDOMServer.decodeReply( + body, + turbopackServerMap, + ); + + expect(nums).toEqual(nums2); + expect(nums.every((n, i) => Object.is(n, nums2[i]))).toBe(true); + }); + + it('can pass a BigInt as a reply', async () => { + const body = await ReactServerDOMClient.encodeReply(90071992547409910000n); + const n = await ReactServerDOMServer.decodeReply(body, turbopackServerMap); + + expect(n).toEqual(90071992547409910000n); + }); + + it('can pass FormData as a reply', async () => { + const formData = new FormData(); + formData.set('hello', 'world'); + formData.append('list', '1'); + formData.append('list', '2'); + formData.append('list', '3'); + const typedArray = new Uint8Array([0, 1, 2, 3]); + const blob = new Blob([typedArray]); + formData.append('blob', blob, 'filename.blob'); + + const body = await ReactServerDOMClient.encodeReply(formData); + const formData2 = await ReactServerDOMServer.decodeReply( + body, + turbopackServerMap, + ); + + expect(formData2).not.toBe(formData); + expect(Array.from(formData2).length).toBe(5); + expect(formData2.get('hello')).toBe('world'); + expect(formData2.getAll('list')).toEqual(['1', '2', '3']); + const blob2 = formData.get('blob'); + expect(blob2.size).toBe(4); + expect(blob2.name).toBe('filename.blob'); + expect(blob2.type).toBe(''); + const typedArray2 = new Uint8Array(await arrayBuffer(blob2)); + expect(typedArray2).toEqual(typedArray); + }); + + it('can pass multiple Files in FormData', async () => { + const typedArrayA = new Uint8Array([0, 1, 2, 3]); + const typedArrayB = new Uint8Array([4, 5]); + const blobA = new Blob([typedArrayA]); + const blobB = new Blob([typedArrayB]); + const formData = new FormData(); + formData.append('filelist', 'string'); + formData.append('filelist', blobA); + formData.append('filelist', blobB); + + const body = await ReactServerDOMClient.encodeReply(formData); + const formData2 = await ReactServerDOMServer.decodeReply( + body, + turbopackServerMap, + ); + + const filelist2 = formData2.getAll('filelist'); + expect(filelist2.length).toBe(3); + expect(filelist2[0]).toBe('string'); + const blobA2 = filelist2[1]; + expect(blobA2.size).toBe(4); + expect(blobA2.name).toBe('blob'); + expect(blobA2.type).toBe(''); + const typedArrayA2 = new Uint8Array(await arrayBuffer(blobA2)); + expect(typedArrayA2).toEqual(typedArrayA); + const blobB2 = filelist2[2]; + expect(blobB2.size).toBe(2); + expect(blobB2.name).toBe('blob'); + expect(blobB2.type).toBe(''); + const typedArrayB2 = new Uint8Array(await arrayBuffer(blobB2)); + expect(typedArrayB2).toEqual(typedArrayB); + }); + + it('can pass two independent FormData with same keys', async () => { + const formDataA = new FormData(); + formDataA.set('greeting', 'hello'); + const formDataB = new FormData(); + formDataB.set('greeting', 'hi'); + + const body = await ReactServerDOMClient.encodeReply({ + a: formDataA, + b: formDataB, + }); + const {a: formDataA2, b: formDataB2} = + await ReactServerDOMServer.decodeReply(body, turbopackServerMap); + + expect(Array.from(formDataA2).length).toBe(1); + expect(Array.from(formDataB2).length).toBe(1); + expect(formDataA2.get('greeting')).toBe('hello'); + expect(formDataB2.get('greeting')).toBe('hi'); + }); + + it('can pass a Date as a reply', async () => { + const d = new Date(1234567890123); + const body = await ReactServerDOMClient.encodeReply(d); + const d2 = await ReactServerDOMServer.decodeReply(body, turbopackServerMap); + + expect(d).toEqual(d2); + expect(d % 1000).toEqual(123); // double-check the milliseconds made it through + }); + + it('can pass a Map as a reply', async () => { + const objKey = {obj: 'key'}; + const m = new Map([ + ['hi', {greet: 'world'}], + [objKey, 123], + ]); + const body = await ReactServerDOMClient.encodeReply(m); + const m2 = await ReactServerDOMServer.decodeReply(body, turbopackServerMap); + + expect(m2 instanceof Map).toBe(true); + expect(m2.size).toBe(2); + expect(m2.get('hi').greet).toBe('world'); + expect(m2).toEqual(m); + }); + + it('can pass a Set as a reply', async () => { + const objKey = {obj: 'key'}; + const s = new Set(['hi', objKey]); + + const body = await ReactServerDOMClient.encodeReply(s); + const s2 = await ReactServerDOMServer.decodeReply(body, turbopackServerMap); + + expect(s2 instanceof Set).toBe(true); + expect(s2.size).toBe(2); + expect(s2.has('hi')).toBe(true); + expect(s2).toEqual(s); + }); +}); diff --git a/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js b/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js new file mode 100644 index 0000000000000..7b10593220b60 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/__tests__/utils/TurbopackMock.js @@ -0,0 +1,148 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const url = require('url'); +const Module = require('module'); + +let turbopackModuleIdx = 0; +const turbopackServerModules = {}; +const turbopackClientModules = {}; +const turbopackErroredModules = {}; +const turbopackServerMap = {}; +const turbopackClientMap = {}; +global.__turbopack_require__ = function (id) { + if (turbopackErroredModules[id]) { + throw turbopackErroredModules[id]; + } + return turbopackClientModules[id] || turbopackServerModules[id]; +}; + +const previousCompile = Module.prototype._compile; + +const register = require('react-server-dom-turbopack/node-register'); +// Register node compile +register(); + +const nodeCompile = Module.prototype._compile; + +if (previousCompile === nodeCompile) { + throw new Error( + 'Expected the Node loader to register the _compile extension', + ); +} + +Module.prototype._compile = previousCompile; + +exports.turbopackMap = turbopackClientMap; +exports.turbopackModules = turbopackClientModules; +exports.turbopackServerMap = turbopackServerMap; +exports.moduleLoading = { + prefix: '/prefix/', +}; + +exports.clientModuleError = function clientModuleError(moduleError) { + const idx = '' + turbopackModuleIdx++; + turbopackErroredModules[idx] = moduleError; + const path = url.pathToFileURL(idx).href; + turbopackClientMap[path] = { + id: idx, + chunks: [], + name: '*', + }; + const mod = {exports: {}}; + nodeCompile.call(mod, '"use client"', idx); + return mod.exports; +}; + +exports.clientExports = function clientExports(moduleExports, chunkUrl) { + const chunks = []; + if (chunkUrl !== undefined) { + chunks.push(chunkUrl); + } + const idx = '' + turbopackModuleIdx++; + turbopackClientModules[idx] = moduleExports; + const path = url.pathToFileURL(idx).href; + turbopackClientMap[path] = { + id: idx, + chunks, + name: '*', + }; + // We only add this if this test is testing ESM compat. + if ('__esModule' in moduleExports) { + turbopackClientMap[path + '#'] = { + id: idx, + chunks, + name: '', + }; + } + if (typeof moduleExports.then === 'function') { + moduleExports.then( + asyncModuleExports => { + for (const name in asyncModuleExports) { + turbopackClientMap[path + '#' + name] = { + id: idx, + chunks, + name: name, + }; + } + }, + () => {}, + ); + } + if ('split' in moduleExports) { + // If we're testing module splitting, we encode this name in a separate module id. + const splitIdx = '' + turbopackModuleIdx++; + turbopackClientModules[splitIdx] = { + s: moduleExports.split, + }; + turbopackClientMap[path + '#split'] = { + id: splitIdx, + chunks, + name: 's', + }; + } + const mod = {exports: {}}; + nodeCompile.call(mod, '"use client"', idx); + return mod.exports; +}; + +// This tests server to server references. There's another case of client to server references. +exports.serverExports = function serverExports(moduleExports) { + const idx = '' + turbopackModuleIdx++; + turbopackServerModules[idx] = moduleExports; + const path = url.pathToFileURL(idx).href; + turbopackServerMap[path] = { + id: idx, + chunks: [], + name: '*', + }; + // We only add this if this test is testing ESM compat. + if ('__esModule' in moduleExports) { + turbopackServerMap[path + '#'] = { + id: idx, + chunks: [], + name: '', + }; + } + if ('split' in moduleExports) { + // If we're testing module splitting, we encode this name in a separate module id. + const splitIdx = '' + turbopackModuleIdx++; + turbopackServerModules[splitIdx] = { + s: moduleExports.split, + }; + turbopackServerMap[path + '#split'] = { + id: splitIdx, + chunks: [], + name: 's', + }; + } + const mod = {exports: moduleExports}; + nodeCompile.call(mod, '"use server"', idx); + return mod.exports; +}; diff --git a/packages/react-server-dom-turbopack/src/shared/ReactFlightImportMetadata.js b/packages/react-server-dom-turbopack/src/shared/ReactFlightImportMetadata.js new file mode 100644 index 0000000000000..60460d9c1d6d9 --- /dev/null +++ b/packages/react-server-dom-turbopack/src/shared/ReactFlightImportMetadata.js @@ -0,0 +1,39 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type ImportManifestEntry = { + id: string, + // chunks is an array of filenames + chunks: Array, + name: string, +}; + +// This is the parsed shape of the wire format which is why it is +// condensed to only the essentialy information +export type ImportMetadata = + | [ + /* id */ string, + /* chunk filenames */ Array, + /* name */ string, + /* async */ 1, + ] + | [/* id */ string, /* chunk filenames */ Array, /* name */ string]; + +export const ID = 0; +export const CHUNKS = 1; +export const NAME = 2; +// export const ASYNC = 3; + +// This logic is correct because currently only include the 4th tuple member +// when the module is async. If that changes we will need to actually assert +// the value is true. We don't index into the 4th slot because flow does not +// like the potential out of bounds access +export function isAsyncImport(metadata: ImportMetadata): boolean { + return metadata.length === 4; +} diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-turbopack.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-turbopack.js new file mode 100644 index 0000000000000..3fdeac94f5775 --- /dev/null +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-turbopack.js @@ -0,0 +1,16 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Request} from 'react-server/src/ReactFlightServer'; + +export * from 'react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler'; +export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-turbopack.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-turbopack.js new file mode 100644 index 0000000000000..2beb3986d0726 --- /dev/null +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-turbopack.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ +import type {Request} from 'react-server/src/ReactFlightServer'; + +export * from 'react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler'; +export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +// For now, we get this from the global scope, but this will likely move to a module. +export const supportsRequestStorage = typeof AsyncLocalStorage === 'function'; +export const requestStorage: AsyncLocalStorage = supportsRequestStorage + ? new AsyncLocalStorage() + : (null: any); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-turbopack.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-turbopack.js new file mode 100644 index 0000000000000..f85af8f3c296c --- /dev/null +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-turbopack.js @@ -0,0 +1,19 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {AsyncLocalStorage} from 'async_hooks'; + +import type {Request} from 'react-server/src/ReactFlightServer'; + +export * from 'react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler'; +export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = true; +export const requestStorage: AsyncLocalStorage = + new AsyncLocalStorage(); diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index 97db3a53a4318..a175f4f5910cf 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -78,6 +78,11 @@ declare var __webpack_require__: ((id: string) => any) & { u: string => string, }; +declare function __turbopack_load__(id: string): Promise; +declare var __turbopack_require__: ((id: string) => any) & { + u: string => string, +}; + declare module 'fs/promises' { declare var access: (path: string, mode?: number) => Promise; declare var lstat: ( diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index cd4187a34887a..31d15ecc7b875 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -432,6 +432,109 @@ const bundles = [ externals: ['url', 'module', 'react-server-dom-webpack/server'], }, + /******* React Server DOM Turbopack Server *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-turbopack/server.browser', + global: 'ReactServerDOMServer', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom'], + }, + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-turbopack/server.node', + global: 'ReactServerDOMServer', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'util', 'async_hooks', 'react-dom'], + }, + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-turbopack/server.node.unbundled', + global: 'ReactServerDOMServer', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'util', 'async_hooks', 'react-dom'], + }, + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-turbopack/server.edge', + global: 'ReactServerDOMServer', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'util', 'async_hooks', 'react-dom'], + }, + + /******* React Server DOM Turbopack Client *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-turbopack/client.browser', + global: 'ReactServerDOMClient', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom'], + }, + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-turbopack/client.node', + global: 'ReactServerDOMClient', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom', 'util'], + }, + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-turbopack/client.node.unbundled', + global: 'ReactServerDOMClient', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom', 'util'], + }, + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-turbopack/client.edge', + global: 'ReactServerDOMClient', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom'], + }, + + /******* React Server DOM Turbopack Plugin *******/ + // There is no plugin the moment because Turbopack + // does not expose a plugin interface yet. + + /******* React Server DOM Turbopack Node.js Loader *******/ + { + bundleTypes: [ESM_PROD], + moduleType: RENDERER_UTILS, + entry: 'react-server-dom-turbopack/node-loader', + global: 'ReactServerTurbopackNodeLoader', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['acorn'], + }, + + /******* React Server DOM Turbopack Node.js CommonJS Loader *******/ + { + bundleTypes: [NODE_ES2015], + moduleType: RENDERER_UTILS, + entry: 'react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister', + name: 'react-server-dom-turbopack-node-register', + global: 'ReactFlightWebpackNodeRegister', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['url', 'module', 'react-server-dom-turbopack/server'], + }, + /******* React Server DOM ESM Server *******/ { bundleTypes: [NODE_DEV, NODE_PROD], diff --git a/scripts/rollup/validate/eslintrc.cjs.js b/scripts/rollup/validate/eslintrc.cjs.js index b3ea021a619f4..63406a6a6b245 100644 --- a/scripts/rollup/validate/eslintrc.cjs.js +++ b/scripts/rollup/validate/eslintrc.cjs.js @@ -56,6 +56,10 @@ module.exports = { __webpack_chunk_load__: 'readonly', __webpack_require__: 'readonly', + // Flight Turbopack + __turbopack_load__: 'readonly', + __turbopack_require__: 'readonly', + // jest expect: 'readonly', jest: 'readonly', diff --git a/scripts/rollup/validate/eslintrc.cjs2015.js b/scripts/rollup/validate/eslintrc.cjs2015.js index 435d5e5c820b6..3c8ade7946c71 100644 --- a/scripts/rollup/validate/eslintrc.cjs2015.js +++ b/scripts/rollup/validate/eslintrc.cjs2015.js @@ -56,6 +56,10 @@ module.exports = { __webpack_chunk_load__: 'readonly', __webpack_require__: 'readonly', + // Flight Turbopack + __turbopack_load__: 'readonly', + __turbopack_require__: 'readonly', + // jest expect: 'readonly', jest: 'readonly', diff --git a/scripts/rollup/validate/eslintrc.esm.js b/scripts/rollup/validate/eslintrc.esm.js index 11c72a68f9362..a46004a25bed1 100644 --- a/scripts/rollup/validate/eslintrc.esm.js +++ b/scripts/rollup/validate/eslintrc.esm.js @@ -56,6 +56,10 @@ module.exports = { __webpack_chunk_load__: 'readonly', __webpack_require__: 'readonly', + // Flight Turbopack + __turbopack_load__: 'readonly', + __turbopack_require__: 'readonly', + // jest expect: 'readonly', jest: 'readonly', diff --git a/scripts/rollup/validate/eslintrc.umd.js b/scripts/rollup/validate/eslintrc.umd.js index 020bfe0bb6b0c..3d35c688bdbf0 100644 --- a/scripts/rollup/validate/eslintrc.umd.js +++ b/scripts/rollup/validate/eslintrc.umd.js @@ -61,6 +61,10 @@ module.exports = { __webpack_chunk_load__: 'readonly', __webpack_require__: 'readonly', + // Flight Turbopack + __turbopack_load__: 'readonly', + __turbopack_require__: 'readonly', + // jest jest: 'readonly', diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index c958a4a4ccfae..6f2ba535a27d1 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -49,6 +49,115 @@ module.exports = [ isFlowTyped: true, isServerSupported: true, }, + { + shortName: 'dom-node-webpack', + entryPoints: [ + 'react-server-dom-webpack/server.node', + 'react-server-dom-webpack/client.node', + ], + paths: [ + 'react-dom', + 'react-dom-bindings', + 'react-dom/client', + 'react-dom/server', + 'react-dom/server.node', + 'react-dom/static', + 'react-dom/static.node', + 'react-dom/src/server/react-dom-server.node', + 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node + 'react-dom/src/server/ReactDOMFizzStaticNode.js', + 'react-server-dom-webpack', + 'react-server-dom-webpack/client.node', + 'react-server-dom-webpack/server', + 'react-server-dom-webpack/server.node', + 'react-server-dom-webpack/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node + 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js', + 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer.js', + 'react-server-dom-webpack/node-register', + 'react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js', + 'react-devtools', + 'react-devtools-core', + 'react-devtools-shell', + 'react-devtools-shared', + 'react-interactions', + 'shared/ReactDOMSharedInternals', + ], + isFlowTyped: true, + isServerSupported: true, + }, + { + shortName: 'dom-node-turbopack', + entryPoints: [ + 'react-server-dom-turbopack/server.node.unbundled', + 'react-server-dom-turbopack/client.node.unbundled', + ], + paths: [ + 'react-dom', + 'react-dom-bindings', + 'react-dom/client', + 'react-dom/server', + 'react-dom/server.node', + 'react-dom/static', + 'react-dom/static.node', + 'react-dom/src/server/react-dom-server.node', + 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node + 'react-dom/src/server/ReactDOMFizzStaticNode.js', + 'react-server-dom-turbopack', + 'react-server-dom-turbopack/client.node.unbundled', + 'react-server-dom-turbopack/server', + 'react-server-dom-turbopack/server.node.unbundled', + 'react-server-dom-turbopack/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node.unbundled + 'react-server-dom-turbopack/src/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node.unbundled + 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode.js', + 'react-server-dom-turbopack/node-register', + 'react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js', + 'react-devtools', + 'react-devtools-core', + 'react-devtools-shell', + 'react-devtools-shared', + 'react-interactions', + 'shared/ReactDOMSharedInternals', + ], + isFlowTyped: true, + isServerSupported: true, + }, + { + shortName: 'dom-node-turbopack-bundled', + entryPoints: [ + 'react-server-dom-turbopack/server.node', + 'react-server-dom-turbopack/client.node', + ], + paths: [ + 'react-dom', + 'react-dom-bindings', + 'react-dom/client', + 'react-dom/server', + 'react-dom/server.node', + 'react-dom/static', + 'react-dom/static.node', + 'react-dom/src/server/react-dom-server.node', + 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node + 'react-dom/src/server/ReactDOMFizzStaticNode.js', + 'react-server-dom-turbopack', + 'react-server-dom-turbopack/client.node', + 'react-server-dom-turbopack/server', + 'react-server-dom-turbopack/server.node', + 'react-server-dom-turbopack/src/ReactFlightDOMServerNode.js', // react-server-dom-turbopack/server.node + 'react-server-dom-turbopack/src/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node + 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js', + 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js', + 'react-server-dom-turbopack/node-register', + 'react-server-dom-turbopack/src/ReactFlightTurbopackNodeRegister.js', + 'react-devtools', + 'react-devtools-core', + 'react-devtools-shell', + 'react-devtools-shared', + 'react-interactions', + 'shared/ReactDOMSharedInternals', + ], + isFlowTyped: true, + isServerSupported: true, + }, { shortName: 'dom-bun', entryPoints: ['react-dom', 'react-dom/src/server/react-dom-server.bun.js'], @@ -127,6 +236,36 @@ module.exports = [ isFlowTyped: true, isServerSupported: true, }, + { + shortName: 'dom-browser-turbopack', + entryPoints: [ + 'react-server-dom-turbopack/client.browser', + 'react-server-dom-turbopack/server.browser', + ], + paths: [ + 'react-dom', + 'react-dom/client', + 'react-dom/server', + 'react-dom/server.node', + 'react-dom-bindings', + 'react-server-dom-turbopack', + 'react-server-dom-turbopack/client', + 'react-server-dom-turbopack/client.browser', + 'react-server-dom-turbopack/server.browser', + 'react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js', // react-server-dom-turbopack/client.browser + 'react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js', // react-server-dom-turbopack/server.browser + 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js', + 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser.js', + 'react-devtools', + 'react-devtools-core', + 'react-devtools-shell', + 'react-devtools-shared', + 'react-interactions', + 'shared/ReactDOMSharedInternals', + ], + isFlowTyped: true, + isServerSupported: true, + }, { shortName: 'dom-edge-webpack', entryPoints: [ @@ -163,38 +302,33 @@ module.exports = [ isServerSupported: true, }, { - shortName: 'dom-node-webpack', + shortName: 'dom-edge-turbopack', entryPoints: [ - 'react-server-dom-webpack/server.node', - 'react-server-dom-webpack/client.node', + 'react-server-dom-turbopack/server.edge', + 'react-server-dom-turbopack/client.edge', ], paths: [ 'react-dom', 'react-dom/src/ReactDOMSharedSubset.js', 'react-dom-bindings', 'react-dom/client', - 'react-dom/server', - 'react-dom/server.node', - 'react-dom/static', - 'react-dom/static.node', - 'react-dom/src/server/react-dom-server.node', - 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node - 'react-dom/src/server/ReactDOMFizzStaticNode.js', - 'react-server-dom-webpack', - 'react-server-dom-webpack/client.node', - 'react-server-dom-webpack/server', - 'react-server-dom-webpack/server.node', - 'react-server-dom-webpack/src/ReactFlightDOMServerNode.js', // react-server-dom-webpack/server.node - 'react-server-dom-webpack/src/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node - 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js', - 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer.js', - 'react-server-dom-webpack/node-register', - 'react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js', + 'react-dom/server.edge', + 'react-dom/static.edge', + 'react-dom/unstable_testing', + 'react-dom/src/server/react-dom-server.edge', + 'react-dom/src/server/ReactDOMFizzServerEdge.js', // react-dom/server.edge + 'react-dom/src/server/ReactDOMFizzStaticEdge.js', + 'react-server-dom-turbopack', + 'react-server-dom-turbopack/client.edge', + 'react-server-dom-turbopack/server.edge', + 'react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js', // react-server-dom-webpack/client.edge + 'react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js', // react-server-dom-webpack/server.edge + 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js', + 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer.js', 'react-devtools', 'react-devtools-core', 'react-devtools-shell', 'react-devtools-shared', - 'react-interactions', 'shared/ReactDOMSharedInternals', ], isFlowTyped: true,