/jest.setup.js']
+ };
diff --git a/unit-test/host/jest.setup.js b/unit-test/host/jest.setup.js
new file mode 100644
index 00000000000..dfbfb73bc06
--- /dev/null
+++ b/unit-test/host/jest.setup.js
@@ -0,0 +1,5 @@
+const { setupFederationTest } = require('../mf-test');
+
+module.exports = async () => {
+ await setupFederationTest(require('./modulefederation.config'));
+};
diff --git a/unit-test/host/modulefederation.config.js b/unit-test/host/modulefederation.config.js
new file mode 100644
index 00000000000..ffe4a2b8fd0
--- /dev/null
+++ b/unit-test/host/modulefederation.config.js
@@ -0,0 +1,21 @@
+const { dependencies } = require('./package.json');
+
+module.exports = {
+ name: 'host',
+ library: {type: 'commonjs-module', name: 'host'},
+ remoteType: 'script',
+ remotes: {
+ remote: 'remote@http://localhost:3002/remoteEntry.js',
+ },
+ shared: {
+ ...dependencies,
+ react: {
+ singleton: true,
+ requiredVersion: dependencies['react'],
+ },
+ 'react-dom': {
+ singleton: true,
+ requiredVersion: dependencies['react-dom'],
+ },
+ },
+};
diff --git a/unit-test/host/package.json b/unit-test/host/package.json
new file mode 100644
index 00000000000..a073f3353e2
--- /dev/null
+++ b/unit-test/host/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "unit_test_host",
+ "version": "0.0.0",
+ "dependencies": {
+ "deasync": "^0.1.29",
+ "fibers": "^5.0.3",
+ "future": "^2.3.1",
+ "react": "17.0.2",
+ "react-dom": "17.0.2",
+ "sync-promise": "^1.1.0"
+ },
+ "scripts": {
+ "start": "rsbuild dev",
+ "build": "rsbuild build",
+ "preview": "rsbuild preview",
+ "test": "jest"
+ },
+ "eslintConfig": {},
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "devDependencies": {
+ "@babel/preset-env": "^7.24.5",
+ "@babel/preset-react": "^7.24.1",
+ "@module-federation/enhanced": "^0.1.13",
+ "@module-federation/runtime": "^0.1.13",
+ "@rsbuild/core": "0.6.15",
+ "@rsbuild/plugin-react": "0.6.15",
+ "@rspack/core": "0.6.5",
+ "babel-jest": "^29.7.0",
+ "jest": "^29.7.0",
+ "jest-environment-jsdom": "^29.7.0",
+ "node-fetch": "2.6.9"
+ }
+}
diff --git a/unit-test/host/public/favicon.ico b/unit-test/host/public/favicon.ico
new file mode 100644
index 00000000000..a11777cc471
Binary files /dev/null and b/unit-test/host/public/favicon.ico differ
diff --git a/unit-test/host/public/index.html b/unit-test/host/public/index.html
new file mode 100644
index 00000000000..17336bb272b
--- /dev/null
+++ b/unit-test/host/public/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Host
+
+
+
+
+
+
diff --git a/unit-test/host/rsbuild.config.ts b/unit-test/host/rsbuild.config.ts
new file mode 100644
index 00000000000..0e858dade9e
--- /dev/null
+++ b/unit-test/host/rsbuild.config.ts
@@ -0,0 +1,30 @@
+import { defineConfig } from '@rsbuild/core';
+import { pluginReact } from '@rsbuild/plugin-react';
+import {ModuleFederationPlugin} from '@module-federation/enhanced/rspack'
+//@ts-ignore
+import mfConfig from './modulefederation.config';
+import rspack from '@rspack/core';
+
+const rsbuildPlugin = () => ({
+ name: 'example',
+ setup(api) {
+ api.onAfterBuild(() => console.log('done'));
+ },
+});
+
+export default defineConfig({
+ server: {
+ port: 3000,
+ },
+ output: {
+ targets: ['node'],
+ },
+ plugins: [pluginReact()],
+ tools: {
+ rspack: (config, { appendPlugins }) => {
+ appendPlugins([
+ new ModuleFederationPlugin(mfConfig),
+ ]);
+ },
+ },
+});
diff --git a/unit-test/host/scripts/build.js b/unit-test/host/scripts/build.js
new file mode 100644
index 00000000000..60a17bd95d2
--- /dev/null
+++ b/unit-test/host/scripts/build.js
@@ -0,0 +1,3 @@
+process.env.NODE_ENV = 'production';
+require('./overrides/webpack-config');
+require('react-scripts/scripts/build');
diff --git a/unit-test/host/scripts/overrides/webpack-config.js b/unit-test/host/scripts/overrides/webpack-config.js
new file mode 100644
index 00000000000..c7f55b49aca
--- /dev/null
+++ b/unit-test/host/scripts/overrides/webpack-config.js
@@ -0,0 +1,16 @@
+const { ModuleFederationPlugin } = require('webpack').container;
+
+const webpackConfigPath = 'react-scripts/config/webpack.config';
+const webpackConfig = require(webpackConfigPath);
+
+const override = config => {
+ config.plugins.push(new ModuleFederationPlugin(require('../../modulefederation.config.js')));
+
+ config.output.publicPath = 'auto';
+
+ return config;
+};
+
+require.cache[require.resolve(webpackConfigPath)].exports = env => override(webpackConfig(env));
+
+module.exports = require(webpackConfigPath);
diff --git a/unit-test/host/scripts/start.js b/unit-test/host/scripts/start.js
new file mode 100644
index 00000000000..17742ef30a2
--- /dev/null
+++ b/unit-test/host/scripts/start.js
@@ -0,0 +1,3 @@
+process.env.NODE_ENV = process.env.NODE_ENV || 'development';
+require('./overrides/webpack-config');
+require('react-scripts/scripts/start');
diff --git a/unit-test/host/src/App.js b/unit-test/host/src/App.js
new file mode 100644
index 00000000000..210d89f59a6
--- /dev/null
+++ b/unit-test/host/src/App.js
@@ -0,0 +1,15 @@
+import React from 'react';
+import RemoteButton from 'remote/Button'
+// const RemoteButton = React.lazy(() => import('remote/Button'));
+
+const App = () => (
+
+
Basic Host-Remote
+ Host
+ Loading Button}>
+
+
+
+);
+
+export default App;
diff --git a/unit-test/host/src/bootstrap.js b/unit-test/host/src/bootstrap.js
new file mode 100644
index 00000000000..f36b3686266
--- /dev/null
+++ b/unit-test/host/src/bootstrap.js
@@ -0,0 +1,11 @@
+// import React from 'react';
+// import ReactDOM from 'react-dom';
+//
+// import App from './App';
+//
+// ReactDOM.render(
+//
+//
+// ,
+// // document.getElementById('root'),
+// );
diff --git a/unit-test/host/src/index.js b/unit-test/host/src/index.js
new file mode 100644
index 00000000000..b93c7a0268a
--- /dev/null
+++ b/unit-test/host/src/index.js
@@ -0,0 +1 @@
+import('./bootstrap');
diff --git a/unit-test/host/thing.js b/unit-test/host/thing.js
new file mode 100644
index 00000000000..5a21fb825d8
--- /dev/null
+++ b/unit-test/host/thing.js
@@ -0,0 +1 @@
+require('./jest.setup')()
diff --git a/unit-test/mf-test/index.js b/unit-test/mf-test/index.js
new file mode 100644
index 00000000000..4b009371c4b
--- /dev/null
+++ b/unit-test/mf-test/index.js
@@ -0,0 +1,142 @@
+const fs = require('fs');
+const path = require('path');
+const fetch = require('node-fetch');
+const { init, loadRemote } = require('@module-federation/runtime');
+
+const generateSharedConfig = (mfConfig) => {
+ const sharedConfig = {};
+ for (const [packageName, packageConfig] of Object.entries(mfConfig.shared)) {
+ let version = false;
+ try {
+ version = require(path.join(packageName, 'package.json')).version;
+ } catch (e) {
+ // Handle error if needed
+ }
+ if (typeof packageConfig === 'string') {
+ sharedConfig[packageName] = {
+ version,
+ lib: () => require(packageName),
+ };
+ } else {
+ sharedConfig[packageName] = {
+ version,
+ ...packageConfig,
+ lib: () => require(packageName),
+ };
+ }
+ }
+ return sharedConfig;
+};
+
+const setupFederationTest = async (mfConfig) => {
+ const sharedConfig = generateSharedConfig(mfConfig);
+ let remotes = [];
+
+ const harnessPath = path.resolve(__dirname, 'node_modules', 'federation-test');
+ let harnessData = [];
+
+ for (const [remote, entry] of Object.entries(mfConfig.remotes)) {
+ const [name, url] = entry.split('@');
+ const manifest = url.replace('remoteEntry.js', 'mf-manifest.json');
+ const response = await fetch(manifest);
+ const data = await response.json();
+
+ const parsedPath = new URL(url).origin;
+ const subPath = data.metaData.remoteEntry.path;
+
+ const buildUrl = (parsedPath, subPath, file) => {
+ return subPath ? `${parsedPath}/${subPath}/${file}` : `${parsedPath}/${file}`;
+ };
+
+ remotes.push(buildUrl(parsedPath, subPath, data.metaData.remoteEntry.name));
+
+ const jsFiles = [
+ ...data.shared.flatMap(shared => [...shared.assets.js.sync, ...shared.assets.js.async].map(file => buildUrl(parsedPath, subPath, file))),
+ ...data.exposes.flatMap(expose => [...expose.assets.js.sync, ...expose.assets.js.async].map(file => buildUrl(parsedPath, subPath, file)))
+ ];
+
+ const cssFiles = [
+ ...data.shared.flatMap(shared => [...shared.assets.css.sync, ...shared.assets.css.async].map(file => buildUrl(parsedPath, subPath, file))),
+ ...data.exposes.flatMap(expose => [...expose.assets.css.sync, ...expose.assets.css.async].map(file => buildUrl(parsedPath, subPath, file)))
+ ];
+
+ remotes.push(...jsFiles, ...cssFiles);
+
+ const fakePackagePath = path.resolve(__dirname, 'node_modules', data.id);
+ const fakePackageJsonPath = path.join(fakePackagePath, 'package.json');
+ const fakePackageIndexPath = path.join(fakePackagePath, 'index.js');
+
+ if (!fs.existsSync(fakePackagePath)) {
+ fs.mkdirSync(fakePackagePath, { recursive: true });
+ }
+
+ const exportsContent = data.exposes.reduce((exportsObj, expose) => {
+ let exposeName = expose.name;
+ if (!exposeName.endsWith('.js')) {
+ exposeName += '.js';
+ }
+ exportsObj[expose.path] = './virtual' + exposeName;
+ const resolvePath = path.join(fakePackagePath, './virtual' + exposeName);
+
+ harnessData.push(resolvePath);
+
+ fs.writeFileSync(resolvePath, `
+ const container = require('./remoteEntry.js')[${JSON.stringify(data.id)}];
+ const target = {};
+
+ let e;
+ const cx = container.get(${JSON.stringify(expose.path)}).then((m) => {
+ e = m();
+ Object.assign(target, e);
+ });
+
+
+ module.exports = new Proxy(target, {
+ get(target, prop) {
+ if(prop === 'setupTest') return cx;
+ if (!e) {
+ return cx;
+ } else if (prop in e) {
+ return e[prop];
+ } else {
+ return e;
+ }
+ }
+ });
+ `, 'utf-8');
+ return exportsObj;
+ }, {});
+
+ const packageJsonContent = {
+ name: data.id,
+ version: '1.0.0',
+ exports: exportsContent
+ };
+ const indexJsContent = `
+ module.exports = () => 'Hello from fake package!';
+ `;
+
+ fs.writeFileSync(fakePackageJsonPath, JSON.stringify(packageJsonContent, null, 2));
+ fs.writeFileSync(fakePackageIndexPath, indexJsContent);
+
+ for (const fileUrl of remotes) {
+ const fileName = path.basename(fileUrl);
+ const filePath = path.join(fakePackagePath, fileName);
+ const fileResponse = await fetch(fileUrl);
+ const fileData = await fileResponse.buffer();
+ fs.writeFileSync(filePath, fileData);
+ }
+ }
+
+ if (!fs.existsSync(harnessPath)) {
+ fs.mkdirSync(harnessPath, { recursive: true });
+ }
+
+ fs.writeFileSync('node_modules/federation-test/index.js', `module.exports = Promise.all(${JSON.stringify(harnessData)}.map((p) => require(p).setupTest))`, 'utf-8');
+ fs.writeFileSync('node_modules/federation-test/package.json', '{"name": "federation-test", "main": "./index.js"}', 'utf-8');
+};
+
+module.exports = {
+ generateSharedConfig,
+ setupFederationTest
+};
diff --git a/unit-test/package.json b/unit-test/package.json
new file mode 100644
index 00000000000..66565dbf077
--- /dev/null
+++ b/unit-test/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "unit_test",
+ "version": "0.0.1",
+ "description": "Create React app Running with rsbuild",
+ "workspaces": [
+ "./host",
+ "./remote"
+ ],
+ "scripts": {
+ "start": "pnpm --filter unit_test_* start",
+ "build": "pnpm --filter unit_test_* build",
+ "serve": "serve ./remote/dist/server -p 3001",
+ "preview": "pnpm run build && pnpm --filter unit_test_* preview",
+ "e2e:ci": "pnpm start & npx cypress run --config-file ../cypress-e2e/config/cypress.config.ts --config '{\"supportFile\": \"../cypress-e2e/support/e2e.ts\"}' --spec \"./e2e/*.cy.ts\" --browser=chrome && lsof -ti tcp:3000,3001,3002 | xargs kill"
+ },
+ "devDependencies": {
+ "wait-on": "7.2.0",
+ "concurrently": "7.6.0",
+ "forever": "4.0.3",
+ "serve": "^14.2.3"
+ }
+}
diff --git a/unit-test/remote/.gitignore b/unit-test/remote/.gitignore
new file mode 100644
index 00000000000..4d29575de80
--- /dev/null
+++ b/unit-test/remote/.gitignore
@@ -0,0 +1,23 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/unit-test/remote/modulefederation.config.js b/unit-test/remote/modulefederation.config.js
new file mode 100644
index 00000000000..5962a6ab9ff
--- /dev/null
+++ b/unit-test/remote/modulefederation.config.js
@@ -0,0 +1,21 @@
+const { dependencies } = require('./package.json');
+
+module.exports = {
+ name: 'remote',
+ library: {type: 'commonjs-module', name: 'remote'},
+ exposes: {
+ './Button': './src/Button',
+ },
+ filename: 'remoteEntry.js',
+ shared: {
+ ...dependencies,
+ react: {
+ singleton: true,
+ requiredVersion: dependencies['react'],
+ },
+ 'react-dom': {
+ singleton: true,
+ requiredVersion: dependencies['react-dom'],
+ },
+ },
+};
diff --git a/unit-test/remote/package.json b/unit-test/remote/package.json
new file mode 100644
index 00000000000..af7a11843e9
--- /dev/null
+++ b/unit-test/remote/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "unit_test_remote",
+ "version": "0.0.0",
+ "dependencies": {
+ "react": "17.0.2",
+ "react-dom": "17.0.2"
+ },
+ "scripts": {
+ "start": "rsbuild dev",
+ "build": "rsbuild build",
+ "preview": "rsbuild preview"
+ },
+ "eslintConfig": {},
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "devDependencies": {
+ "@module-federation/enhanced": "^0.1.13",
+ "@rsbuild/core": "0.6.15",
+ "@rsbuild/plugin-react": "0.6.15",
+ "@rspack/core": "0.6.5"
+ }
+}
diff --git a/unit-test/remote/public/favicon.ico b/unit-test/remote/public/favicon.ico
new file mode 100644
index 00000000000..a11777cc471
Binary files /dev/null and b/unit-test/remote/public/favicon.ico differ
diff --git a/unit-test/remote/public/index.html b/unit-test/remote/public/index.html
new file mode 100644
index 00000000000..b73f992d90d
--- /dev/null
+++ b/unit-test/remote/public/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Remote
+
+
+
+
+
+
diff --git a/unit-test/remote/rsbuild.config.ts b/unit-test/remote/rsbuild.config.ts
new file mode 100644
index 00000000000..af682121cf8
--- /dev/null
+++ b/unit-test/remote/rsbuild.config.ts
@@ -0,0 +1,30 @@
+import { defineConfig } from '@rsbuild/core';
+import { pluginReact } from '@rsbuild/plugin-react';
+import {ModuleFederationPlugin} from '@module-federation/enhanced/rspack'
+//@ts-ignore
+import mfConfig from './modulefederation.config';
+import rspack from '@rspack/core';
+
+const rsbuildPlugin = () => ({
+ name: 'example',
+ setup(api) {
+ api.onAfterBuild(() => console.log('done'));
+ },
+});
+
+export default defineConfig({
+ server: {
+ port: 3002,
+ },
+ output: {
+ targets: ['node'],
+ },
+ plugins: [pluginReact()],
+ tools: {
+ rspack: (config, { appendPlugins }) => {
+ appendPlugins([
+ new ModuleFederationPlugin(mfConfig),
+ ]);
+ },
+ },
+});
diff --git a/unit-test/remote/scripts/build.js b/unit-test/remote/scripts/build.js
new file mode 100644
index 00000000000..60a17bd95d2
--- /dev/null
+++ b/unit-test/remote/scripts/build.js
@@ -0,0 +1,3 @@
+process.env.NODE_ENV = 'production';
+require('./overrides/webpack-config');
+require('react-scripts/scripts/build');
diff --git a/unit-test/remote/scripts/overrides/webpack-config.js b/unit-test/remote/scripts/overrides/webpack-config.js
new file mode 100644
index 00000000000..c7f55b49aca
--- /dev/null
+++ b/unit-test/remote/scripts/overrides/webpack-config.js
@@ -0,0 +1,16 @@
+const { ModuleFederationPlugin } = require('webpack').container;
+
+const webpackConfigPath = 'react-scripts/config/webpack.config';
+const webpackConfig = require(webpackConfigPath);
+
+const override = config => {
+ config.plugins.push(new ModuleFederationPlugin(require('../../modulefederation.config.js')));
+
+ config.output.publicPath = 'auto';
+
+ return config;
+};
+
+require.cache[require.resolve(webpackConfigPath)].exports = env => override(webpackConfig(env));
+
+module.exports = require(webpackConfigPath);
diff --git a/unit-test/remote/scripts/start.js b/unit-test/remote/scripts/start.js
new file mode 100644
index 00000000000..17742ef30a2
--- /dev/null
+++ b/unit-test/remote/scripts/start.js
@@ -0,0 +1,3 @@
+process.env.NODE_ENV = process.env.NODE_ENV || 'development';
+require('./overrides/webpack-config');
+require('react-scripts/scripts/start');
diff --git a/unit-test/remote/src/App.js b/unit-test/remote/src/App.js
new file mode 100644
index 00000000000..c2689dc3baa
--- /dev/null
+++ b/unit-test/remote/src/App.js
@@ -0,0 +1,11 @@
+import LocalButton from './Button';
+
+const App = () => (
+
+
Basic Host-Remote
+ Remote
+
+
+);
+
+export default App;
diff --git a/unit-test/remote/src/Button.js b/unit-test/remote/src/Button.js
new file mode 100644
index 00000000000..fd10bdecb58
--- /dev/null
+++ b/unit-test/remote/src/Button.js
@@ -0,0 +1,2 @@
+const Button = () => ;
+export default Button;
diff --git a/unit-test/remote/src/bootstrap.js b/unit-test/remote/src/bootstrap.js
new file mode 100644
index 00000000000..c9ebadbf857
--- /dev/null
+++ b/unit-test/remote/src/bootstrap.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import App from './App';
+
+ReactDOM.render(
+
+
+ ,
+ document.getElementById('root'),
+);
diff --git a/unit-test/remote/src/index.js b/unit-test/remote/src/index.js
new file mode 100644
index 00000000000..b93c7a0268a
--- /dev/null
+++ b/unit-test/remote/src/index.js
@@ -0,0 +1 @@
+import('./bootstrap');