From e6b5f7ea2ed21eb549550a4963455fea541e8738 Mon Sep 17 00:00:00 2001
From: Fran Dios
Date: Thu, 9 Dec 2021 16:06:44 +0900
Subject: [PATCH 01/87] Start react-server-dom-vite as a copy of the Webpack
implementation
---
.eslintrc.js | 8 +
ReactVersions.js | 1 +
...FlightClientHostConfig.dom-browser-vite.js | 12 +
.../ReactFlightClientHostConfig.dom-vite.js | 12 +
.../ReactFiberHostConfig.dom-browser-vite.js | 10 +
.../forks/ReactFiberHostConfig.dom-vite.js | 10 +
packages/react-server-dom-vite/README.md | 5 +
.../react-server-dom-vite/esm/package.json | 3 +
.../esm/react-server-dom-vite-node-loader.js | 10 +
packages/react-server-dom-vite/index.js | 10 +
.../react-server-dom-vite/node-register.js | 10 +
.../npm/esm/package.json | 3 +
packages/react-server-dom-vite/npm/index.js | 7 +
.../npm/node-register.js | 3 +
packages/react-server-dom-vite/npm/plugin.js | 3 +
.../npm/writer.browser.server.js | 7 +
packages/react-server-dom-vite/npm/writer.js | 6 +
.../npm/writer.node.server.js | 7 +
packages/react-server-dom-vite/package.json | 66 +++
packages/react-server-dom-vite/plugin.js | 10 +
.../ReactFlightClientWebpackBundlerConfig.js | 74 +++
.../src/ReactFlightDOMClient.js | 84 +++
.../src/ReactFlightDOMServerBrowser.js | 51 ++
.../src/ReactFlightDOMServerNode.js | 59 ++
.../ReactFlightServerWebpackBundlerConfig.js | 48 ++
.../src/ReactFlightWebpackNodeLoader.js | 267 +++++++++
.../src/ReactFlightWebpackNodeRegister.js | 95 +++
.../src/ReactFlightWebpackPlugin.js | 358 ++++++++++++
.../src/__tests__/ReactFlightDOM-test.js | 548 ++++++++++++++++++
.../__tests__/ReactFlightDOMBrowser-test.js | 343 +++++++++++
.../writer.browser.server.js | 10 +
packages/react-server-dom-vite/writer.js | 13 +
.../writer.node.server.js | 10 +
...eactFlightServerConfig.dom-browser-vite.js | 11 +
.../forks/ReactFlightServerConfig.dom-vite.js | 11 +
...eactServerFormatConfig.dom-browser-vite.js | 10 +
.../forks/ReactServerFormatConfig.dom-vite.js | 10 +
...eactServerStreamConfig.dom-browser-vite.js | 10 +
.../forks/ReactServerStreamConfig.dom-vite.js | 10 +
scripts/rollup/bundles.js | 64 ++
scripts/rollup/validate/index.js | 2 +
scripts/shared/inlinedHostConfigs.js | 43 ++
42 files changed, 2334 insertions(+)
create mode 100644 packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser-vite.js
create mode 100644 packages/react-client/src/forks/ReactFlightClientHostConfig.dom-vite.js
create mode 100644 packages/react-reconciler/src/forks/ReactFiberHostConfig.dom-browser-vite.js
create mode 100644 packages/react-reconciler/src/forks/ReactFiberHostConfig.dom-vite.js
create mode 100644 packages/react-server-dom-vite/README.md
create mode 100644 packages/react-server-dom-vite/esm/package.json
create mode 100644 packages/react-server-dom-vite/esm/react-server-dom-vite-node-loader.js
create mode 100644 packages/react-server-dom-vite/index.js
create mode 100644 packages/react-server-dom-vite/node-register.js
create mode 100644 packages/react-server-dom-vite/npm/esm/package.json
create mode 100644 packages/react-server-dom-vite/npm/index.js
create mode 100644 packages/react-server-dom-vite/npm/node-register.js
create mode 100644 packages/react-server-dom-vite/npm/plugin.js
create mode 100644 packages/react-server-dom-vite/npm/writer.browser.server.js
create mode 100644 packages/react-server-dom-vite/npm/writer.js
create mode 100644 packages/react-server-dom-vite/npm/writer.node.server.js
create mode 100644 packages/react-server-dom-vite/package.json
create mode 100644 packages/react-server-dom-vite/plugin.js
create mode 100644 packages/react-server-dom-vite/src/ReactFlightClientWebpackBundlerConfig.js
create mode 100644 packages/react-server-dom-vite/src/ReactFlightDOMClient.js
create mode 100644 packages/react-server-dom-vite/src/ReactFlightDOMServerBrowser.js
create mode 100644 packages/react-server-dom-vite/src/ReactFlightDOMServerNode.js
create mode 100644 packages/react-server-dom-vite/src/ReactFlightServerWebpackBundlerConfig.js
create mode 100644 packages/react-server-dom-vite/src/ReactFlightWebpackNodeLoader.js
create mode 100644 packages/react-server-dom-vite/src/ReactFlightWebpackNodeRegister.js
create mode 100644 packages/react-server-dom-vite/src/ReactFlightWebpackPlugin.js
create mode 100644 packages/react-server-dom-vite/src/__tests__/ReactFlightDOM-test.js
create mode 100644 packages/react-server-dom-vite/src/__tests__/ReactFlightDOMBrowser-test.js
create mode 100644 packages/react-server-dom-vite/writer.browser.server.js
create mode 100644 packages/react-server-dom-vite/writer.js
create mode 100644 packages/react-server-dom-vite/writer.node.server.js
create mode 100644 packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-vite.js
create mode 100644 packages/react-server/src/forks/ReactFlightServerConfig.dom-vite.js
create mode 100644 packages/react-server/src/forks/ReactServerFormatConfig.dom-browser-vite.js
create mode 100644 packages/react-server/src/forks/ReactServerFormatConfig.dom-vite.js
create mode 100644 packages/react-server/src/forks/ReactServerStreamConfig.dom-browser-vite.js
create mode 100644 packages/react-server/src/forks/ReactServerStreamConfig.dom-vite.js
diff --git a/.eslintrc.js b/.eslintrc.js
index 0bc90137a5c..659a893fcf1 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -144,6 +144,7 @@ module.exports = {
'packages/react-fs/**/*.js',
'packages/react-refresh/**/*.js',
'packages/react-server-dom-webpack/**/*.js',
+ 'packages/react-server-dom-vite/**/*.js',
'packages/react-test-renderer/**/*.js',
'packages/react-debug-tools/**/*.js',
'packages/react-devtools-extensions/**/*.js',
@@ -255,6 +256,13 @@ module.exports = {
__webpack_require__: 'readonly',
},
},
+ {
+ files: ['packages/react-server-dom-vite/**/*.js'],
+ globals: {
+ __webpack_chunk_load__: 'readonly',
+ __webpack_require__: 'readonly',
+ },
+ },
{
files: ['packages/scheduler/**/*.js'],
globals: {
diff --git a/ReactVersions.js b/ReactVersions.js
index 32dde5ab521..dba58625916 100644
--- a/ReactVersions.js
+++ b/ReactVersions.js
@@ -48,6 +48,7 @@ const experimentalPackages = [
'react-fs',
'react-pg',
'react-server-dom-webpack',
+ 'react-server-dom-vite',
];
module.exports = {
diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser-vite.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser-vite.js
new file mode 100644
index 00000000000..7e17cffce53
--- /dev/null
+++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser-vite.js
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
+export * from 'react-client/src/ReactFlightClientHostConfigStream';
+export * from 'react-server-dom-vite/src/ReactFlightClientWebpackBundlerConfig';
diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-vite.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-vite.js
new file mode 100644
index 00000000000..7e17cffce53
--- /dev/null
+++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-vite.js
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
+export * from 'react-client/src/ReactFlightClientHostConfigStream';
+export * from 'react-server-dom-vite/src/ReactFlightClientWebpackBundlerConfig';
diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.dom-browser-vite.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.dom-browser-vite.js
new file mode 100644
index 00000000000..d830c8501be
--- /dev/null
+++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.dom-browser-vite.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from 'react-dom/src/client/ReactDOMHostConfig';
diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.dom-vite.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.dom-vite.js
new file mode 100644
index 00000000000..d830c8501be
--- /dev/null
+++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.dom-vite.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from 'react-dom/src/client/ReactDOMHostConfig';
diff --git a/packages/react-server-dom-vite/README.md b/packages/react-server-dom-vite/README.md
new file mode 100644
index 00000000000..fd0771f4d7b
--- /dev/null
+++ b/packages/react-server-dom-vite/README.md
@@ -0,0 +1,5 @@
+# react-server-dom-vite
+
+Experimental React Flight bindings for DOM using Vite.
+
+**Use it at your own risk.**
diff --git a/packages/react-server-dom-vite/esm/package.json b/packages/react-server-dom-vite/esm/package.json
new file mode 100644
index 00000000000..3dbc1ca591c
--- /dev/null
+++ b/packages/react-server-dom-vite/esm/package.json
@@ -0,0 +1,3 @@
+{
+ "type": "module"
+}
diff --git a/packages/react-server-dom-vite/esm/react-server-dom-vite-node-loader.js b/packages/react-server-dom-vite/esm/react-server-dom-vite-node-loader.js
new file mode 100644
index 00000000000..d7a01f6f221
--- /dev/null
+++ b/packages/react-server-dom-vite/esm/react-server-dom-vite-node-loader.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Facebook, Inc. and its 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/ReactFlightWebpackNodeLoader.js';
diff --git a/packages/react-server-dom-vite/index.js b/packages/react-server-dom-vite/index.js
new file mode 100644
index 00000000000..67e9a28e029
--- /dev/null
+++ b/packages/react-server-dom-vite/index.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Facebook, Inc. and its 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/ReactFlightDOMClient';
diff --git a/packages/react-server-dom-vite/node-register.js b/packages/react-server-dom-vite/node-register.js
new file mode 100644
index 00000000000..03754438bf3
--- /dev/null
+++ b/packages/react-server-dom-vite/node-register.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Facebook, Inc. and its 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/ReactFlightWebpackNodeRegister';
diff --git a/packages/react-server-dom-vite/npm/esm/package.json b/packages/react-server-dom-vite/npm/esm/package.json
new file mode 100644
index 00000000000..3dbc1ca591c
--- /dev/null
+++ b/packages/react-server-dom-vite/npm/esm/package.json
@@ -0,0 +1,3 @@
+{
+ "type": "module"
+}
diff --git a/packages/react-server-dom-vite/npm/index.js b/packages/react-server-dom-vite/npm/index.js
new file mode 100644
index 00000000000..8000702be9e
--- /dev/null
+++ b/packages/react-server-dom-vite/npm/index.js
@@ -0,0 +1,7 @@
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./cjs/react-server-dom-vite.production.min.js');
+} else {
+ module.exports = require('./cjs/react-server-dom-vite.development.js');
+}
diff --git a/packages/react-server-dom-vite/npm/node-register.js b/packages/react-server-dom-vite/npm/node-register.js
new file mode 100644
index 00000000000..5d1503fe004
--- /dev/null
+++ b/packages/react-server-dom-vite/npm/node-register.js
@@ -0,0 +1,3 @@
+'use strict';
+
+module.exports = require('./cjs/react-server-dom-vite-node-register.js');
diff --git a/packages/react-server-dom-vite/npm/plugin.js b/packages/react-server-dom-vite/npm/plugin.js
new file mode 100644
index 00000000000..ffb0bbe2988
--- /dev/null
+++ b/packages/react-server-dom-vite/npm/plugin.js
@@ -0,0 +1,3 @@
+'use strict';
+
+module.exports = require('./cjs/react-server-dom-vite-plugin.js');
diff --git a/packages/react-server-dom-vite/npm/writer.browser.server.js b/packages/react-server-dom-vite/npm/writer.browser.server.js
new file mode 100644
index 00000000000..65ab3bb72ba
--- /dev/null
+++ b/packages/react-server-dom-vite/npm/writer.browser.server.js
@@ -0,0 +1,7 @@
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./cjs/react-server-dom-vite-writer.browser.production.min.server.js');
+} else {
+ module.exports = require('./cjs/react-server-dom-vite-writer.browser.development.server.js');
+}
diff --git a/packages/react-server-dom-vite/npm/writer.js b/packages/react-server-dom-vite/npm/writer.js
new file mode 100644
index 00000000000..13a632e6411
--- /dev/null
+++ b/packages/react-server-dom-vite/npm/writer.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-vite/npm/writer.node.server.js b/packages/react-server-dom-vite/npm/writer.node.server.js
new file mode 100644
index 00000000000..ee69b99c15b
--- /dev/null
+++ b/packages/react-server-dom-vite/npm/writer.node.server.js
@@ -0,0 +1,7 @@
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./cjs/react-server-dom-vite-writer.node.production.min.server.js');
+} else {
+ module.exports = require('./cjs/react-server-dom-vite-writer.node.development.server.js');
+}
diff --git a/packages/react-server-dom-vite/package.json b/packages/react-server-dom-vite/package.json
new file mode 100644
index 00000000000..3b5b9cd2156
--- /dev/null
+++ b/packages/react-server-dom-vite/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "react-server-dom-vite",
+ "description": "React Server Components bindings for DOM using Vite. This is intended to be integrated into meta-frameworks. It is not intended to be imported directly.",
+ "version": "0.1.0",
+ "keywords": [
+ "react"
+ ],
+ "homepage": "https://reactjs.org/",
+ "bugs": "https://github.com/facebook/react/issues",
+ "license": "MIT",
+ "files": [
+ "LICENSE",
+ "README.md",
+ "build-info.json",
+ "index.js",
+ "plugin.js",
+ "writer.js",
+ "writer.browser.server.js",
+ "writer.node.server.js",
+ "node-register.js",
+ "cjs/",
+ "umd/",
+ "esm/"
+ ],
+ "exports": {
+ ".": "./index.js",
+ "./plugin": "./plugin.js",
+ "./writer": {
+ "react-server": {
+ "node": "./writer.node.server.js",
+ "browser": "./writer.browser.server.js"
+ },
+ "default": "./writer.js"
+ },
+ "./writer.node.server": "./writer.node.server.js",
+ "./writer.browser.server": "./writer.browser.server.js",
+ "./node-loader": "./esm/react-server-dom-vite-node-loader.js",
+ "./node-register": "./node-register.js",
+ "./package.json": "./package.json"
+ },
+ "main": "index.js",
+ "repository": {
+ "type" : "git",
+ "url" : "https://github.com/facebook/react.git",
+ "directory": "packages/react-server-dom-vite"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0",
+ "react-dom": "^17.0.0",
+ "webpack": "^5.59.0"
+ },
+ "dependencies": {
+ "acorn": "^6.2.1",
+ "neo-async": "^2.6.1",
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1"
+ },
+ "browserify": {
+ "transform": [
+ "loose-envify"
+ ]
+ }
+}
diff --git a/packages/react-server-dom-vite/plugin.js b/packages/react-server-dom-vite/plugin.js
new file mode 100644
index 00000000000..64c004e7e0f
--- /dev/null
+++ b/packages/react-server-dom-vite/plugin.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Facebook, Inc. and its 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 {default} from './src/ReactFlightWebpackPlugin';
diff --git a/packages/react-server-dom-vite/src/ReactFlightClientWebpackBundlerConfig.js b/packages/react-server-dom-vite/src/ReactFlightClientWebpackBundlerConfig.js
new file mode 100644
index 00000000000..f3c4e1bf1c1
--- /dev/null
+++ b/packages/react-server-dom-vite/src/ReactFlightClientWebpackBundlerConfig.js
@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) Facebook, Inc. and its 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 opaque type ModuleMetaData = {
+ id: string,
+ chunks: Array,
+ name: string,
+};
+
+// eslint-disable-next-line no-unused-vars
+export opaque type ModuleReference = ModuleMetaData;
+
+export function resolveModuleReference(
+ moduleData: ModuleMetaData,
+): ModuleReference {
+ return moduleData;
+}
+
+// The chunk cache contains all the chunks we've preloaded so far.
+// If they're still pending they're a thenable. This map also exists
+// in Webpack but unfortunately it's not exposed so we have to
+// replicate it in user space. null means that it has already loaded.
+const chunkCache: Map | Error> = new Map();
+
+// Start preloading the modules since we might need them soon.
+// This function doesn't suspend.
+export function preloadModule(moduleData: ModuleReference): void {
+ const chunks = moduleData.chunks;
+ for (let i = 0; i < chunks.length; i++) {
+ const chunkId = chunks[i];
+ const entry = chunkCache.get(chunkId);
+ if (entry === undefined) {
+ const thenable = __webpack_chunk_load__(chunkId);
+ const resolve = chunkCache.set.bind(chunkCache, chunkId, null);
+ const reject = chunkCache.set.bind(chunkCache, chunkId);
+ thenable.then(resolve, reject);
+ chunkCache.set(chunkId, thenable);
+ }
+ }
+}
+
+// Actually require the module or suspend if it's not yet ready.
+// Increase priority if necessary.
+export function requireModule(moduleData: ModuleReference): T {
+ const chunks = moduleData.chunks;
+ for (let i = 0; i < chunks.length; i++) {
+ const chunkId = chunks[i];
+ const entry = chunkCache.get(chunkId);
+ if (entry !== null) {
+ // We assume that preloadModule has been called before.
+ // So we don't expect to see entry being undefined here, that's an error.
+ // Let's throw either an error or the Promise.
+ throw entry;
+ }
+ }
+ const moduleExports = __webpack_require__(moduleData.id);
+ if (moduleData.name === '*') {
+ // This is a placeholder value that represents that the caller imported this
+ // as a CommonJS module as is.
+ return moduleExports;
+ }
+ if (moduleData.name === '') {
+ // This is a placeholder value that represents that the caller accessed the
+ // default property of this if it was an ESM interop module.
+ return moduleExports.__esModule ? moduleExports.default : moduleExports;
+ }
+ return moduleExports[moduleData.name];
+}
diff --git a/packages/react-server-dom-vite/src/ReactFlightDOMClient.js b/packages/react-server-dom-vite/src/ReactFlightDOMClient.js
new file mode 100644
index 00000000000..9c9d17c11f5
--- /dev/null
+++ b/packages/react-server-dom-vite/src/ReactFlightDOMClient.js
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) Facebook, Inc. and its 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 {Response as FlightResponse} from 'react-client/src/ReactFlightClientStream';
+
+import {
+ createResponse,
+ reportGlobalError,
+ processStringChunk,
+ processBinaryChunk,
+ close,
+} from 'react-client/src/ReactFlightClientStream';
+
+function startReadingFromStream(
+ response: FlightResponse,
+ stream: ReadableStream,
+): void {
+ const reader = stream.getReader();
+ function progress({done, value}) {
+ if (done) {
+ close(response);
+ return;
+ }
+ const buffer: Uint8Array = (value: any);
+ processBinaryChunk(response, buffer);
+ return reader.read().then(progress, error);
+ }
+ function error(e) {
+ reportGlobalError(response, e);
+ }
+ reader.read().then(progress, error);
+}
+
+function createFromReadableStream(stream: ReadableStream): FlightResponse {
+ const response: FlightResponse = createResponse();
+ startReadingFromStream(response, stream);
+ return response;
+}
+
+function createFromFetch(
+ promiseForResponse: Promise,
+): FlightResponse {
+ const response: FlightResponse = createResponse();
+ promiseForResponse.then(
+ function(r) {
+ startReadingFromStream(response, (r.body: any));
+ },
+ function(e) {
+ reportGlobalError(response, e);
+ },
+ );
+ return response;
+}
+
+function createFromXHR(request: XMLHttpRequest): FlightResponse {
+ const response: FlightResponse = createResponse();
+ let processedLength = 0;
+ function progress(e: ProgressEvent): void {
+ const chunk = request.responseText;
+ processStringChunk(response, chunk, processedLength);
+ processedLength = chunk.length;
+ }
+ function load(e: ProgressEvent): void {
+ progress(e);
+ close(response);
+ }
+ function error(e: ProgressEvent): void {
+ reportGlobalError(response, new TypeError('Network error'));
+ }
+ request.addEventListener('progress', progress);
+ request.addEventListener('load', load);
+ request.addEventListener('error', error);
+ request.addEventListener('abort', error);
+ request.addEventListener('timeout', error);
+ return response;
+}
+
+export {createFromXHR, createFromFetch, createFromReadableStream};
diff --git a/packages/react-server-dom-vite/src/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-vite/src/ReactFlightDOMServerBrowser.js
new file mode 100644
index 00000000000..9d632dfb071
--- /dev/null
+++ b/packages/react-server-dom-vite/src/ReactFlightDOMServerBrowser.js
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) Facebook, Inc. and its 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 {ReactModel} from 'react-server/src/ReactFlightServer';
+import type {BundlerConfig} from './ReactFlightServerWebpackBundlerConfig';
+
+import {
+ createRequest,
+ startWork,
+ startFlowing,
+} from 'react-server/src/ReactFlightServer';
+
+type Options = {
+ onError?: (error: mixed) => void,
+};
+
+function renderToReadableStream(
+ model: ReactModel,
+ webpackMap: BundlerConfig,
+ options?: Options,
+): ReadableStream {
+ const request = createRequest(
+ model,
+ webpackMap,
+ options ? options.onError : undefined,
+ );
+ const stream = new ReadableStream({
+ start(controller) {
+ startWork(request);
+ },
+ pull(controller) {
+ // Pull is called immediately even if the stream is not passed to anything.
+ // That's buffering too early. We want to start buffering once the stream
+ // is actually used by something so we can give it the best result possible
+ // at that point.
+ if (stream.locked) {
+ startFlowing(request, controller);
+ }
+ },
+ cancel(reason) {},
+ });
+ return stream;
+}
+
+export {renderToReadableStream};
diff --git a/packages/react-server-dom-vite/src/ReactFlightDOMServerNode.js b/packages/react-server-dom-vite/src/ReactFlightDOMServerNode.js
new file mode 100644
index 00000000000..5f992d4b03e
--- /dev/null
+++ b/packages/react-server-dom-vite/src/ReactFlightDOMServerNode.js
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) Facebook, Inc. and its 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 {ReactModel} from 'react-server/src/ReactFlightServer';
+import type {BundlerConfig} from './ReactFlightServerWebpackBundlerConfig';
+import type {Writable} from 'stream';
+
+import {
+ createRequest,
+ startWork,
+ startFlowing,
+} from 'react-server/src/ReactFlightServer';
+
+function createDrainHandler(destination, request) {
+ return () => startFlowing(request, destination);
+}
+
+type Options = {
+ onError?: (error: mixed) => void,
+};
+
+type Controls = {|
+ pipe(destination: T): T,
+|};
+
+function renderToPipeableStream(
+ model: ReactModel,
+ webpackMap: BundlerConfig,
+ options?: Options,
+): Controls {
+ const request = createRequest(
+ model,
+ webpackMap,
+ options ? options.onError : undefined,
+ );
+ let hasStartedFlowing = false;
+ startWork(request);
+ return {
+ pipe(destination: T): T {
+ if (hasStartedFlowing) {
+ throw new Error(
+ 'React currently only supports piping to one writable stream.',
+ );
+ }
+ hasStartedFlowing = true;
+ startFlowing(request, destination);
+ destination.on('drain', createDrainHandler(destination, request));
+ return destination;
+ },
+ };
+}
+
+export {renderToPipeableStream};
diff --git a/packages/react-server-dom-vite/src/ReactFlightServerWebpackBundlerConfig.js b/packages/react-server-dom-vite/src/ReactFlightServerWebpackBundlerConfig.js
new file mode 100644
index 00000000000..c8469eeba80
--- /dev/null
+++ b/packages/react-server-dom-vite/src/ReactFlightServerWebpackBundlerConfig.js
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+type WebpackMap = {
+ [filepath: string]: {
+ [name: string]: ModuleMetaData,
+ },
+};
+
+export type BundlerConfig = WebpackMap;
+
+// eslint-disable-next-line no-unused-vars
+export type ModuleReference = {
+ $$typeof: Symbol,
+ filepath: string,
+ name: string,
+};
+
+export type ModuleMetaData = {
+ id: string,
+ chunks: Array,
+ name: string,
+};
+
+export type ModuleKey = string;
+
+const MODULE_TAG = Symbol.for('react.module.reference');
+
+export function getModuleKey(reference: ModuleReference): ModuleKey {
+ return reference.filepath + '#' + reference.name;
+}
+
+export function isModuleReference(reference: Object): boolean {
+ return reference.$$typeof === MODULE_TAG;
+}
+
+export function resolveModuleMetaData(
+ config: BundlerConfig,
+ moduleReference: ModuleReference,
+): ModuleMetaData {
+ return config[moduleReference.filepath][moduleReference.name];
+}
diff --git a/packages/react-server-dom-vite/src/ReactFlightWebpackNodeLoader.js b/packages/react-server-dom-vite/src/ReactFlightWebpackNodeLoader.js
new file mode 100644
index 00000000000..db618f0eb17
--- /dev/null
+++ b/packages/react-server-dom-vite/src/ReactFlightWebpackNodeLoader.js
@@ -0,0 +1,267 @@
+/**
+ * Copyright (c) Facebook, Inc. and its 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 acorn from 'acorn';
+
+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 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.',
+ );
+ }
+ }
+ const resolved = await defaultResolve(specifier, context, defaultResolve);
+ if (resolved.url.endsWith('.server.js')) {
+ const parentURL = context.parentURL;
+ if (parentURL && !parentURL.endsWith('.server.js')) {
+ let reason;
+ if (specifier.endsWith('.server.js')) {
+ reason = `"${specifier}"`;
+ } else {
+ reason = `"${specifier}" (which expands to "${resolved.url}")`;
+ }
+ throw new Error(
+ `Cannot import ${reason} from "${parentURL}". ` +
+ 'By react-server convention, .server.js files can only be imported from other .server.js files. ' +
+ 'That way nobody accidentally sends these to the client by indirectly importing it.',
+ );
+ }
+ }
+ return resolved;
+}
+
+export async function getSource(
+ url: string,
+ context: GetSourceContext,
+ defaultGetSource: GetSourceFunction,
+) {
+ // We stash this in case we end up needing to resolve export * statements later.
+ stashedGetSource = defaultGetSource;
+ return defaultGetSource(url, context, defaultGetSource);
+}
+
+function addExportNames(names, node) {
+ 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 loadClientImport(
+ url: string,
+ defaultTransformSource: TransformSourceFunction,
+): Promise<{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,
+ );
+ return defaultTransformSource(
+ source,
+ {format: 'module', url},
+ defaultTransformSource,
+ );
+}
+
+async function parseExportNamesInto(
+ transformedSource: string,
+ names: Array,
+ parentURL: string,
+ defaultTransformSource,
+): Promise {
+ const {body} = acorn.parse(transformedSource, {
+ ecmaVersion: '2019',
+ sourceType: 'module',
+ });
+ 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 loadClientImport(url, defaultTransformSource);
+ if (typeof source !== 'string') {
+ throw new Error('Expected the transformed source to be a string.');
+ }
+ parseExportNamesInto(source, names, url, defaultTransformSource);
+ 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.specificers) {
+ const specificers = node.specificers;
+ for (let j = 0; j < specificers.length; j++) {
+ addExportNames(names, specificers[j].exported);
+ }
+ }
+ continue;
+ }
+ }
+}
+
+export async function transformSource(
+ source: Source,
+ context: TransformSourceContext,
+ defaultTransformSource: TransformSourceFunction,
+): Promise<{source: Source}> {
+ const transformed = await defaultTransformSource(
+ source,
+ context,
+ defaultTransformSource,
+ );
+ if (context.format === 'module' && context.url.endsWith('.client.js')) {
+ const transformedSource = transformed.source;
+ if (typeof transformedSource !== 'string') {
+ throw new Error('Expected source to have been transformed to a string.');
+ }
+
+ const names = [];
+ await parseExportNamesInto(
+ transformedSource,
+ names,
+ context.url,
+ defaultTransformSource,
+ );
+
+ let newSrc =
+ "const MODULE_REFERENCE = Symbol.for('react.module.reference');\n";
+ for (let i = 0; i < names.length; i++) {
+ const name = names[i];
+ if (name === 'default') {
+ newSrc += 'export default ';
+ } else {
+ newSrc += 'export const ' + name + ' = ';
+ }
+ newSrc += '{ $$typeof: MODULE_REFERENCE, filepath: ';
+ newSrc += JSON.stringify(context.url);
+ newSrc += ', name: ';
+ newSrc += JSON.stringify(name);
+ newSrc += '};\n';
+ }
+
+ return {source: newSrc};
+ }
+ return transformed;
+}
diff --git a/packages/react-server-dom-vite/src/ReactFlightWebpackNodeRegister.js b/packages/react-server-dom-vite/src/ReactFlightWebpackNodeRegister.js
new file mode 100644
index 00000000000..a5f889d3eb1
--- /dev/null
+++ b/packages/react-server-dom-vite/src/ReactFlightWebpackNodeRegister.js
@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) Facebook, Inc. and its 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 url = require('url');
+
+// $FlowFixMe
+const Module = require('module');
+
+module.exports = function register() {
+ const MODULE_REFERENCE = Symbol.for('react.module.reference');
+ const proxyHandlers = {
+ get: function(target, name, receiver) {
+ 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 'filepath':
+ return target.filepath;
+ case 'name':
+ return target.name;
+ // We need to special case this because createElement reads it if we pass this
+ // reference.
+ case 'defaultProps':
+ return undefined;
+ 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.
+ target.default = {
+ $$typeof: MODULE_REFERENCE,
+ filepath: target.filepath,
+ // This a placeholder value that tells the client to conditionally use the
+ // whole object or just the default export.
+ name: '',
+ };
+ return true;
+ }
+ let cachedReference = target[name];
+ if (!cachedReference) {
+ cachedReference = target[name] = {
+ $$typeof: MODULE_REFERENCE,
+ filepath: target.filepath,
+ name: name,
+ };
+ }
+ return cachedReference;
+ },
+ set: function() {
+ throw new Error('Cannot assign to a client module from a server module.');
+ },
+ };
+
+ (require: any).extensions['.client.js'] = function(module, path) {
+ const moduleId = url.pathToFileURL(path).href;
+ const moduleReference: {[string]: any} = {
+ $$typeof: MODULE_REFERENCE,
+ filepath: moduleId,
+ name: '*', // Represents the whole object instead of a particular import.
+ };
+ module.exports = new Proxy(moduleReference, proxyHandlers);
+ };
+
+ const originalResolveFilename = Module._resolveFilename;
+
+ Module._resolveFilename = function(request, parent, isMain, options) {
+ const resolved = originalResolveFilename.apply(this, arguments);
+ if (resolved.endsWith('.server.js')) {
+ if (
+ parent &&
+ parent.filename &&
+ !parent.filename.endsWith('.server.js')
+ ) {
+ let reason;
+ if (request.endsWith('.server.js')) {
+ reason = `"${request}"`;
+ } else {
+ reason = `"${request}" (which expands to "${resolved}")`;
+ }
+ throw new Error(
+ `Cannot import ${reason} from "${parent.filename}". ` +
+ 'By react-server convention, .server.js files can only be imported from other .server.js files. ' +
+ 'That way nobody accidentally sends these to the client by indirectly importing it.',
+ );
+ }
+ }
+ return resolved;
+ };
+};
diff --git a/packages/react-server-dom-vite/src/ReactFlightWebpackPlugin.js b/packages/react-server-dom-vite/src/ReactFlightWebpackPlugin.js
new file mode 100644
index 00000000000..0c49d293733
--- /dev/null
+++ b/packages/react-server-dom-vite/src/ReactFlightWebpackPlugin.js
@@ -0,0 +1,358 @@
+/**
+ * Copyright (c) Facebook, Inc. and its 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 {join} from 'path';
+import {pathToFileURL} from 'url';
+
+import asyncLib from 'neo-async';
+
+import ModuleDependency from 'webpack/lib/dependencies/ModuleDependency';
+import NullDependency from 'webpack/lib/dependencies/NullDependency';
+import Template from 'webpack/lib/Template';
+import {
+ sources,
+ WebpackError,
+ Compilation,
+ AsyncDependenciesBlock,
+} from 'webpack';
+
+import isArray from 'shared/isArray';
+
+class ClientReferenceDependency extends ModuleDependency {
+ constructor(request) {
+ super(request);
+ }
+
+ get type() {
+ return 'client-reference';
+ }
+}
+
+// This is the module that will be used to anchor all client references to.
+// I.e. it will have all the client files as async deps from this point on.
+// We use the Flight client implementation because you can't get to these
+// without the client runtime so it's the first time in the loading sequence
+// you might want them.
+const clientImportName = 'react-server-dom-webpack';
+const clientFileName = require.resolve('../');
+
+type ClientReferenceSearchPath = {
+ directory: string,
+ recursive?: boolean,
+ include: RegExp,
+ exclude?: RegExp,
+};
+
+type ClientReferencePath = string | ClientReferenceSearchPath;
+
+type Options = {
+ isServer: boolean,
+ clientReferences?: ClientReferencePath | $ReadOnlyArray,
+ chunkName?: string,
+ manifestFilename?: string,
+};
+
+const PLUGIN_NAME = 'React Server Plugin';
+
+export default class ReactFlightWebpackPlugin {
+ clientReferences: $ReadOnlyArray;
+ chunkName: string;
+ manifestFilename: string;
+
+ constructor(options: Options) {
+ if (!options || typeof options.isServer !== 'boolean') {
+ throw new Error(
+ PLUGIN_NAME + ': You must specify the isServer option as a boolean.',
+ );
+ }
+ if (options.isServer) {
+ throw new Error('TODO: Implement the server compiler.');
+ }
+ if (!options.clientReferences) {
+ this.clientReferences = [
+ {
+ directory: '.',
+ recursive: true,
+ include: /\.client\.(js|ts|jsx|tsx)$/,
+ },
+ ];
+ } else if (
+ typeof options.clientReferences === 'string' ||
+ !isArray(options.clientReferences)
+ ) {
+ this.clientReferences = [(options.clientReferences: $FlowFixMe)];
+ } else {
+ this.clientReferences = options.clientReferences;
+ }
+ if (typeof options.chunkName === 'string') {
+ this.chunkName = options.chunkName;
+ if (!/\[(index|request)\]/.test(this.chunkName)) {
+ this.chunkName += '[index]';
+ }
+ } else {
+ this.chunkName = 'client[index]';
+ }
+ this.manifestFilename =
+ options.manifestFilename || 'react-client-manifest.json';
+ }
+
+ apply(compiler: any) {
+ const _this = this;
+ let resolvedClientReferences;
+ let clientFileNameFound = false;
+
+ // Find all client files on the file system
+ compiler.hooks.beforeCompile.tapAsync(
+ PLUGIN_NAME,
+ ({contextModuleFactory}, callback) => {
+ const contextResolver = compiler.resolverFactory.get('context', {});
+
+ _this.resolveAllClientFiles(
+ compiler.context,
+ contextResolver,
+ compiler.inputFileSystem,
+ contextModuleFactory,
+ function(err, resolvedClientRefs) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ resolvedClientReferences = resolvedClientRefs;
+ callback();
+ },
+ );
+ },
+ );
+
+ compiler.hooks.thisCompilation.tap(
+ PLUGIN_NAME,
+ (compilation, {normalModuleFactory}) => {
+ compilation.dependencyFactories.set(
+ ClientReferenceDependency,
+ normalModuleFactory,
+ );
+ compilation.dependencyTemplates.set(
+ ClientReferenceDependency,
+ new NullDependency.Template(),
+ );
+
+ const handler = parser => {
+ // We need to add all client references as dependency of something in the graph so
+ // Webpack knows which entries need to know about the relevant chunks and include the
+ // map in their runtime. The things that actually resolves the dependency is the Flight
+ // client runtime. So we add them as a dependency of the Flight client runtime.
+ // Anything that imports the runtime will be made aware of these chunks.
+ parser.hooks.program.tap(PLUGIN_NAME, () => {
+ const module = parser.state.module;
+
+ if (module.resource !== clientFileName) {
+ return;
+ }
+
+ clientFileNameFound = true;
+
+ if (resolvedClientReferences) {
+ for (let i = 0; i < resolvedClientReferences.length; i++) {
+ const dep = resolvedClientReferences[i];
+
+ const chunkName = _this.chunkName
+ .replace(/\[index\]/g, '' + i)
+ .replace(/\[request\]/g, Template.toPath(dep.userRequest));
+
+ const block = new AsyncDependenciesBlock(
+ {
+ name: chunkName,
+ },
+ null,
+ dep.request,
+ );
+
+ block.addDependency(dep);
+ module.addBlock(block);
+ }
+ }
+ });
+ };
+
+ normalModuleFactory.hooks.parser
+ .for('javascript/auto')
+ .tap('HarmonyModulesPlugin', handler);
+
+ normalModuleFactory.hooks.parser
+ .for('javascript/esm')
+ .tap('HarmonyModulesPlugin', handler);
+
+ normalModuleFactory.hooks.parser
+ .for('javascript/dynamic')
+ .tap('HarmonyModulesPlugin', handler);
+ },
+ );
+
+ compiler.hooks.make.tap(PLUGIN_NAME, compilation => {
+ compilation.hooks.processAssets.tap(
+ {
+ name: PLUGIN_NAME,
+ stage: Compilation.PROCESS_ASSETS_STAGE_REPORT,
+ },
+ function() {
+ if (clientFileNameFound === false) {
+ compilation.warnings.push(
+ new WebpackError(
+ `Client runtime at ${clientImportName} was not found. React Server Components module map file ${_this.manifestFilename} was not created.`,
+ ),
+ );
+ return;
+ }
+
+ const json = {};
+ compilation.chunkGroups.forEach(function(chunkGroup) {
+ const chunkIds = chunkGroup.chunks.map(function(c) {
+ return c.id;
+ });
+
+ function recordModule(id, module) {
+ // TODO: Hook into deps instead of the target module.
+ // That way we know by the type of dep whether to include.
+ // It also resolves conflicts when the same module is in multiple chunks.
+
+ if (!/\.client\.(js|ts)x?$/.test(module.resource)) {
+ return;
+ }
+
+ const moduleProvidedExports = compilation.moduleGraph
+ .getExportsInfo(module)
+ .getProvidedExports();
+
+ const moduleExports = {};
+ ['', '*']
+ .concat(
+ Array.isArray(moduleProvidedExports)
+ ? moduleProvidedExports
+ : [],
+ )
+ .forEach(function(name) {
+ moduleExports[name] = {
+ id,
+ chunks: chunkIds,
+ name: name,
+ };
+ });
+ const href = pathToFileURL(module.resource).href;
+
+ if (href !== undefined) {
+ json[href] = moduleExports;
+ }
+ }
+
+ chunkGroup.chunks.forEach(function(chunk) {
+ const chunkModules = compilation.chunkGraph.getChunkModulesIterable(
+ chunk,
+ );
+
+ Array.from(chunkModules).forEach(function(module) {
+ const moduleId = compilation.chunkGraph.getModuleId(module);
+
+ recordModule(moduleId, module);
+ // If this is a concatenation, register each child to the parent ID.
+ if (module.modules) {
+ module.modules.forEach(concatenatedMod => {
+ recordModule(moduleId, concatenatedMod);
+ });
+ }
+ });
+ });
+ });
+
+ const output = JSON.stringify(json, null, 2);
+ compilation.emitAsset(
+ _this.manifestFilename,
+ new sources.RawSource(output, false),
+ );
+ },
+ );
+ });
+ }
+
+ // This attempts to replicate the dynamic file path resolution used for other wildcard
+ // resolution in Webpack is using.
+ resolveAllClientFiles(
+ context: string,
+ contextResolver: any,
+ fs: any,
+ contextModuleFactory: any,
+ callback: (
+ err: null | Error,
+ result?: $ReadOnlyArray,
+ ) => void,
+ ) {
+ asyncLib.map(
+ this.clientReferences,
+ (
+ clientReferencePath: string | ClientReferenceSearchPath,
+ cb: (
+ err: null | Error,
+ result?: $ReadOnlyArray,
+ ) => void,
+ ): void => {
+ if (typeof clientReferencePath === 'string') {
+ cb(null, [new ClientReferenceDependency(clientReferencePath)]);
+ return;
+ }
+ const clientReferenceSearch: ClientReferenceSearchPath = clientReferencePath;
+ contextResolver.resolve(
+ {},
+ context,
+ clientReferencePath.directory,
+ {},
+ (err, resolvedDirectory) => {
+ if (err) return cb(err);
+ const options = {
+ resource: resolvedDirectory,
+ resourceQuery: '',
+ recursive:
+ clientReferenceSearch.recursive === undefined
+ ? true
+ : clientReferenceSearch.recursive,
+ regExp: clientReferenceSearch.include,
+ include: undefined,
+ exclude: clientReferenceSearch.exclude,
+ };
+ contextModuleFactory.resolveDependencies(
+ fs,
+ options,
+ (err2: null | Error, deps: Array) => {
+ if (err2) return cb(err2);
+ const clientRefDeps = deps.map(dep => {
+ // use userRequest instead of request. request always end with undefined which is wrong
+ const request = join(resolvedDirectory, dep.userRequest);
+ const clientRefDep = new ClientReferenceDependency(request);
+ clientRefDep.userRequest = dep.userRequest;
+ return clientRefDep;
+ });
+ cb(null, clientRefDeps);
+ },
+ );
+ },
+ );
+ },
+ (
+ err: null | Error,
+ result: $ReadOnlyArray<$ReadOnlyArray>,
+ ): void => {
+ if (err) return callback(err);
+ const flat = [];
+ for (let i = 0; i < result.length; i++) {
+ flat.push.apply(flat, result[i]);
+ }
+ callback(null, flat);
+ },
+ );
+ }
+}
diff --git a/packages/react-server-dom-vite/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-vite/src/__tests__/ReactFlightDOM-test.js
new file mode 100644
index 00000000000..bcae6c50f7c
--- /dev/null
+++ b/packages/react-server-dom-vite/src/__tests__/ReactFlightDOM-test.js
@@ -0,0 +1,548 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+// Polyfills for test environment
+global.ReadableStream = require('web-streams-polyfill/ponyfill/es6').ReadableStream;
+global.TextDecoder = require('util').TextDecoder;
+
+// Don't wait before processing work on the server.
+// TODO: we can replace this with FlightServer.act().
+global.setImmediate = cb => cb();
+
+let webpackModuleIdx = 0;
+let webpackModules = {};
+let webpackMap = {};
+global.__webpack_require__ = function(id) {
+ return webpackModules[id];
+};
+
+let act;
+let Stream;
+let React;
+let ReactDOM;
+let ReactServerDOMWriter;
+let ReactServerDOMReader;
+
+describe('ReactFlightDOM', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ webpackModules = {};
+ webpackMap = {};
+ act = require('jest-react').act;
+ Stream = require('stream');
+ React = require('react');
+ ReactDOM = require('react-dom');
+ ReactServerDOMWriter = require('react-server-dom-webpack/writer.node.server');
+ ReactServerDOMReader = require('react-server-dom-webpack');
+ });
+
+ function getTestStream() {
+ const writable = new Stream.PassThrough();
+ const readable = new ReadableStream({
+ start(controller) {
+ writable.on('data', chunk => {
+ controller.enqueue(chunk);
+ });
+ writable.on('end', () => {
+ controller.close();
+ });
+ },
+ });
+ return {
+ readable,
+ writable,
+ };
+ }
+
+ function moduleReference(moduleExport) {
+ const idx = webpackModuleIdx++;
+ webpackModules[idx] = {
+ d: moduleExport,
+ };
+ webpackMap['path/' + idx] = {
+ default: {
+ id: '' + idx,
+ chunks: [],
+ name: 'd',
+ },
+ };
+ const MODULE_TAG = Symbol.for('react.module.reference');
+ return {$$typeof: MODULE_TAG, filepath: 'path/' + idx, name: 'default'};
+ }
+
+ async function waitForSuspense(fn) {
+ while (true) {
+ try {
+ return fn();
+ } catch (promise) {
+ if (typeof promise.then === 'function') {
+ await promise;
+ } else {
+ throw promise;
+ }
+ }
+ }
+ }
+
+ it('should resolve HTML using Node streams', async () => {
+ function Text({children}) {
+ return {children} ;
+ }
+ function HTML() {
+ return (
+
+ hello
+ world
+
+ );
+ }
+
+ function App() {
+ const model = {
+ html: ,
+ };
+ return model;
+ }
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
+ ,
+ webpackMap,
+ );
+ pipe(writable);
+ const response = ReactServerDOMReader.createFromReadableStream(readable);
+ await waitForSuspense(() => {
+ const model = response.readRoot();
+ expect(model).toEqual({
+ html: (
+
+ hello
+ world
+
+ ),
+ });
+ });
+ });
+
+ it('should resolve the root', async () => {
+ const {Suspense} = React;
+
+ // Model
+ function Text({children}) {
+ return {children} ;
+ }
+ function HTML() {
+ return (
+
+ hello
+ world
+
+ );
+ }
+ function RootModel() {
+ return {
+ html: ,
+ };
+ }
+
+ // View
+ function Message({response}) {
+ return {response.readRoot().html} ;
+ }
+ function App({response}) {
+ return (
+ Loading...}>
+
+
+ );
+ }
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
+ ,
+ webpackMap,
+ );
+ pipe(writable);
+ const response = ReactServerDOMReader.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOM.createRoot(container);
+ await act(async () => {
+ root.render( );
+ });
+ expect(container.innerHTML).toBe(
+ '',
+ );
+ });
+
+ it('should not get confused by $', async () => {
+ const {Suspense} = React;
+
+ // Model
+ function RootModel() {
+ return {text: '$1'};
+ }
+
+ // View
+ function Message({response}) {
+ return {response.readRoot().text}
;
+ }
+ function App({response}) {
+ return (
+ Loading...}>
+
+
+ );
+ }
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
+ ,
+ webpackMap,
+ );
+ pipe(writable);
+ const response = ReactServerDOMReader.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOM.createRoot(container);
+ await act(async () => {
+ root.render( );
+ });
+ expect(container.innerHTML).toBe('$1
');
+ });
+
+ it('should not get confused by @', async () => {
+ const {Suspense} = React;
+
+ // Model
+ function RootModel() {
+ return {text: '@div'};
+ }
+
+ // View
+ function Message({response}) {
+ return {response.readRoot().text}
;
+ }
+ function App({response}) {
+ return (
+ Loading...}>
+
+
+ );
+ }
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
+ ,
+ webpackMap,
+ );
+ pipe(writable);
+ const response = ReactServerDOMReader.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOM.createRoot(container);
+ await act(async () => {
+ root.render( );
+ });
+ expect(container.innerHTML).toBe('@div
');
+ });
+
+ it('should progressively reveal server components', async () => {
+ let reportedErrors = [];
+ const {Suspense} = React;
+
+ // Client Components
+
+ class ErrorBoundary extends React.Component {
+ state = {hasError: false, error: null};
+ static getDerivedStateFromError(error) {
+ return {
+ hasError: true,
+ error,
+ };
+ }
+ render() {
+ if (this.state.hasError) {
+ return this.props.fallback(this.state.error);
+ }
+ return this.props.children;
+ }
+ }
+
+ function MyErrorBoundary({children}) {
+ return (
+ {e.message}
}>
+ {children}
+
+ );
+ }
+
+ // Model
+ function Text({children}) {
+ return children;
+ }
+
+ function makeDelayedText() {
+ let error, _resolve, _reject;
+ let promise = new Promise((resolve, reject) => {
+ _resolve = () => {
+ promise = null;
+ resolve();
+ };
+ _reject = e => {
+ error = e;
+ promise = null;
+ reject(e);
+ };
+ });
+ function DelayedText({children}, data) {
+ if (promise) {
+ throw promise;
+ }
+ if (error) {
+ throw error;
+ }
+ return {children} ;
+ }
+ return [DelayedText, _resolve, _reject];
+ }
+
+ const [Friends, resolveFriends] = makeDelayedText();
+ const [Name, resolveName] = makeDelayedText();
+ const [Posts, resolvePosts] = makeDelayedText();
+ const [Photos, resolvePhotos] = makeDelayedText();
+ const [Games, , rejectGames] = makeDelayedText();
+
+ // View
+ function ProfileDetails({avatar}) {
+ return (
+
+ :name:
+ {avatar}
+
+ );
+ }
+ function ProfileSidebar({friends}) {
+ return (
+
+ );
+ }
+ function ProfilePosts({posts}) {
+ return {posts}
;
+ }
+ function ProfileGames({games}) {
+ return {games}
;
+ }
+
+ const MyErrorBoundaryClient = moduleReference(MyErrorBoundary);
+
+ function ProfileContent() {
+ return (
+ <>
+ :avatar:} />
+ (loading sidebar)
}>
+ :friends:} />
+
+ (loading posts)}>
+ :posts:} />
+
+
+ (loading games)}>
+ :games:} />
+
+
+ >
+ );
+ }
+
+ const model = {
+ rootContent: ,
+ };
+
+ function ProfilePage({response}) {
+ return response.readRoot().rootContent;
+ }
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
+ model,
+ webpackMap,
+ {
+ onError(x) {
+ reportedErrors.push(x);
+ },
+ },
+ );
+ pipe(writable);
+ const response = ReactServerDOMReader.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOM.createRoot(container);
+ await act(async () => {
+ root.render(
+ (loading)}>
+
+ ,
+ );
+ });
+ expect(container.innerHTML).toBe('(loading)
');
+
+ // This isn't enough to show anything.
+ await act(async () => {
+ resolveFriends();
+ });
+ expect(container.innerHTML).toBe('(loading)
');
+
+ // We can now show the details. Sidebar and posts are still loading.
+ await act(async () => {
+ resolveName();
+ });
+ // Advance time enough to trigger a nested fallback.
+ jest.advanceTimersByTime(500);
+ expect(container.innerHTML).toBe(
+ ':name::avatar:
' +
+ '(loading sidebar)
' +
+ '(loading posts)
' +
+ '(loading games)
',
+ );
+
+ expect(reportedErrors).toEqual([]);
+
+ const theError = new Error('Game over');
+ // Let's *fail* loading games.
+ await act(async () => {
+ rejectGames(theError);
+ });
+ expect(container.innerHTML).toBe(
+ ':name::avatar:
' +
+ '(loading sidebar)
' +
+ '(loading posts)
' +
+ 'Game over
', // TODO: should not have message in prod.
+ );
+
+ expect(reportedErrors).toEqual([theError]);
+ reportedErrors = [];
+
+ // We can now show the sidebar.
+ await act(async () => {
+ resolvePhotos();
+ });
+ expect(container.innerHTML).toBe(
+ ':name::avatar:
' +
+ ':photos::friends:
' +
+ '(loading posts)
' +
+ 'Game over
', // TODO: should not have message in prod.
+ );
+
+ // Show everything.
+ await act(async () => {
+ resolvePosts();
+ });
+ expect(container.innerHTML).toBe(
+ ':name::avatar:
' +
+ ':photos::friends:
' +
+ ':posts:
' +
+ 'Game over
', // TODO: should not have message in prod.
+ );
+
+ expect(reportedErrors).toEqual([]);
+ });
+
+ it('should preserve state of client components on refetch', async () => {
+ const {Suspense} = React;
+
+ // Client
+
+ function Page({response}) {
+ return response.readRoot();
+ }
+
+ function Input() {
+ return ;
+ }
+
+ const InputClient = moduleReference(Input);
+
+ // Server
+
+ function App({color}) {
+ // Verify both DOM and Client children.
+ return (
+
+
+
+
+ );
+ }
+
+ const container = document.createElement('div');
+ const root = ReactDOM.createRoot(container);
+
+ const stream1 = getTestStream();
+ const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
+ ,
+ webpackMap,
+ );
+ pipe(stream1.writable);
+ const response1 = ReactServerDOMReader.createFromReadableStream(
+ stream1.readable,
+ );
+ await act(async () => {
+ root.render(
+ (loading)}>
+
+ ,
+ );
+ });
+ expect(container.children.length).toBe(1);
+ expect(container.children[0].tagName).toBe('DIV');
+ expect(container.children[0].style.color).toBe('red');
+
+ // Change the DOM state for both inputs.
+ const inputA = container.children[0].children[0];
+ expect(inputA.tagName).toBe('INPUT');
+ inputA.value = 'hello';
+ const inputB = container.children[0].children[1];
+ expect(inputB.tagName).toBe('INPUT');
+ inputB.value = 'goodbye';
+
+ const stream2 = getTestStream();
+ const {pipe: pipe2} = ReactServerDOMWriter.renderToPipeableStream(
+ ,
+ webpackMap,
+ );
+ pipe2(stream2.writable);
+ const response2 = ReactServerDOMReader.createFromReadableStream(
+ stream2.readable,
+ );
+ await act(async () => {
+ root.render(
+ (loading)}>
+
+ ,
+ );
+ });
+ expect(container.children.length).toBe(1);
+ expect(container.children[0].tagName).toBe('DIV');
+ expect(container.children[0].style.color).toBe('blue');
+
+ // Verify we didn't destroy the DOM for either input.
+ expect(inputA === container.children[0].children[0]).toBe(true);
+ expect(inputA.tagName).toBe('INPUT');
+ expect(inputA.value).toBe('hello');
+ expect(inputB === container.children[0].children[1]).toBe(true);
+ expect(inputB.tagName).toBe('INPUT');
+ expect(inputB.value).toBe('goodbye');
+ });
+});
diff --git a/packages/react-server-dom-vite/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-vite/src/__tests__/ReactFlightDOMBrowser-test.js
new file mode 100644
index 00000000000..8b82caac6bc
--- /dev/null
+++ b/packages/react-server-dom-vite/src/__tests__/ReactFlightDOMBrowser-test.js
@@ -0,0 +1,343 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+// Polyfills for test environment
+global.ReadableStream = require('web-streams-polyfill/ponyfill/es6').ReadableStream;
+global.TextEncoder = require('util').TextEncoder;
+global.TextDecoder = require('util').TextDecoder;
+
+let webpackModuleIdx = 0;
+let webpackModules = {};
+let webpackMap = {};
+global.__webpack_require__ = function(id) {
+ return webpackModules[id];
+};
+
+let act;
+let React;
+let ReactDOM;
+let ReactServerDOMWriter;
+let ReactServerDOMReader;
+
+describe('ReactFlightDOMBrowser', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ webpackModules = {};
+ webpackMap = {};
+ act = require('jest-react').act;
+ React = require('react');
+ ReactDOM = require('react-dom');
+ ReactServerDOMWriter = require('react-server-dom-webpack/writer.browser.server');
+ ReactServerDOMReader = require('react-server-dom-webpack');
+ });
+
+ function moduleReference(moduleExport) {
+ const idx = webpackModuleIdx++;
+ webpackModules[idx] = {
+ d: moduleExport,
+ };
+ webpackMap['path/' + idx] = {
+ default: {
+ id: '' + idx,
+ chunks: [],
+ name: 'd',
+ },
+ };
+ const MODULE_TAG = Symbol.for('react.module.reference');
+ return {$$typeof: MODULE_TAG, filepath: 'path/' + idx, name: 'default'};
+ }
+
+ async function waitForSuspense(fn) {
+ while (true) {
+ try {
+ return fn();
+ } catch (promise) {
+ if (typeof promise.then === 'function') {
+ await promise;
+ } else {
+ throw promise;
+ }
+ }
+ }
+ }
+
+ it('should resolve HTML using W3C streams', async () => {
+ function Text({children}) {
+ return {children} ;
+ }
+ function HTML() {
+ return (
+
+ hello
+ world
+
+ );
+ }
+
+ function App() {
+ const model = {
+ html: ,
+ };
+ return model;
+ }
+
+ const stream = ReactServerDOMWriter.renderToReadableStream( );
+ const response = ReactServerDOMReader.createFromReadableStream(stream);
+ await waitForSuspense(() => {
+ const model = response.readRoot();
+ expect(model).toEqual({
+ html: (
+
+ hello
+ world
+
+ ),
+ });
+ });
+ });
+
+ it('should resolve HTML using W3C streams', async () => {
+ function Text({children}) {
+ return {children} ;
+ }
+ function HTML() {
+ return (
+
+ hello
+ world
+
+ );
+ }
+
+ function App() {
+ const model = {
+ html: