diff --git a/.eslintrc.js b/.eslintrc.js index 9d142d359a..86ad482eff 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -331,6 +331,7 @@ module.exports = { 'packages/react-server-dom-turbopack/**/*.js', 'packages/react-server-dom-parcel/**/*.js', 'packages/react-server-dom-fb/**/*.js', + 'packages/react-server-dom-unbundled/**/*.js', 'packages/react-test-renderer/**/*.js', 'packages/react-debug-tools/**/*.js', 'packages/react-devtools-extensions/**/*.js', diff --git a/.github/workflows/runtime_build_and_test.yml b/.github/workflows/runtime_build_and_test.yml index ce11f76530..6e5332e196 100644 --- a/.github/workflows/runtime_build_and_test.yml +++ b/.github/workflows/runtime_build_and_test.yml @@ -41,7 +41,7 @@ jobs: with: path: | **/node_modules - key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} lookup-only: true - uses: actions/setup-node@v4 if: steps.node_modules.outputs.cache-hit != 'true' @@ -55,10 +55,8 @@ jobs: with: path: | **/node_modules - key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - restore-keys: | - runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}- - runtime-node_modules-v6- + key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + # Don't use restore-keys here. Otherwise the cache grows indefinitely. - run: yarn install --frozen-lockfile if: steps.node_modules.outputs.cache-hit != 'true' - name: Save cache @@ -67,7 +65,7 @@ jobs: with: path: | **/node_modules - key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} runtime_compiler_node_modules_cache: name: Cache Runtime, Compiler node_modules @@ -82,7 +80,7 @@ jobs: with: path: | **/node_modules - key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }} + key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }} lookup-only: true - uses: actions/setup-node@v4 if: steps.node_modules.outputs.cache-hit != 'true' @@ -98,10 +96,8 @@ jobs: with: path: | **/node_modules - key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }} - restore-keys: | - runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}- - runtime-and-compiler-node_modules-v6- + key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }} + # Don't use restore-keys here. Otherwise the cache grows indefinitely. - run: yarn install --frozen-lockfile if: steps.node_modules.outputs.cache-hit != 'true' - run: yarn --cwd compiler install --frozen-lockfile @@ -112,7 +108,7 @@ jobs: with: path: | **/node_modules - key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }} + key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }} # ----- FLOW ----- discover_flow_inline_configs: @@ -154,10 +150,8 @@ jobs: with: path: | **/node_modules - key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - restore-keys: | - runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}- - runtime-node_modules-v6- + key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + # Don't use restore-keys here. Otherwise the cache grows indefinitely. - name: Ensure clean build directory run: rm -rf build - run: yarn install --frozen-lockfile @@ -184,10 +178,8 @@ jobs: with: path: | **/node_modules - key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - restore-keys: | - runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}- - runtime-node_modules-v6- + key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + # Don't use restore-keys here. Otherwise the cache grows indefinitely. - name: Ensure clean build directory run: rm -rf build - run: yarn install --frozen-lockfile @@ -216,7 +208,7 @@ jobs: with: path: | **/node_modules - key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - name: Ensure clean build directory run: rm -rf build - run: yarn install --frozen-lockfile @@ -274,10 +266,8 @@ jobs: with: path: | **/node_modules - key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }} - restore-keys: | - runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}- - runtime-and-compiler-node_modules-v6- + key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }} + # Don't use restore-keys here. Otherwise the cache grows indefinitely. - name: Ensure clean build directory run: rm -rf build - run: yarn install --frozen-lockfile @@ -306,7 +296,7 @@ jobs: with: path: | **/node_modules - key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }} + key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }} - name: Install runtime dependencies run: yarn install --frozen-lockfile if: steps.node_modules.outputs.cache-hit != 'true' @@ -349,10 +339,8 @@ jobs: with: path: | **/node_modules - key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }} - restore-keys: | - runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}- - runtime-and-compiler-node_modules-v6- + key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }} + # Don't use restore-keys here. Otherwise the cache grows indefinitely. - name: Ensure clean build directory run: rm -rf build - run: yarn install --frozen-lockfile @@ -440,10 +428,8 @@ jobs: with: path: | **/node_modules - key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }} - restore-keys: | - runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}- - runtime-and-compiler-node_modules-v6- + key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }} + # Don't use restore-keys here. Otherwise the cache grows indefinitely. - name: Ensure clean build directory run: rm -rf build - run: yarn install --frozen-lockfile @@ -483,10 +469,8 @@ jobs: with: path: | **/node_modules - key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - restore-keys: | - runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}- - runtime-node_modules-v6- + key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + # Don't use restore-keys here. Otherwise the cache grows indefinitely. - name: Ensure clean build directory run: rm -rf build - run: yarn install --frozen-lockfile @@ -548,10 +532,8 @@ jobs: with: path: | **/node_modules - key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - restore-keys: | - runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}- - runtime-node_modules-v6- + key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + # Don't use restore-keys here. Otherwise the cache grows indefinitely. - name: Ensure clean build directory run: rm -rf build - run: yarn install --frozen-lockfile @@ -588,10 +570,8 @@ jobs: with: path: | **/node_modules - key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - restore-keys: | - runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}- - runtime-node_modules-v6- + key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + # Don't use restore-keys here. Otherwise the cache grows indefinitely. - name: Ensure clean build directory run: rm -rf build - run: yarn install --frozen-lockfile @@ -740,10 +720,8 @@ jobs: with: path: | **/node_modules - key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - restore-keys: | - runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}- - runtime-node_modules-v6- + key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + # Don't use restore-keys here. Otherwise the cache grows indefinitely. - name: Ensure clean build directory run: rm -rf build - run: yarn install --frozen-lockfile @@ -802,10 +780,8 @@ jobs: with: path: | **/node_modules - key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - restore-keys: | - runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}- - runtime-node_modules-v6- + key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + # Don't use restore-keys here. Otherwise the cache grows indefinitely. - name: Ensure clean build directory run: rm -rf build - run: yarn install --frozen-lockfile diff --git a/fixtures/flight/loader/region.js b/fixtures/flight/loader/region.js index c81538bc71..864d429394 100644 --- a/fixtures/flight/loader/region.js +++ b/fixtures/flight/loader/region.js @@ -3,7 +3,7 @@ import { load as reactLoad, getSource as getSourceImpl, transformSource as reactTransformSource, -} from 'react-server-dom-webpack/node-loader'; +} from 'react-server-dom-unbundled/node-loader'; export {resolve}; diff --git a/fixtures/flight/server/global.js b/fixtures/flight/server/global.js index f097378056..0883faf627 100644 --- a/fixtures/flight/server/global.js +++ b/fixtures/flight/server/global.js @@ -36,7 +36,7 @@ const http = require('http'); const React = require('react'); const {renderToPipeableStream} = require('react-dom/server'); -const {createFromNodeStream} = require('react-server-dom-webpack/client'); +const {createFromNodeStream} = require('react-server-dom-unbundled/client'); const {PassThrough} = require('stream'); const app = express(); diff --git a/fixtures/flight/server/region.js b/fixtures/flight/server/region.js index ccf7907aac..d6d8418a3d 100644 --- a/fixtures/flight/server/region.js +++ b/fixtures/flight/server/region.js @@ -5,7 +5,8 @@ const path = require('path'); const url = require('url'); -const register = require('react-server-dom-webpack/node-register'); +const register = require('react-server-dom-unbundled/node-register'); +// TODO: This seems to have no effect anymore. Remove? register(); const babelRegister = require('@babel/register'); @@ -76,7 +77,7 @@ function getDebugChannel(req) { async function renderApp(res, returnValue, formState, noCache, debugChannel) { const {renderToPipeableStream} = await import( - 'react-server-dom-webpack/server' + 'react-server-dom-unbundled/server' ); // const m = require('../src/App.js'); const m = await import('../src/App.js'); @@ -134,7 +135,7 @@ async function renderApp(res, returnValue, formState, noCache, debugChannel) { async function prerenderApp(res, returnValue, formState, noCache) { const {prerenderToNodeStream} = await import( - 'react-server-dom-webpack/static' + 'react-server-dom-unbundled/static' ); // const m = require('../src/App.js'); const m = await import('../src/App.js'); @@ -202,7 +203,7 @@ app.get('/', async function (req, res) { app.post('/', bodyParser.text(), async function (req, res) { const noCache = req.headers['cache-control'] === 'no-cache'; const {decodeReply, decodeReplyFromBusboy, decodeAction, decodeFormState} = - await import('react-server-dom-webpack/server'); + await import('react-server-dom-unbundled/server'); const serverReference = req.get('rsc-action'); if (serverReference) { // This is the client-side case diff --git a/fixtures/flight/src/App.js b/fixtures/flight/src/App.js index df2c8922b8..0bfe0fe630 100644 --- a/fixtures/flight/src/App.js +++ b/fixtures/flight/src/App.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import {renderToReadableStream} from 'react-server-dom-webpack/server'; +import {renderToReadableStream} from 'react-server-dom-unbundled/server'; import {createFromReadableStream} from 'react-server-dom-webpack/client'; import {PassThrough, Readable} from 'stream'; diff --git a/package.json b/package.json index 0108818eaf..6969bc14f8 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "build-for-devtools": "cross-env yarn build react/index,react/jsx,react/compiler-runtime,react-dom/index,react-dom/client,react-dom/unstable_testing,react-dom/test-utils,react-is,react-debug-tools,scheduler,react-test-renderer,react-refresh,react-art --type=NODE --release-channel=experimental", "build-for-devtools-dev": "yarn build-for-devtools --type=NODE_DEV", "build-for-devtools-prod": "yarn build-for-devtools --type=NODE_PROD", - "build-for-flight-dev": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react.react-server,react-dom/index,react-dom/client,react-dom/server,react-dom.react-server,react-dom-server.node,react-dom-server-legacy.node,scheduler,react-server-dom-webpack/ --type=NODE_DEV,ESM_PROD,NODE_ES2015 && mv ./build/node_modules ./build/oss-experimental", + "build-for-flight-dev": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react.react-server,react-dom/index,react-dom/client,react-dom/server,react-dom.react-server,react-dom-server.node,react-dom-server-legacy.node,scheduler,react-server-dom-webpack/,react-server-dom-unbundled/ --type=NODE_DEV,ESM_PROD,NODE_ES2015 && mv ./build/node_modules ./build/oss-experimental", "build-for-vt-dev": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react-dom/index,react-dom/client,react-dom/server,react-dom-server.node,react-dom-server-legacy.node,scheduler --type=NODE_DEV && mv ./build/node_modules ./build/oss-experimental", "flow-typed-install": "yarn flow-typed install --skip --skipFlowRestart --ignore-deps=dev", "linc": "node ./scripts/tasks/linc.js", diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-unbundled.js similarity index 59% rename from packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js rename to packages/react-client/src/forks/ReactFlightClientConfig.dom-node-unbundled.js index 9840d5bc91..c95a3ba07f 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-unbundled.js @@ -6,13 +6,13 @@ * * @flow */ + export {default as rendererVersion} from 'shared/ReactVersion'; -export const rendererPackageName = 'react-server-dom-webpack'; +export const rendererPackageName = 'react-server-dom-unbundled'; export * from 'react-client/src/ReactFlightClientStreamConfigNode'; export * from 'react-client/src/ReactClientConsoleConfigServer'; -export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack'; -export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackServer'; -export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigTargetWebpackServer'; +export * from 'react-server-dom-unbundled/src/client/ReactFlightClientConfigBundlerNode'; +export * from 'react-server-dom-unbundled/src/client/ReactFlightClientConfigTargetNodeServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js index 65e1252ee5..9840d5bc91 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js @@ -6,13 +6,13 @@ * * @flow */ - export {default as rendererVersion} from 'shared/ReactVersion'; export const rendererPackageName = 'react-server-dom-webpack'; export * from 'react-client/src/ReactFlightClientStreamConfigNode'; export * from 'react-client/src/ReactClientConsoleConfigServer'; -export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerNode'; +export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack'; +export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackServer'; export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigTargetWebpackServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = true; diff --git a/packages/react-server-dom-unbundled/README.md b/packages/react-server-dom-unbundled/README.md new file mode 100644 index 0000000000..1df84033b4 --- /dev/null +++ b/packages/react-server-dom-unbundled/README.md @@ -0,0 +1,5 @@ +# react-server-dom-unbundled + +Test-only React Flight bindings for DOM using Node.js. + +This only exists for internal testing. diff --git a/packages/react-server-dom-webpack/client.node.unbundled.js b/packages/react-server-dom-unbundled/client.js similarity index 74% rename from packages/react-server-dom-webpack/client.node.unbundled.js rename to packages/react-server-dom-unbundled/client.js index e5f8c2cb72..c3ec7662d6 100644 --- a/packages/react-server-dom-webpack/client.node.unbundled.js +++ b/packages/react-server-dom-unbundled/client.js @@ -7,4 +7,4 @@ * @flow */ -export * from './src/client/react-flight-dom-client.node.unbundled'; +export * from './src/client/react-flight-dom-client.node'; diff --git a/packages/react-server-dom-unbundled/esm/package.json b/packages/react-server-dom-unbundled/esm/package.json new file mode 100644 index 0000000000..3dbc1ca591 --- /dev/null +++ b/packages/react-server-dom-unbundled/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/react-server-dom-unbundled/esm/react-server-dom-unbundled-node-loader.production.js b/packages/react-server-dom-unbundled/esm/react-server-dom-unbundled-node-loader.production.js new file mode 100644 index 0000000000..da3948d4c0 --- /dev/null +++ b/packages/react-server-dom-unbundled/esm/react-server-dom-unbundled-node-loader.production.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/ReactFlightUnbundledNodeLoader.js'; diff --git a/packages/react-server-dom-unbundled/index.js b/packages/react-server-dom-unbundled/index.js new file mode 100644 index 0000000000..233bab2d97 --- /dev/null +++ b/packages/react-server-dom-unbundled/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-webpack/client instead.'); diff --git a/packages/react-server-dom-unbundled/node-register.js b/packages/react-server-dom-unbundled/node-register.js new file mode 100644 index 0000000000..9727bc8d79 --- /dev/null +++ b/packages/react-server-dom-unbundled/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/ReactFlightUnbundledNodeRegister'); diff --git a/packages/react-server-dom-unbundled/npm/client.js b/packages/react-server-dom-unbundled/npm/client.js new file mode 100644 index 0000000000..5fd8000b1c --- /dev/null +++ b/packages/react-server-dom-unbundled/npm/client.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-unbundled-client.node.production.js'); +} else { + module.exports = require('./cjs/react-server-dom-unbundled-client.node.development.js'); +} diff --git a/packages/react-server-dom-unbundled/npm/esm/package.json b/packages/react-server-dom-unbundled/npm/esm/package.json new file mode 100644 index 0000000000..3dbc1ca591 --- /dev/null +++ b/packages/react-server-dom-unbundled/npm/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/react-server-dom-unbundled/npm/index.js b/packages/react-server-dom-unbundled/npm/index.js new file mode 100644 index 0000000000..42ff0d7436 --- /dev/null +++ b/packages/react-server-dom-unbundled/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-unbundled/client instead.'); diff --git a/packages/react-server-dom-unbundled/npm/node-register.js b/packages/react-server-dom-unbundled/npm/node-register.js new file mode 100644 index 0000000000..a354e8b2ff --- /dev/null +++ b/packages/react-server-dom-unbundled/npm/node-register.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./cjs/react-server-dom-unbundled-node-register.js'); diff --git a/packages/react-server-dom-unbundled/npm/server.js b/packages/react-server-dom-unbundled/npm/server.js new file mode 100644 index 0000000000..13a632e641 --- /dev/null +++ b/packages/react-server-dom-unbundled/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-unbundled/npm/server.node.js b/packages/react-server-dom-unbundled/npm/server.node.js new file mode 100644 index 0000000000..20164d88d2 --- /dev/null +++ b/packages/react-server-dom-unbundled/npm/server.node.js @@ -0,0 +1,20 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-unbundled-server.node.production.js'); +} else { + s = require('./cjs/react-server-dom-unbundled-server.node.development.js'); +} + +exports.renderToReadableStream = s.renderToReadableStream; +exports.renderToPipeableStream = s.renderToPipeableStream; +exports.decodeReply = s.decodeReply; +exports.decodeReplyFromBusboy = s.decodeReplyFromBusboy; +exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable; +exports.decodeAction = s.decodeAction; +exports.decodeFormState = s.decodeFormState; +exports.registerServerReference = s.registerServerReference; +exports.registerClientReference = s.registerClientReference; +exports.createClientModuleProxy = s.createClientModuleProxy; +exports.createTemporaryReferenceSet = s.createTemporaryReferenceSet; diff --git a/packages/react-server-dom-unbundled/npm/static.js b/packages/react-server-dom-unbundled/npm/static.js new file mode 100644 index 0000000000..13a632e641 --- /dev/null +++ b/packages/react-server-dom-unbundled/npm/static.js @@ -0,0 +1,6 @@ +'use strict'; + +throw new Error( + 'The React Server Writer cannot be used outside a react-server environment. ' + + 'You must configure Node.js using the `--conditions react-server` flag.' +); diff --git a/packages/react-server-dom-unbundled/npm/static.node.js b/packages/react-server-dom-unbundled/npm/static.node.js new file mode 100644 index 0000000000..dd62382b19 --- /dev/null +++ b/packages/react-server-dom-unbundled/npm/static.node.js @@ -0,0 +1,11 @@ +'use strict'; + +var s; +if (process.env.NODE_ENV === 'production') { + s = require('./cjs/react-server-dom-unbundled-server.node.production.js'); +} else { + s = require('./cjs/react-server-dom-unbundled-server.node.development.js'); +} + +exports.prerender = s.prerender; +exports.prerenderToNodeStream = s.prerenderToNodeStream; diff --git a/packages/react-server-dom-unbundled/package.json b/packages/react-server-dom-unbundled/package.json new file mode 100644 index 0000000000..b3836d895f --- /dev/null +++ b/packages/react-server-dom-unbundled/package.json @@ -0,0 +1,46 @@ +{ + "name": "react-server-dom-unbundled", + "description": "React Server Components bindings for DOM using Node.js. This only exists for internal testing.", + "version": "0.0.0", + "private": true, + "files": [ + "LICENSE", + "README.md", + "index.js", + "client.js", + "server.js", + "server.node.js", + "static.js", + "static.node.js", + "node-register.js", + "cjs/", + "esm/" + ], + "exports": { + ".": "./index.js", + "./client": "./client.js", + "./server": { + "react-server": "./server.node.js", + "default": "./server.js" + }, + "./server.node": "./server.node.js", + "./static": { + "react-server": "./static.node.js", + "default": "./static.js" + }, + "./static.node": "./static.node.js", + "./node-loader": "./esm/react-server-dom-unbundled-node-loader.production.js", + "./node-register": "./node-register.js", + "./src/*": "./src/*.js", + "./package.json": "./package.json" + }, + "main": "index.js", + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "dependencies": { + "acorn-loose": "^8.3.0", + "webpack-sources": "^3.2.0" + } +} diff --git a/packages/react-server-dom-unbundled/server.js b/packages/react-server-dom-unbundled/server.js new file mode 100644 index 0000000000..83d8b8a017 --- /dev/null +++ b/packages/react-server-dom-unbundled/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-webpack/server.node.unbundled.js b/packages/react-server-dom-unbundled/server.node.js similarity index 88% rename from packages/react-server-dom-webpack/server.node.unbundled.js rename to packages/react-server-dom-unbundled/server.node.js index db7af8607b..bd00ba7275 100644 --- a/packages/react-server-dom-webpack/server.node.unbundled.js +++ b/packages/react-server-dom-unbundled/server.node.js @@ -19,4 +19,4 @@ export { registerClientReference, createClientModuleProxy, createTemporaryReferenceSet, -} from './src/server/react-flight-dom-server.node.unbundled'; +} from './src/server/react-flight-dom-server.node'; diff --git a/packages/react-server-dom-unbundled/src/ReactFlightUnbundledNodeLoader.js b/packages/react-server-dom-unbundled/src/ReactFlightUnbundledNodeLoader.js new file mode 100644 index 0000000000..9799acc3a0 --- /dev/null +++ b/packages/react-server-dom-unbundled/src/ReactFlightUnbundledNodeLoader.js @@ -0,0 +1,804 @@ +/** + * 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'; + +import readMappings from 'webpack-sources/lib/helpers/readMappings.js'; +import createMappingsSerializer from 'webpack-sources/lib/helpers/createMappingsSerializer.js'; + +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); +} + +type ExportedEntry = { + localName: string, + exportedName: string, + type: null | string, + loc: { + start: {line: number, column: number}, + end: {line: number, column: number}, + }, + originalLine: number, + originalColumn: number, + originalSource: number, + nameIndex: number, +}; + +function addExportedEntry( + exportedEntries: Array, + localNames: Set, + localName: string, + exportedName: string, + type: null | 'function', + loc: { + start: {line: number, column: number}, + end: {line: number, column: number}, + }, +) { + if (localNames.has(localName)) { + // If the same local name is exported more than once, we only need one of the names. + return; + } + exportedEntries.push({ + localName, + exportedName, + type, + loc, + originalLine: -1, + originalColumn: -1, + originalSource: -1, + nameIndex: -1, + }); +} + +function addLocalExportedNames( + exportedEntries: Array, + localNames: Set, + node: any, +) { + switch (node.type) { + case 'Identifier': + addExportedEntry( + exportedEntries, + localNames, + node.name, + node.name, + null, + node.loc, + ); + return; + case 'ObjectPattern': + for (let i = 0; i < node.properties.length; i++) + addLocalExportedNames(exportedEntries, localNames, node.properties[i]); + return; + case 'ArrayPattern': + for (let i = 0; i < node.elements.length; i++) { + const element = node.elements[i]; + if (element) + addLocalExportedNames(exportedEntries, localNames, element); + } + return; + case 'Property': + addLocalExportedNames(exportedEntries, localNames, node.value); + return; + case 'AssignmentPattern': + addLocalExportedNames(exportedEntries, localNames, node.left); + return; + case 'RestElement': + addLocalExportedNames(exportedEntries, localNames, node.argument); + return; + case 'ParenthesizedExpression': + addLocalExportedNames(exportedEntries, localNames, node.expression); + return; + } +} + +function transformServerModule( + source: string, + program: any, + url: string, + sourceMap: any, + loader: LoadFunction, +): string { + const body = program.body; + + // This entry list needs to be in source location order. + const exportedEntries: Array = []; + // Dedupe set. + const localNames: Set = new Set(); + + 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') { + addExportedEntry( + exportedEntries, + localNames, + node.declaration.name, + 'default', + null, + node.declaration.loc, + ); + } else if (node.declaration.type === 'FunctionDeclaration') { + if (node.declaration.id) { + addExportedEntry( + exportedEntries, + localNames, + node.declaration.id.name, + 'default', + 'function', + node.declaration.id.loc, + ); + } 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( + exportedEntries, + localNames, + declarations[j].id, + ); + } + } else { + const name = node.declaration.id.name; + addExportedEntry( + exportedEntries, + localNames, + name, + name, + + node.declaration.type === 'FunctionDeclaration' + ? 'function' + : null, + node.declaration.id.loc, + ); + } + } + if (node.specifiers) { + const specifiers = node.specifiers; + for (let j = 0; j < specifiers.length; j++) { + const specifier = specifiers[j]; + addExportedEntry( + exportedEntries, + localNames, + specifier.local.name, + specifier.exported.name, + null, + specifier.local.loc, + ); + } + } + continue; + } + } + + let mappings = + sourceMap && typeof sourceMap.mappings === 'string' + ? sourceMap.mappings + : ''; + let newSrc = source; + + if (exportedEntries.length > 0) { + let lastSourceIndex = 0; + let lastOriginalLine = 0; + let lastOriginalColumn = 0; + let lastNameIndex = 0; + let sourceLineCount = 0; + let lastMappedLine = 0; + + if (sourceMap) { + // We iterate source mapping entries and our matched exports in parallel to source map + // them to their original location. + let nextEntryIdx = 0; + let nextEntryLine = exportedEntries[nextEntryIdx].loc.start.line; + let nextEntryColumn = exportedEntries[nextEntryIdx].loc.start.column; + readMappings( + mappings, + ( + generatedLine: number, + generatedColumn: number, + sourceIndex: number, + originalLine: number, + originalColumn: number, + nameIndex: number, + ) => { + if ( + generatedLine > nextEntryLine || + (generatedLine === nextEntryLine && + generatedColumn > nextEntryColumn) + ) { + // We're past the entry which means that the best match we have is the previous entry. + if (lastMappedLine === nextEntryLine) { + // Match + exportedEntries[nextEntryIdx].originalLine = lastOriginalLine; + exportedEntries[nextEntryIdx].originalColumn = lastOriginalColumn; + exportedEntries[nextEntryIdx].originalSource = lastSourceIndex; + exportedEntries[nextEntryIdx].nameIndex = lastNameIndex; + } else { + // Skip if we didn't have any mappings on the exported line. + } + nextEntryIdx++; + if (nextEntryIdx < exportedEntries.length) { + nextEntryLine = exportedEntries[nextEntryIdx].loc.start.line; + nextEntryColumn = exportedEntries[nextEntryIdx].loc.start.column; + } else { + nextEntryLine = -1; + nextEntryColumn = -1; + } + } + lastMappedLine = generatedLine; + if (sourceIndex > -1) { + lastSourceIndex = sourceIndex; + } + if (originalLine > -1) { + lastOriginalLine = originalLine; + } + if (originalColumn > -1) { + lastOriginalColumn = originalColumn; + } + if (nameIndex > -1) { + lastNameIndex = nameIndex; + } + }, + ); + if (nextEntryIdx < exportedEntries.length) { + if (lastMappedLine === nextEntryLine) { + // Match + exportedEntries[nextEntryIdx].originalLine = lastOriginalLine; + exportedEntries[nextEntryIdx].originalColumn = lastOriginalColumn; + exportedEntries[nextEntryIdx].originalSource = lastSourceIndex; + exportedEntries[nextEntryIdx].nameIndex = lastNameIndex; + } + } + + for ( + let lastIdx = mappings.length - 1; + lastIdx >= 0 && mappings[lastIdx] === ';'; + lastIdx-- + ) { + // If the last mapped lines don't contain any segments, we don't get a callback from readMappings + // so we need to pad the number of mapped lines, with one for each empty line. + lastMappedLine++; + } + + sourceLineCount = program.loc.end.line; + if (sourceLineCount < lastMappedLine) { + throw new Error( + 'The source map has more mappings than there are lines.', + ); + } + // If the original source string had more lines than there are mappings in the source map. + // Add some extra padding of unmapped lines so that any lines that we add line up. + for ( + let extraLines = sourceLineCount - lastMappedLine; + extraLines > 0; + extraLines-- + ) { + mappings += ';'; + } + } else { + // If a file doesn't have a source map then we generate a blank source map that just + // contains the original content and segments pointing to the original lines. + sourceLineCount = 1; + let idx = -1; + while ((idx = source.indexOf('\n', idx + 1)) !== -1) { + sourceLineCount++; + } + mappings = 'AAAA' + ';AACA'.repeat(sourceLineCount - 1); + sourceMap = { + version: 3, + sources: [url], + sourcesContent: [source], + mappings: mappings, + sourceRoot: '', + }; + lastSourceIndex = 0; + lastOriginalLine = sourceLineCount; + lastOriginalColumn = 0; + lastNameIndex = -1; + lastMappedLine = sourceLineCount; + + for (let i = 0; i < exportedEntries.length; i++) { + // Point each entry to original location. + const entry = exportedEntries[i]; + entry.originalSource = 0; + entry.originalLine = entry.loc.start.line; + // We use column zero since we do the short-hand line-only source maps above. + entry.originalColumn = 0; // entry.loc.start.column; + } + } + + newSrc += '\n\n;'; + newSrc += + 'import {registerServerReference} from "react-server-dom-webpack/server";\n'; + if (mappings) { + mappings += ';;'; + } + + const createMapping = createMappingsSerializer(); + + // Create an empty mapping pointing to where we last left off to reset the counters. + let generatedLine = 1; + createMapping( + generatedLine, + 0, + lastSourceIndex, + lastOriginalLine, + lastOriginalColumn, + lastNameIndex, + ); + for (let i = 0; i < exportedEntries.length; i++) { + const entry = exportedEntries[i]; + generatedLine++; + if (entry.type !== 'function') { + // We first check if the export is a function and if so annotate it. + newSrc += 'if (typeof ' + entry.localName + ' === "function") '; + } + newSrc += 'registerServerReference(' + entry.localName + ','; + newSrc += JSON.stringify(url) + ','; + newSrc += JSON.stringify(entry.exportedName) + ');\n'; + + mappings += createMapping( + generatedLine, + 0, + entry.originalSource, + entry.originalLine, + entry.originalColumn, + entry.nameIndex, + ); + } + } + + if (sourceMap) { + // Override with an new mappings and serialize an inline source map. + sourceMap.mappings = mappings; + newSrc += + '//# sourceMappingURL=data:application/json;charset=utf-8;base64,' + + Buffer.from(JSON.stringify(sourceMap)).toString('base64'); + } + + 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( + program: any, + url: string, + sourceMap: any, + loader: LoadFunction, +): Promise { + const body = program.body; + + const names: Array = []; + + await parseExportNamesInto(body, names, url, loader); + + if (names.length === 0) { + return ''; + } + + let newSrc = + 'import {registerClientReference} from "react-server-dom-webpack/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'; + } + + // TODO: Generate source maps for Client Reference functions so they can point to their + // original locations. + 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 sourceMappingURL = null; + let sourceMappingStart = 0; + let sourceMappingEnd = 0; + let sourceMappingLines = 0; + + let program; + try { + program = acorn.parse(source, { + ecmaVersion: '2024', + sourceType: 'module', + locations: true, + onComment( + block: boolean, + text: string, + start: number, + end: number, + startLoc: {line: number, column: number}, + endLoc: {line: number, column: number}, + ) { + if ( + text.startsWith('# sourceMappingURL=') || + text.startsWith('@ sourceMappingURL=') + ) { + sourceMappingURL = text.slice(19); + sourceMappingStart = start; + sourceMappingEnd = end; + sourceMappingLines = endLoc.line - startLoc.line; + } + }, + }); + } 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; + + const body = program.body; + 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.', + ); + } + + let sourceMap = null; + if (sourceMappingURL) { + const sourceMapResult = await loader( + sourceMappingURL, + // $FlowFixMe + { + format: 'json', + conditions: [], + importAssertions: {type: 'json'}, + importAttributes: {type: 'json'}, + }, + loader, + ); + const sourceMapString = + typeof sourceMapResult.source === 'string' + ? sourceMapResult.source + : // $FlowFixMe + sourceMapResult.source.toString('utf8'); + sourceMap = JSON.parse(sourceMapString); + + // Strip the source mapping comment. We'll re-add it below if needed. + source = + source.slice(0, sourceMappingStart) + + '\n'.repeat(sourceMappingLines) + + source.slice(sourceMappingEnd); + } + + if (useClient) { + return transformClientModule(program, url, sourceMap, loader); + } + + return transformServerModule(source, program, url, sourceMap, 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-unbundled/src/ReactFlightUnbundledNodeRegister.js b/packages/react-server-dom-unbundled/src/ReactFlightUnbundledNodeRegister.js new file mode 100644 index 0000000000..5b4cd9de25 --- /dev/null +++ b/packages/react-server-dom-unbundled/src/ReactFlightUnbundledNodeRegister.js @@ -0,0 +1,109 @@ +/** + * 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-unbundled/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) { + 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-unbundled/src/ReactFlightUnbundledReferences.js b/packages/react-server-dom-unbundled/src/ReactFlightUnbundledReferences.js new file mode 100644 index 0000000000..de437414ef --- /dev/null +++ b/packages/react-server-dom-unbundled/src/ReactFlightUnbundledReferences.js @@ -0,0 +1,352 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactClientValue} from 'react-server/src/ReactFlightServer'; + +export type ServerReference = T & { + $$typeof: symbol, + $$id: string, + $$bound: null | Array, + $$location?: Error, +}; + +// eslint-disable-next-line no-unused-vars +export type ClientReference = { + $$typeof: symbol, + $$id: string, + $$async: boolean, +}; + +const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference'); +const SERVER_REFERENCE_TAG = Symbol.for('react.server.reference'); + +export function isClientReference(reference: Object): boolean { + return reference.$$typeof === CLIENT_REFERENCE_TAG; +} + +export function isServerReference(reference: Object): boolean { + return reference.$$typeof === SERVER_REFERENCE_TAG; +} + +export function registerClientReference( + proxyImplementation: any, + id: string, + exportName: string, +): ClientReference { + return registerClientReferenceImpl( + proxyImplementation, + id + '#' + exportName, + false, + ); +} + +function registerClientReferenceImpl( + proxyImplementation: any, + id: string, + async: boolean, +): ClientReference { + return Object.defineProperties(proxyImplementation, { + $$typeof: {value: CLIENT_REFERENCE_TAG}, + $$id: {value: id}, + $$async: {value: async}, + }); +} + +// $FlowFixMe[method-unbinding] +const FunctionBind = Function.prototype.bind; +// $FlowFixMe[method-unbinding] +const ArraySlice = Array.prototype.slice; +function bind(this: ServerReference): any { + // $FlowFixMe[incompatible-call] + const newFn = FunctionBind.apply(this, arguments); + if (this.$$typeof === SERVER_REFERENCE_TAG) { + if (__DEV__) { + const thisBind = arguments[0]; + if (thisBind != null) { + console.error( + 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().', + ); + } + } + const args = ArraySlice.call(arguments, 1); + const $$typeof = {value: SERVER_REFERENCE_TAG}; + const $$id = {value: this.$$id}; + const $$bound = {value: this.$$bound ? this.$$bound.concat(args) : args}; + return Object.defineProperties( + (newFn: any), + (__DEV__ + ? { + $$typeof, + $$id, + $$bound, + $$location: { + value: this.$$location, + configurable: true, + }, + bind: {value: bind, configurable: true}, + } + : { + $$typeof, + $$id, + $$bound, + bind: {value: bind, configurable: true}, + }) as PropertyDescriptorMap, + ); + } + return newFn; +} + +export function registerServerReference( + reference: T, + id: string, + exportName: null | string, +): ServerReference { + const $$typeof = {value: SERVER_REFERENCE_TAG}; + const $$id = { + value: exportName === null ? id : id + '#' + exportName, + configurable: true, + }; + const $$bound = {value: null, configurable: true}; + return Object.defineProperties( + (reference: any), + __DEV__ + ? ({ + $$typeof, + $$id, + $$bound, + $$location: { + value: Error('react-stack-top-frame'), + configurable: true, + }, + bind: {value: bind, configurable: true}, + } as PropertyDescriptorMap) + : ({ + $$typeof, + $$id, + $$bound, + bind: {value: bind, configurable: true}, + } as PropertyDescriptorMap), + ); +} + +const PROMISE_PROTOTYPE = Promise.prototype; + +const deepProxyHandlers: Proxy$traps = { + get: function ( + target: Function, + name: string | symbol, + receiver: Proxy, + ) { + switch (name) { + // These names are read by the Flight runtime if you end up using the exports object. + case '$$typeof': + // These names are a little too common. We should probably have a way to + // have the Flight runtime extract the inner target instead. + return target.$$typeof; + case '$$id': + return target.$$id; + case '$$async': + return target.$$async; + case 'name': + return target.name; + case 'displayName': + return undefined; + // We need to special case this because createElement reads it if we pass this + // reference. + case 'defaultProps': + return undefined; + // React looks for debugInfo on thenables. + case '_debugInfo': + return undefined; + // Avoid this attempting to be serialized. + case 'toJSON': + return undefined; + case Symbol.toPrimitive: + // $FlowFixMe[prop-missing] + return Object.prototype[Symbol.toPrimitive]; + case Symbol.toStringTag: + // $FlowFixMe[prop-missing] + return Object.prototype[Symbol.toStringTag]; + case 'Provider': + throw new Error( + `Cannot render a Client Context Provider on the Server. ` + + `Instead, you can export a Client Component wrapper ` + + `that itself renders a Client Context Provider.`, + ); + case 'then': + throw new Error( + `Cannot await or return from a thenable. ` + + `You cannot await a client module from a server component.`, + ); + } + // eslint-disable-next-line react-internal/safe-string-coercion + const expression = String(target.name) + '.' + String(name); + throw new Error( + `Cannot access ${expression} on the server. ` + + 'You cannot dot into a client module from a server component. ' + + 'You can only pass the imported name through.', + ); + }, + set: function () { + throw new Error('Cannot assign to a client module from a server module.'); + }, +}; + +function getReference(target: Function, name: string | symbol): $FlowFixMe { + switch (name) { + // These names are read by the Flight runtime if you end up using the exports object. + case '$$typeof': + return target.$$typeof; + case '$$id': + return target.$$id; + case '$$async': + return target.$$async; + case 'name': + return target.name; + // We need to special case this because createElement reads it if we pass this + // reference. + case 'defaultProps': + return undefined; + // React looks for debugInfo on thenables. + case '_debugInfo': + return undefined; + // Avoid this attempting to be serialized. + case 'toJSON': + return undefined; + case Symbol.toPrimitive: + // $FlowFixMe[prop-missing] + return Object.prototype[Symbol.toPrimitive]; + case Symbol.toStringTag: + // $FlowFixMe[prop-missing] + return Object.prototype[Symbol.toStringTag]; + case '__esModule': + // Something is conditionally checking which export to use. We'll pretend to be + // an ESM compat module but then we'll check again on the client. + const moduleId = target.$$id; + target.default = registerClientReferenceImpl( + (function () { + throw new Error( + `Attempted to call the default export of ${moduleId} from the server ` + + `but it's on the client. It's not possible to invoke a client function from ` + + `the server, it can only be rendered as a Component or passed to props of a ` + + `Client Component.`, + ); + }: any), + target.$$id + '#', + target.$$async, + ); + return true; + case 'then': + if (target.then) { + // Use a cached value + return target.then; + } + if (!target.$$async) { + // If this module is expected to return a Promise (such as an AsyncModule) then + // we should resolve that with a client reference that unwraps the Promise on + // the client. + + const clientReference: ClientReference = + registerClientReferenceImpl(({}: any), target.$$id, true); + const proxy = new Proxy(clientReference, proxyHandlers); + + // Treat this as a resolved Promise for React's use() + target.status = 'fulfilled'; + target.value = proxy; + + const then = (target.then = registerClientReferenceImpl( + (function then(resolve, reject: any) { + // Expose to React. + return Promise.resolve(resolve(proxy)); + }: any), + // If this is not used as a Promise but is treated as a reference to a `.then` + // export then we should treat it as a reference to that name. + target.$$id + '#then', + false, + )); + return then; + } else { + // Since typeof .then === 'function' is a feature test we'd continue recursing + // indefinitely if we return a function. Instead, we return an object reference + // if we check further. + return undefined; + } + } + if (typeof name === 'symbol') { + throw new Error( + 'Cannot read Symbol exports. Only named exports are supported on a client module ' + + 'imported on the server.', + ); + } + let cachedReference = target[name]; + if (!cachedReference) { + const reference: ClientReference = registerClientReferenceImpl( + (function () { + throw new Error( + // eslint-disable-next-line react-internal/safe-string-coercion + `Attempted to call ${String(name)}() from the server but ${String( + name, + )} is on the client. ` + + `It's not possible to invoke a client function from the server, it can ` + + `only be rendered as a Component or passed to props of a Client Component.`, + ); + }: any), + target.$$id + '#' + name, + target.$$async, + ); + Object.defineProperty((reference: any), 'name', {value: name}); + cachedReference = target[name] = new Proxy(reference, deepProxyHandlers); + } + return cachedReference; +} + +const proxyHandlers = { + get: function ( + target: Function, + name: string | symbol, + receiver: Proxy, + ): $FlowFixMe { + return getReference(target, name); + }, + getOwnPropertyDescriptor: function ( + target: Function, + name: string | symbol, + ): $FlowFixMe { + let descriptor = Object.getOwnPropertyDescriptor(target, name); + if (!descriptor) { + descriptor = { + value: getReference(target, name), + writable: false, + configurable: false, + enumerable: false, + }; + Object.defineProperty(target, name, descriptor); + } + return descriptor; + }, + getPrototypeOf(target: Function): Object { + // Pretend to be a Promise in case anyone asks. + return PROMISE_PROTOTYPE; + }, + set: function (): empty { + throw new Error('Cannot assign to a client module from a server module.'); + }, +}; + +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-webpack/src/client/ReactFlightClientConfigBundlerNode.js b/packages/react-server-dom-unbundled/src/client/ReactFlightClientConfigBundlerNode.js similarity index 100% rename from packages/react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerNode.js rename to packages/react-server-dom-unbundled/src/client/ReactFlightClientConfigBundlerNode.js diff --git a/packages/react-server-dom-unbundled/src/client/ReactFlightClientConfigTargetNodeServer.js b/packages/react-server-dom-unbundled/src/client/ReactFlightClientConfigTargetNodeServer.js new file mode 100644 index 0000000000..f5793fdab4 --- /dev/null +++ b/packages/react-server-dom-unbundled/src/client/ReactFlightClientConfigTargetNodeServer.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {preinitScriptForSSR} from 'react-client/src/ReactFlightClientConfig'; + +export type ModuleLoading = null | { + prefix: string, + crossOrigin?: 'use-credentials' | '', +}; + +export function prepareDestinationWithChunks( + moduleLoading: ModuleLoading, + // Chunks are double-indexed [..., idx, filenamex, idy, filenamey, ...] + chunks: Array, + nonce: ?string, +) { + if (moduleLoading !== null) { + for (let i = 1; i < chunks.length; i += 2) { + preinitScriptForSSR( + moduleLoading.prefix + chunks[i], + nonce, + moduleLoading.crossOrigin, + ); + } + } +} diff --git a/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientEdge.js new file mode 100644 index 0000000000..2cf668f679 --- /dev/null +++ b/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientEdge.js @@ -0,0 +1,255 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js'; + +import type { + DebugChannel, + FindSourceMapURLCallback, + Response as FlightResponse, +} from 'react-client/src/ReactFlightClient'; + +import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient'; + +import type { + ServerConsumerModuleMap, + ModuleLoading, + ServerManifest, +} from 'react-client/src/ReactFlightClientConfig'; + +type ServerConsumerManifest = { + moduleMap: ServerConsumerModuleMap, + moduleLoading: ModuleLoading, + serverModuleMap: null | ServerManifest, +}; + +import { + createResponse, + createStreamState, + getRoot, + reportGlobalError, + processBinaryChunk, + close, +} from 'react-client/src/ReactFlightClient'; + +import { + processReply, + createServerReference as createServerReferenceImpl, +} from 'react-client/src/ReactFlightReplyClient'; + +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + +import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; + +export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; + +export type {TemporaryReferenceSet}; + +function noServerCall() { + throw new Error( + 'Server Functions cannot be called during initial render. ' + + 'This would create a fetch waterfall. Try to use a Server Component ' + + 'to pass data to Client Components instead.', + ); +} + +export function createServerReference, T>( + id: any, + callServer: any, +): (...A) => Promise { + return createServerReferenceImpl(id, noServerCall); +} + +type EncodeFormActionCallback = ( + id: any, + args: Promise, +) => ReactCustomFormAction; + +export type Options = { + serverConsumerManifest: ServerConsumerManifest, + nonce?: string, + encodeFormAction?: EncodeFormActionCallback, + temporaryReferences?: TemporaryReferenceSet, + findSourceMapURL?: FindSourceMapURLCallback, + replayConsoleLogs?: boolean, + environmentName?: string, + startTime?: number, + endTime?: number, + // For the Edge client we only support a single-direction debug channel. + debugChannel?: {readable?: ReadableStream, ...}, +}; + +function createResponseFromOptions(options: Options) { + const debugChannel: void | DebugChannel = + __DEV__ && options && options.debugChannel !== undefined + ? { + hasReadable: options.debugChannel.readable !== undefined, + callback: null, + } + : undefined; + + return createResponse( + options.serverConsumerManifest.moduleMap, + options.serverConsumerManifest.serverModuleMap, + options.serverConsumerManifest.moduleLoading, + noServerCall, + options.encodeFormAction, + typeof options.nonce === 'string' ? options.nonce : undefined, + options && options.temporaryReferences + ? options.temporaryReferences + : undefined, + __DEV__ && options && options.findSourceMapURL + ? options.findSourceMapURL + : undefined, + __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false + __DEV__ && options && options.environmentName + ? options.environmentName + : undefined, + __DEV__ && options && options.startTime != null + ? options.startTime + : undefined, + __DEV__ && options && options.endTime != null ? options.endTime : undefined, + debugChannel, + ); +} + +function startReadingFromStream( + response: FlightResponse, + stream: ReadableStream, + onDone: () => void, + debugValue: mixed, +): void { + const streamState = createStreamState(response, debugValue); + const reader = stream.getReader(); + function progress({ + done, + value, + }: { + done: boolean, + value: ?any, + ... + }): void | Promise { + if (done) { + return onDone(); + } + const buffer: Uint8Array = (value: any); + processBinaryChunk(response, streamState, buffer); + return reader.read().then(progress).catch(error); + } + function error(e: any) { + reportGlobalError(response, e); + } + reader.read().then(progress).catch(error); +} + +function createFromReadableStream( + stream: ReadableStream, + options: Options, +): Thenable { + const response: FlightResponse = createResponseFromOptions(options); + + if ( + __DEV__ && + options && + options.debugChannel && + options.debugChannel.readable + ) { + let streamDoneCount = 0; + const handleDone = () => { + if (++streamDoneCount === 2) { + close(response); + } + }; + startReadingFromStream(response, options.debugChannel.readable, handleDone); + startReadingFromStream(response, stream, handleDone, stream); + } else { + startReadingFromStream( + response, + stream, + close.bind(null, response), + stream, + ); + } + + return getRoot(response); +} + +function createFromFetch( + promiseForResponse: Promise, + options: Options, +): Thenable { + const response: FlightResponse = createResponseFromOptions(options); + promiseForResponse.then( + function (r) { + if ( + __DEV__ && + options && + options.debugChannel && + options.debugChannel.readable + ) { + let streamDoneCount = 0; + const handleDone = () => { + if (++streamDoneCount === 2) { + close(response); + } + }; + startReadingFromStream( + response, + options.debugChannel.readable, + handleDone, + ); + startReadingFromStream(response, (r.body: any), handleDone, r); + } else { + startReadingFromStream( + response, + (r.body: any), + close.bind(null, response), + r, + ); + } + }, + function (e) { + reportGlobalError(response, e); + }, + ); + return getRoot(response); +} + +function encodeReply( + value: ReactServerValue, + options?: {temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal}, +): Promise< + string | URLSearchParams | FormData, +> /* We don't use URLSearchParams yet but maybe */ { + return new Promise((resolve, reject) => { + const abort = processReply( + value, + '', + options && options.temporaryReferences + ? options.temporaryReferences + : undefined, + resolve, + reject, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort((signal: any).reason); + } else { + const listener = () => { + abort((signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + }); +} + +export {createFromFetch, createFromReadableStream, encodeReply}; diff --git a/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientNode.js new file mode 100644 index 0000000000..8863b1bf1c --- /dev/null +++ b/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientNode.js @@ -0,0 +1,139 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js'; + +import type { + DebugChannel, + FindSourceMapURLCallback, + Response, +} from 'react-client/src/ReactFlightClient'; + +import type { + ServerConsumerModuleMap, + ModuleLoading, + ServerManifest, +} from 'react-client/src/ReactFlightClientConfig'; + +type ServerConsumerManifest = { + moduleMap: ServerConsumerModuleMap, + moduleLoading: ModuleLoading, + serverModuleMap: null | ServerManifest, +}; + +import type {Readable} from 'stream'; + +import { + createResponse, + createStreamState, + getRoot, + reportGlobalError, + processStringChunk, + processBinaryChunk, + close, +} from 'react-client/src/ReactFlightClient'; + +export * from './ReactFlightDOMClientEdge'; + +function noServerCall() { + throw new Error( + 'Server Functions cannot be called during initial render. ' + + 'This would create a fetch waterfall. Try to use a Server Component ' + + 'to pass data to Client Components instead.', + ); +} + +type EncodeFormActionCallback = ( + id: any, + args: Promise, +) => ReactCustomFormAction; + +export type Options = { + nonce?: string, + encodeFormAction?: EncodeFormActionCallback, + findSourceMapURL?: FindSourceMapURLCallback, + replayConsoleLogs?: boolean, + environmentName?: string, + startTime?: number, + endTime?: number, + // For the Node.js client we only support a single-direction debug channel. + debugChannel?: Readable, +}; + +function startReadingFromStream( + response: Response, + stream: Readable, + onEnd: () => void, +): void { + const streamState = createStreamState(response, stream); + + stream.on('data', chunk => { + if (typeof chunk === 'string') { + processStringChunk(response, streamState, chunk); + } else { + processBinaryChunk(response, streamState, chunk); + } + }); + + stream.on('error', error => { + reportGlobalError(response, error); + }); + + stream.on('end', onEnd); +} + +function createFromNodeStream( + stream: Readable, + serverConsumerManifest: ServerConsumerManifest, + options?: Options, +): Thenable { + const debugChannel: void | DebugChannel = + __DEV__ && options && options.debugChannel !== undefined + ? {hasReadable: true, callback: null} + : undefined; + + const response: Response = createResponse( + serverConsumerManifest.moduleMap, + serverConsumerManifest.serverModuleMap, + serverConsumerManifest.moduleLoading, + noServerCall, + options ? options.encodeFormAction : undefined, + options && typeof options.nonce === 'string' ? options.nonce : undefined, + undefined, // TODO: If encodeReply is supported, this should support temporaryReferences + __DEV__ && options && options.findSourceMapURL + ? options.findSourceMapURL + : undefined, + __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false + __DEV__ && options && options.environmentName + ? options.environmentName + : undefined, + __DEV__ && options && options.startTime != null + ? options.startTime + : undefined, + __DEV__ && options && options.endTime != null ? options.endTime : undefined, + debugChannel, + ); + + if (__DEV__ && options && options.debugChannel) { + let streamEndedCount = 0; + const handleEnd = () => { + if (++streamEndedCount === 2) { + close(response); + } + }; + startReadingFromStream(response, options.debugChannel, handleEnd); + startReadingFromStream(response, stream, handleEnd); + } else { + startReadingFromStream(response, stream, close.bind(null, response)); + } + + return getRoot(response); +} + +export {createFromNodeStream}; diff --git a/packages/react-server-dom-webpack/src/client/react-flight-dom-client.node.unbundled.js b/packages/react-server-dom-unbundled/src/client/react-flight-dom-client.node.js similarity index 100% rename from packages/react-server-dom-webpack/src/client/react-flight-dom-client.node.unbundled.js rename to packages/react-server-dom-unbundled/src/client/react-flight-dom-client.node.js diff --git a/packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js new file mode 100644 index 0000000000..647498a65d --- /dev/null +++ b/packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js @@ -0,0 +1,695 @@ +/** + * 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 './ReactFlightServerConfigUnbundledBundler'; +import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; +import type {Busboy} from 'busboy'; +import type {Writable} from 'stream'; +import type {Thenable} from 'shared/ReactTypes'; + +import type {Duplex} from 'stream'; + +import {Readable} from 'stream'; + +import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; + +import { + createRequest, + createPrerenderRequest, + startWork, + startFlowing, + startFlowingDebug, + stopFlowing, + abort, + resolveDebugMessage, + closeDebugChannel, +} from 'react-server/src/ReactFlightServer'; + +import { + createResponse, + reportGlobalError, + close, + resolveField, + resolveFile, + resolveFileInfo, + resolveFileChunk, + resolveFileComplete, + getRoot, +} from 'react-server/src/ReactFlightReplyServer'; + +import { + decodeAction, + decodeFormState, +} from 'react-server/src/ReactFlightActionServer'; + +export { + registerServerReference, + registerClientReference, + createClientModuleProxy, +} from '../ReactFlightUnbundledReferences'; + +import { + createStringDecoder, + readPartialStringChunk, + readFinalStringChunk, +} from 'react-client/src/ReactFlightClientStreamConfigNode'; + +import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode'; + +import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences'; + +export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences'; + +export type {TemporaryReferenceSet}; + +function createDrainHandler(destination: Destination, request: Request) { + return () => startFlowing(request, destination); +} + +function createCancelHandler(request: Request, reason: string) { + return () => { + stopFlowing(request); + abort(request, new Error(reason)); + }; +} + +function startReadingFromDebugChannelReadable( + request: Request, + stream: Readable | WebSocket, +): void { + const stringDecoder = createStringDecoder(); + let lastWasPartial = false; + let stringBuffer = ''; + function onData(chunk: string | Uint8Array) { + if (typeof chunk === 'string') { + if (lastWasPartial) { + stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0)); + lastWasPartial = false; + } + stringBuffer += chunk; + } else { + const buffer: Uint8Array = (chunk: any); + stringBuffer += readPartialStringChunk(stringDecoder, buffer); + lastWasPartial = true; + } + const messages = stringBuffer.split('\n'); + for (let i = 0; i < messages.length - 1; i++) { + resolveDebugMessage(request, messages[i]); + } + stringBuffer = messages[messages.length - 1]; + } + function onError(error: mixed) { + abort( + request, + new Error('Lost connection to the Debug Channel.', { + cause: error, + }), + ); + } + function onClose() { + closeDebugChannel(request); + } + if ( + // $FlowFixMe[method-unbinding] + typeof stream.addEventListener === 'function' && + // $FlowFixMe[method-unbinding] + typeof stream.binaryType === 'string' + ) { + const ws: WebSocket = (stream: any); + ws.binaryType = 'arraybuffer'; + ws.addEventListener('message', event => { + // $FlowFixMe + onData(event.data); + }); + ws.addEventListener('error', event => { + // $FlowFixMe + onError(event.error); + }); + ws.addEventListener('close', onClose); + } else { + const readable: Readable = (stream: any); + readable.on('data', onData); + readable.on('error', onError); + readable.on('end', onClose); + } +} + +type Options = { + debugChannel?: Readable | Writable | Duplex | WebSocket, + environmentName?: string | (() => string), + filterStackFrame?: (url: string, functionName: string) => boolean, + onError?: (error: mixed) => void, + identifierPrefix?: string, + temporaryReferences?: TemporaryReferenceSet, +}; + +type PipeableStream = { + abort(reason: mixed): void, + pipe(destination: T): T, +}; + +function renderToPipeableStream( + model: ReactClientValue, + webpackMap: ClientManifest, + options?: Options, +): PipeableStream { + const debugChannel = __DEV__ && options ? options.debugChannel : undefined; + const debugChannelReadable: void | Readable | WebSocket = + __DEV__ && + debugChannel !== undefined && + // $FlowFixMe[method-unbinding] + (typeof debugChannel.read === 'function' || + typeof debugChannel.readyState === 'number') + ? (debugChannel: any) + : undefined; + const debugChannelWritable: void | Writable = + __DEV__ && debugChannel !== undefined + ? // $FlowFixMe[method-unbinding] + typeof debugChannel.write === 'function' + ? (debugChannel: any) + : // $FlowFixMe[method-unbinding] + typeof debugChannel.send === 'function' + ? createFakeWritableFromWebSocket((debugChannel: any)) + : undefined + : undefined; + const request = createRequest( + model, + webpackMap, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + debugChannelReadable !== undefined, + ); + let hasStartedFlowing = false; + startWork(request); + if (debugChannelWritable !== undefined) { + startFlowingDebug(request, debugChannelWritable); + } + if (debugChannelReadable !== undefined) { + startReadingFromDebugChannelReadable(request, debugChannelReadable); + } + return { + pipe(destination: T): T { + if (hasStartedFlowing) { + throw new Error( + 'React currently only supports piping to one writable stream.', + ); + } + hasStartedFlowing = true; + startFlowing(request, destination); + destination.on('drain', createDrainHandler(destination, request)); + destination.on( + 'error', + createCancelHandler( + request, + 'The destination stream errored while writing data.', + ), + ); + // We don't close until the debug channel closes. + if (!__DEV__ || debugChannelReadable === undefined) { + destination.on( + 'close', + createCancelHandler(request, 'The destination stream closed early.'), + ); + } + return destination; + }, + abort(reason: mixed) { + abort(request, reason); + }, + }; +} + +function createFakeWritableFromWebSocket(webSocket: WebSocket): Writable { + return ({ + write(chunk: string | Uint8Array) { + webSocket.send((chunk: any)); + return true; + }, + end() { + webSocket.close(); + }, + destroy(reason) { + if (typeof reason === 'object' && reason !== null) { + reason = reason.message; + } + if (typeof reason === 'string') { + webSocket.close(1011, reason); + } else { + webSocket.close(1011); + } + }, + }: any); +} + +function createFakeWritableFromReadableStreamController( + controller: ReadableStreamController, +): Writable { + // The current host config expects a Writable so we create + // a fake writable for now to push into the Readable. + return ({ + write(chunk: string | Uint8Array) { + if (typeof chunk === 'string') { + chunk = textEncoder.encode(chunk); + } + controller.enqueue(chunk); + // in web streams there is no backpressure so we can always write more + return true; + }, + end() { + controller.close(); + }, + destroy(error) { + // $FlowFixMe[method-unbinding] + if (typeof controller.error === 'function') { + // $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types. + controller.error(error); + } else { + controller.close(); + } + }, + }: any); +} + +function startReadingFromDebugChannelReadableStream( + request: Request, + stream: ReadableStream, +): void { + const reader = stream.getReader(); + const stringDecoder = createStringDecoder(); + let stringBuffer = ''; + function progress({ + done, + value, + }: { + done: boolean, + value: ?any, + ... + }): void | Promise { + const buffer: Uint8Array = (value: any); + stringBuffer += done + ? readFinalStringChunk(stringDecoder, new Uint8Array(0)) + : readPartialStringChunk(stringDecoder, buffer); + const messages = stringBuffer.split('\n'); + for (let i = 0; i < messages.length - 1; i++) { + resolveDebugMessage(request, messages[i]); + } + stringBuffer = messages[messages.length - 1]; + if (done) { + closeDebugChannel(request); + return; + } + return reader.read().then(progress).catch(error); + } + function error(e: any) { + abort( + request, + new Error('Lost connection to the Debug Channel.', { + cause: e, + }), + ); + } + reader.read().then(progress).catch(error); +} + +function renderToReadableStream( + model: ReactClientValue, + webpackMap: ClientManifest, + options?: Omit & { + debugChannel?: {readable?: ReadableStream, writable?: WritableStream, ...}, + signal?: AbortSignal, + }, +): ReadableStream { + const debugChannelReadable = + __DEV__ && options && options.debugChannel + ? options.debugChannel.readable + : undefined; + const debugChannelWritable = + __DEV__ && options && options.debugChannel + ? options.debugChannel.writable + : undefined; + const request = createRequest( + model, + webpackMap, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + debugChannelReadable !== undefined, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort(request, (signal: any).reason); + } else { + const listener = () => { + abort(request, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + if (debugChannelWritable !== undefined) { + let debugWritable: Writable; + const debugStream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + debugWritable = + createFakeWritableFromReadableStreamController(controller); + }, + pull: (controller): ?Promise => { + startFlowingDebug(request, debugWritable); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + debugStream.pipeTo(debugChannelWritable); + } + if (debugChannelReadable !== undefined) { + startReadingFromDebugChannelReadableStream(request, debugChannelReadable); + } + let writable: Writable; + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + writable = createFakeWritableFromReadableStreamController(controller); + startWork(request); + }, + pull: (controller): ?Promise => { + startFlowing(request, writable); + }, + cancel: (reason): ?Promise => { + stopFlowing(request); + abort(request, reason); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + return stream; +} + +function createFakeWritableFromNodeReadable(readable: any): Writable { + // The current host config expects a Writable so we create + // a fake writable for now to push into the Readable. + return ({ + write(chunk: string | Uint8Array) { + return readable.push(chunk); + }, + end() { + readable.push(null); + }, + destroy(error) { + readable.destroy(error); + }, + }: any); +} + +type PrerenderOptions = { + environmentName?: string | (() => string), + filterStackFrame?: (url: string, functionName: string) => boolean, + onError?: (error: mixed) => void, + identifierPrefix?: string, + temporaryReferences?: TemporaryReferenceSet, + signal?: AbortSignal, +}; + +type StaticResult = { + prelude: Readable, +}; + +function prerenderToNodeStream( + model: ReactClientValue, + webpackMap: ClientManifest, + options?: PrerenderOptions, +): Promise { + return new Promise((resolve, reject) => { + const onFatalError = reject; + function onAllReady() { + const readable: Readable = new Readable({ + read() { + startFlowing(request, writable); + }, + }); + const writable = createFakeWritableFromNodeReadable(readable); + resolve({prelude: readable}); + } + + const request = createPrerenderRequest( + model, + webpackMap, + onAllReady, + onFatalError, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + false, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + const reason = (signal: any).reason; + abort(request, reason); + } else { + const listener = () => { + const reason = (signal: any).reason; + abort(request, reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + startWork(request); + }); +} + +function prerender( + model: ReactClientValue, + webpackMap: ClientManifest, + options?: Options & { + signal?: AbortSignal, + }, +): Promise<{ + prelude: ReadableStream, +}> { + return new Promise((resolve, reject) => { + const onFatalError = reject; + function onAllReady() { + let writable: Writable; + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + writable = + createFakeWritableFromReadableStreamController(controller); + }, + pull: (controller): ?Promise => { + startFlowing(request, writable); + }, + cancel: (reason): ?Promise => { + stopFlowing(request); + abort(request, reason); + }, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + resolve({prelude: stream}); + } + const request = createPrerenderRequest( + model, + webpackMap, + onAllReady, + onFatalError, + options ? options.onError : undefined, + options ? options.identifierPrefix : undefined, + options ? options.temporaryReferences : undefined, + __DEV__ && options ? options.environmentName : undefined, + __DEV__ && options ? options.filterStackFrame : undefined, + false, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + const reason = (signal: any).reason; + abort(request, reason); + } else { + const listener = () => { + const reason = (signal: any).reason; + abort(request, reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + startWork(request); + }); +} + +function decodeReplyFromBusboy( + busboyStream: Busboy, + webpackMap: ServerManifest, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + const response = createResponse( + webpackMap, + '', + options ? options.temporaryReferences : undefined, + ); + let pendingFiles = 0; + const queuedFields: Array = []; + busboyStream.on('field', (name, value) => { + if (pendingFiles > 0) { + // Because the 'end' event fires two microtasks after the next 'field' + // we would resolve files and fields out of order. To handle this properly + // we queue any fields we receive until the previous file is done. + queuedFields.push(name, value); + } else { + try { + resolveField(response, name, value); + } catch (error) { + busboyStream.destroy(error); + } + } + }); + busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => { + if (encoding.toLowerCase() === 'base64') { + busboyStream.destroy( + new Error( + "React doesn't accept base64 encoded file uploads because we don't expect " + + "form data passed from a browser to ever encode data that way. If that's " + + 'the wrong assumption, we can easily fix it.', + ), + ); + return; + } + pendingFiles++; + const file = resolveFileInfo(response, name, filename, mimeType); + value.on('data', chunk => { + resolveFileChunk(response, file, chunk); + }); + value.on('end', () => { + try { + resolveFileComplete(response, name, file); + pendingFiles--; + if (pendingFiles === 0) { + // Release any queued fields + for (let i = 0; i < queuedFields.length; i += 2) { + resolveField(response, queuedFields[i], queuedFields[i + 1]); + } + queuedFields.length = 0; + } + } catch (error) { + busboyStream.destroy(error); + } + }); + }); + busboyStream.on('finish', () => { + close(response); + }); + busboyStream.on('error', err => { + reportGlobalError( + response, + // $FlowFixMe[incompatible-call] types Error and mixed are incompatible + err, + ); + }); + return getRoot(response); +} + +function decodeReply( + body: string | FormData, + webpackMap: ServerManifest, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + if (typeof body === 'string') { + const form = new FormData(); + form.append('0', body); + body = form; + } + const response = createResponse( + webpackMap, + '', + options ? options.temporaryReferences : undefined, + body, + ); + const root = getRoot(response); + close(response); + return root; +} + +function decodeReplyFromAsyncIterable( + iterable: AsyncIterable<[string, string | File]>, + webpackMap: ServerManifest, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + const iterator: AsyncIterator<[string, string | File]> = + iterable[ASYNC_ITERATOR](); + + const response = createResponse( + webpackMap, + '', + options ? options.temporaryReferences : undefined, + ); + + function progress( + entry: + | {done: false, +value: [string, string | File], ...} + | {done: true, +value: void, ...}, + ) { + if (entry.done) { + close(response); + } else { + const [name, value] = entry.value; + if (typeof value === 'string') { + resolveField(response, name, value); + } else { + resolveFile(response, name, value); + } + iterator.next().then(progress, error); + } + } + function error(reason: Error) { + reportGlobalError(response, reason); + if (typeof (iterator: any).throw === 'function') { + // The iterator protocol doesn't necessarily include this but a generator do. + // $FlowFixMe should be able to pass mixed + iterator.throw(reason).then(error, error); + } + } + + iterator.next().then(progress, error); + + return getRoot(response); +} + +export { + renderToReadableStream, + renderToPipeableStream, + prerender, + prerenderToNodeStream, + decodeReply, + decodeReplyFromBusboy, + decodeReplyFromAsyncIterable, + decodeAction, + decodeFormState, +}; diff --git a/packages/react-server-dom-unbundled/src/server/ReactFlightServerConfigUnbundledBundler.js b/packages/react-server-dom-unbundled/src/server/ReactFlightServerConfigUnbundledBundler.js new file mode 100644 index 0000000000..d2c8b038c0 --- /dev/null +++ b/packages/react-server-dom-unbundled/src/server/ReactFlightServerConfigUnbundledBundler.js @@ -0,0 +1,108 @@ +/** + * 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 '../ReactFlightUnbundledReferences'; + +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 '../ReactFlightUnbundledReferences'; + +export function getClientReferenceKey( + reference: ClientReference, +): ClientReferenceKey { + return reference.$$async ? reference.$$id + '#async' : reference.$$id; +} + +export function resolveClientReferenceMetadata( + config: ClientManifest, + clientReference: ClientReference, +): ClientReferenceMetadata { + const modulePath = clientReference.$$id; + let name = ''; + let resolvedModuleData = config[modulePath]; + if (resolvedModuleData) { + // The potentially aliased name. + name = resolvedModuleData.name; + } else { + // We didn't find this specific export name but we might have the * export + // which contains this name as well. + // TODO: It's unfortunate that we now have to parse this string. We should + // probably go back to encoding path and name separately on the client reference. + const idx = modulePath.lastIndexOf('#'); + if (idx !== -1) { + name = modulePath.slice(idx + 1); + resolvedModuleData = config[modulePath.slice(0, idx)]; + } + if (!resolvedModuleData) { + throw new Error( + 'Could not find the module "' + + modulePath + + '" in the React Client Manifest. ' + + 'This is probably a bug in the React Server Components bundler.', + ); + } + } + if (resolvedModuleData.async === true && clientReference.$$async === true) { + throw new Error( + 'The module "' + + modulePath + + '" is marked as an async ESM module but was loaded as a CJS proxy. ' + + 'This is probably a bug in the React Server Components bundler.', + ); + } + if (resolvedModuleData.async === true || clientReference.$$async === true) { + return [resolvedModuleData.id, resolvedModuleData.chunks, name, 1]; + } else { + return [resolvedModuleData.id, resolvedModuleData.chunks, name]; + } +} + +export function getServerReferenceId( + config: ClientManifest, + serverReference: ServerReference, +): ServerReferenceId { + return serverReference.$$id; +} + +export function getServerReferenceBoundArguments( + config: ClientManifest, + serverReference: ServerReference, +): null | Array { + return serverReference.$$bound; +} + +export function getServerReferenceLocation( + config: ClientManifest, + serverReference: ServerReference, +): void | Error { + return serverReference.$$location; +} diff --git a/packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled.js b/packages/react-server-dom-unbundled/src/server/react-flight-dom-server.node.js similarity index 100% rename from packages/react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled.js rename to packages/react-server-dom-unbundled/src/server/react-flight-dom-server.node.js diff --git a/packages/react-server-dom-unbundled/src/shared/ReactFlightImportMetadata.js b/packages/react-server-dom-unbundled/src/shared/ReactFlightImportMetadata.js new file mode 100644 index 0000000000..29b012f605 --- /dev/null +++ b/packages/react-server-dom-unbundled/src/shared/ReactFlightImportMetadata.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type ImportManifestEntry = { + id: string, + // chunks is a double indexed array of chunkId / chunkFilename pairs + chunks: Array, + name: string, + async?: boolean, +}; + +// This is the parsed shape of the wire format which is why it is +// condensed to only the essentialy information +export type ImportMetadata = + | [ + /* id */ string, + /* chunks id/filename pairs, double indexed */ Array, + /* name */ string, + /* async */ 1, + ] + | [ + /* id */ string, + /* chunks id/filename pairs, double indexed */ Array, + /* name */ string, + ]; + +export const ID = 0; +export const CHUNKS = 1; +export const NAME = 2; +// export const ASYNC = 3; + +// This logic is correct because currently only include the 4th tuple member +// when the module is async. If that changes we will need to actually assert +// the value is true. We don't index into the 4th slot because flow does not +// like the potential out of bounds access +export function isAsyncImport(metadata: ImportMetadata): boolean { + return metadata.length === 4; +} diff --git a/packages/react-server-dom-unbundled/static.js b/packages/react-server-dom-unbundled/static.js new file mode 100644 index 0000000000..83d8b8a017 --- /dev/null +++ b/packages/react-server-dom-unbundled/static.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +throw new Error( + 'The React Server cannot be used outside a react-server environment. ' + + 'You must configure Node.js using the `--conditions react-server` flag.', +); diff --git a/packages/react-server-dom-webpack/static.node.unbundled.js b/packages/react-server-dom-unbundled/static.node.js similarity index 80% rename from packages/react-server-dom-webpack/static.node.unbundled.js rename to packages/react-server-dom-unbundled/static.node.js index 3f19796a20..78e70a1cf4 100644 --- a/packages/react-server-dom-webpack/static.node.unbundled.js +++ b/packages/react-server-dom-unbundled/static.node.js @@ -10,4 +10,4 @@ export { prerender, prerenderToNodeStream, -} from './src/server/react-flight-dom-server.node.unbundled'; +} from './src/server/react-flight-dom-server.node'; diff --git a/packages/react-server-dom-webpack/package.json b/packages/react-server-dom-webpack/package.json index d261e13d9f..6722120272 100644 --- a/packages/react-server-dom-webpack/package.json +++ b/packages/react-server-dom-webpack/package.json @@ -17,17 +17,14 @@ "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", "static.js", "static.browser.js", "static.edge.js", "static.node.js", - "static.node.unbundled.js", "node-register.js", "cjs/", "esm/" @@ -39,10 +36,7 @@ "workerd": "./client.edge.js", "deno": "./client.edge.js", "worker": "./client.edge.js", - "node": { - "webpack": "./client.node.js", - "default": "./client.node.unbundled.js" - }, + "node": "./client.node.js", "edge-light": "./client.edge.js", "browser": "./client.browser.js", "default": "./client.browser.js" @@ -50,15 +44,11 @@ "./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": { - "webpack": "./server.node.js", - "default": "./server.node.unbundled.js" - }, + "node": "./server.node.js", "edge-light": "./server.edge.js", "browser": "./server.browser.js" }, @@ -67,15 +57,11 @@ "./server.browser": "./server.browser.js", "./server.edge": "./server.edge.js", "./server.node": "./server.node.js", - "./server.node.unbundled": "./server.node.unbundled.js", "./static": { "react-server": { "workerd": "./static.edge.js", "deno": "./static.browser.js", - "node": { - "webpack": "./static.node.js", - "default": "./static.node.unbundled.js" - }, + "node": "./static.node.js", "edge-light": "./static.edge.js", "browser": "./static.browser.js" }, @@ -84,7 +70,6 @@ "./static.browser": "./static.browser.js", "./static.edge": "./static.edge.js", "./static.node": "./static.node.js", - "./static.node.unbundled": "./static.node.unbundled.js", "./node-loader": "./esm/react-server-dom-webpack-node-loader.production.js", "./node-register": "./node-register.js", "./src/*": "./src/*.js", diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index 0b12f80c08..f1a8e41bc0 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -60,10 +60,10 @@ describe('ReactFlightDOM', () => { FlightReactDOM = require('react-dom'); jest.mock('react-server-dom-webpack/server', () => - require('react-server-dom-webpack/server.node.unbundled'), + require('react-server-dom-unbundled/server.node'), ); jest.mock('react-server-dom-webpack/static', () => - require('react-server-dom-webpack/static.node.unbundled'), + require('react-server-dom-unbundled/static.node'), ); const WebpackMock = require('./utils/WebpackMock'); clientExports = WebpackMock.clientExports; diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js index 38a95f6762..1fe1cb117d 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -45,12 +45,12 @@ describe('ReactFlightDOMNode', () => { // Simulate the condition resolution jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-webpack/server', () => - require('react-server-dom-webpack/server.node'), + jest.requireActual('react-server-dom-webpack/server.node'), ); ReactServer = require('react'); ReactServerDOMServer = require('react-server-dom-webpack/server'); jest.mock('react-server-dom-webpack/static', () => - require('react-server-dom-webpack/static.node'), + jest.requireActual('react-server-dom-webpack/static.node'), ); ReactServerDOMStaticServer = require('react-server-dom-webpack/static'); @@ -64,7 +64,7 @@ describe('ReactFlightDOMNode', () => { __unmockReact(); jest.unmock('react-server-dom-webpack/server'); jest.mock('react-server-dom-webpack/client', () => - require('react-server-dom-webpack/client.node'), + jest.requireActual('react-server-dom-webpack/client.node'), ); React = require('react'); diff --git a/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js b/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js index db1d7af3a6..465eac90ee 100644 --- a/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js +++ b/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js @@ -41,7 +41,7 @@ describe('ReactFlightAsyncDebugInfo', () => { jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-webpack/server', () => - require('react-server-dom-webpack/server.node'), + jest.requireActual('react-server-dom-webpack/server.node'), ); ReactServer = require('react'); ReactServerDOMServer = require('react-server-dom-webpack/server'); @@ -54,7 +54,7 @@ describe('ReactFlightAsyncDebugInfo', () => { __unmockReact(); jest.unmock('react-server-dom-webpack/server'); jest.mock('react-server-dom-webpack/client', () => - require('react-server-dom-webpack/client.node'), + jest.requireActual('react-server-dom-webpack/client.node'), ); React = require('react'); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-unbundled.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-unbundled.js new file mode 100644 index 0000000000..661b756fc8 --- /dev/null +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-unbundled.js @@ -0,0 +1,29 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {AsyncLocalStorage} from 'async_hooks'; + +import type {Request} from 'react-server/src/ReactFlightServer'; +import type {ReactComponentInfo} from 'shared/ReactTypes'; + +export * from 'react-server-dom-unbundled/src/server/ReactFlightServerConfigUnbundledBundler'; +export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = true; +export const requestStorage: AsyncLocalStorage = + new AsyncLocalStorage(); + +export const supportsComponentStorage = __DEV__; +export const componentStorage: AsyncLocalStorage = + supportsComponentStorage ? new AsyncLocalStorage() : (null: any); + +export * from '../ReactFlightServerConfigDebugNode'; + +export * from '../ReactFlightStackConfigV8'; +export * from '../ReactServerConsoleConfigServer'; diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index c4176099b7..8a551d2a15 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -476,25 +476,6 @@ const bundles = [ 'util', ], }, - { - bundleTypes: [NODE_DEV, NODE_PROD], - moduleType: RENDERER, - entry: - 'react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled', - name: 'react-server-dom-webpack-server.node.unbundled', - condition: 'react-server', - global: 'ReactServerDOMServer', - minifyWithProdErrorCodes: false, - wrapWithModuleBoundaries: false, - externals: [ - 'react', - 'react-dom', - 'async_hooks', - 'crypto', - 'stream', - 'util', - ], - }, { bundleTypes: [NODE_DEV, NODE_PROD], moduleType: RENDERER, @@ -529,17 +510,6 @@ const bundles = [ wrapWithModuleBoundaries: false, externals: ['react', 'react-dom', 'util', 'crypto'], }, - { - bundleTypes: [NODE_DEV, NODE_PROD], - moduleType: RENDERER, - entry: - 'react-server-dom-webpack/src/client/react-flight-dom-client.node.unbundled', - name: 'react-server-dom-webpack-client.node.unbundled', - global: 'ReactServerDOMClient', - minifyWithProdErrorCodes: false, - wrapWithModuleBoundaries: false, - externals: ['react', 'react-dom', 'util', 'crypto'], - }, { bundleTypes: [NODE_DEV, NODE_PROD], moduleType: RENDERER, @@ -786,6 +756,63 @@ const bundles = [ externals: ['acorn'], }, + /******* React Server DOM Unbundled Server *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-unbundled/src/server/react-flight-dom-server.node', + name: 'react-server-dom-unbundled-server.node', + condition: 'react-server', + global: 'ReactServerDOMServer', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: [ + 'react', + 'react-dom', + 'async_hooks', + 'crypto', + 'stream', + 'util', + ], + }, + + /******* React Server DOM Unbundled Client *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: RENDERER, + entry: 'react-server-dom-unbundled/src/client/react-flight-dom-client.node', + name: 'react-server-dom-unbundled-client.node', + global: 'ReactServerDOMClient', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['react', 'react-dom', 'util', 'crypto'], + }, + + /******* React Server DOM Unbundled Node.js Loader *******/ + { + bundleTypes: [ESM_PROD], + moduleType: RENDERER_UTILS, + entry: 'react-server-dom-unbundled/node-loader', + condition: 'react-server', + global: 'ReactServerUnbundledNodeLoader', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['acorn'], + }, + + /******* React Server DOM Unbundled Node.js CommonJS Loader *******/ + { + bundleTypes: [NODE_ES2015], + moduleType: RENDERER_UTILS, + entry: 'react-server-dom-unbundled/src/ReactFlightUnbundledNodeRegister', + name: 'react-server-dom-unbundled-node-register', + condition: 'react-server', + global: 'ReactFlightUnbundledNodeRegister', + minifyWithProdErrorCodes: false, + wrapWithModuleBoundaries: false, + externals: ['url', 'module', 'react-server-dom-unbundled/server'], + }, + /******* React Suspense Test Utils *******/ { bundleTypes: [NODE_ES2015], diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index ebdbf6cf52..0bb9da231d 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -63,8 +63,8 @@ module.exports = [ 'react-dom/src/server/react-dom-server.node.js', 'react-dom/test-utils', 'react-dom/unstable_server-external-runtime', - 'react-server-dom-webpack/src/client/react-flight-dom-client.node.unbundled', - 'react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled', + 'react-server-dom-webpack/src/client/react-flight-dom-client.node', + 'react-server-dom-webpack/src/server/react-flight-dom-server.node', ], paths: [ 'react-dom', @@ -84,20 +84,13 @@ module.exports = [ 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', 'react-server-dom-webpack', - 'react-server-dom-webpack/client.node.unbundled', 'react-server-dom-webpack/server', - 'react-server-dom-webpack/server.node.unbundled', 'react-server-dom-webpack/static', - 'react-server-dom-webpack/static.node.unbundled', 'react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js', // react-server-dom-webpack/client.node 'react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js', // react-server-dom-webpack/client.node - 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerNode.js', - 'react-server-dom-webpack/src/client/react-flight-dom-client.node.unbundled', - 'react-server-dom-webpack/src/server/react-flight-dom-server.node.unbundled', + 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack.js', + 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackServer.js', 'react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js', // react-server-dom-webpack/src/server/react-flight-dom-server.node - 'react-devtools', - 'react-devtools-core', - 'react-devtools-shell', 'react-devtools-shared', 'shared/ReactDOMSharedInternals', 'react-server/src/ReactFlightServerConfigDebugNode.js', @@ -240,6 +233,49 @@ module.exports = [ isFlowTyped: true, isServerSupported: true, }, + { + shortName: 'dom-node-unbundled', + entryPoints: [ + 'react-server-dom-unbundled/src/client/react-flight-dom-client.node', + 'react-server-dom-unbundled/src/server/react-flight-dom-server.node', + ], + paths: [ + 'react-dom', + 'react-dom-bindings', + 'react-dom/client', + 'react-dom/profiling', + 'react-dom/server', + 'react-dom/server.node', + 'react-dom/static', + 'react-dom/static.node', + 'react-dom/src/server/react-dom-server.node', + 'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node + 'react-dom/src/server/ReactDOMFizzStaticNode.js', + 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js', + 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js', + 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js', + 'react-server-dom-unbundled', + 'react-server-dom-unbundled/client', + 'react-server-dom-unbundled/server', + 'react-server-dom-unbundled/server.node', + 'react-server-dom-unbundled/static', + 'react-server-dom-unbundled/static.node', + 'react-server-dom-unbundled/src/client/ReactFlightDOMClientEdge.js', // react-server-dom-unbundled/client.node + 'react-server-dom-unbundled/src/client/ReactFlightDOMClientNode.js', // react-server-dom-unbundled/client.node + 'react-server-dom-unbundled/src/client/ReactFlightClientConfigBundlerNode.js', + 'react-server-dom-unbundled/src/client/react-flight-dom-client.node', + 'react-server-dom-unbundled/src/server/react-flight-dom-server.node', + 'react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js', // react-server-dom-unbundled/src/server/react-flight-dom-server.node + 'react-devtools', + 'react-devtools-core', + 'react-devtools-shell', + 'react-devtools-shared', + 'shared/ReactDOMSharedInternals', + 'react-server/src/ReactFlightServerConfigDebugNode.js', + ], + isFlowTyped: true, + isServerSupported: true, + }, { shortName: 'dom-bun', entryPoints: ['react-dom/src/server/react-dom-server.bun.js'],