I am building a React application based on micro-frondends using ModuleFederationPlugin ModuleFederationPlugin from webpack 5.
I have two separated projects (App1 and App2) which expose components and these are used in other project (AppShell).
App1 structure:
components/Button.tsx
App1.tsx
App2 structure:
components/Button.tsx
App2.tsx
Both projects contain component Button.tsx which means that in both project this component has same export. This component is used in App1.tsx (App2.tsx).
Now if I want to use App1 and App2 in AppShell, I always see the Button from App1:
App1 webpack config (App2 is similar):
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
entry: "./src/index.tsx",
output: {
filename: "test-app1.[contenthash].js",
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'],
},
module: {
rules: [
{
test: /\.(ts|js)x?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({template: './public/index.html'}),
new ModuleFederationPlugin({
name: "remoteApp",
library: { type: "var", name: "remoteApp" },
filename: "remoteEntry.js",
exposes: {
"./MyApp": "./src/MyApp",
},
shared: ["react", "react-dom", "@material-ui/core"],
})
]};
AppShell webpack config:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
entry: "./src/index.tsx",
output: {
filename: "app_shell.[contenthash].js",
path: path.resolve(__dirname, "dist"),
publicPath: ""
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'],
},
module: {
rules: [
{
test: /\.(ts|js)x?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /bootstrap\.js$/,
loader: "bundle-loader",
options: {
lazy: true,
},
},
],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({template: './public/index.html'}),
new ModuleFederationPlugin({
name: "app_shell",
remotes: {
"remoteApp-mfe": "remoteApp@http://localhost:8001/remoteEntry.js",
"remoteApp2-mfe": "remoteApp2@http://localhost:8002/remoteEntry.js",
},
shared: {react: {singleton: true}, "react-dom": {singleton: true}, "@material-ui/core": {singleton: true}},
}),
]};
Components usage in AppShell:
import React from "react";
export const AppShell: React.FC = () => {
// @ts-ignore
const App1 = React.lazy(() => import("remoteApp-mfe/MyApp"));
// @ts-ignore
const App2 = React.lazy(() => import("remoteApp2-mfe/MyApp2"));
return (
<>
<h1>App Shell</h1>
<React.Suspense fallback="Loading...">
<App1 />
</React.Suspense>
<React.Suspense fallback="Loading...">
<App2 />
</React.Suspense>
</>
);
};
You can download the test application here.
Do you have any idea how to fix this problem, please? (I know that I can refactor the apps but it is not the solution I am looking for ;)). Thank you.
The error lies in the package names of your individual applications. Each package.json in your app folders has the name "test"
:
app-shell/
public/
src/
package.json -> package name = "test"
...
test-app-01/
public/
src/
package.json -> package name = "test"
...
test-app-02/
public/
src/
package.json -> package name = "test"
...
When your applications getting stitched together on the client-side, all application code from all 3 packages get merged under the same scope "test"
. The problem occurs now because the apps have partly the same folder and file structure.
test-app-01/
public/
src/
components/
MyButton.tsx
// other code
...
test-app-02/
public/
src/
components/
MyButton.tsx
// other code
...
Due to the merging on the client-side under the same scope "test"
the button code gets overridden by either one of the two test-apps
like so:
test/
src/
components/
MyButton.tsx // MyButton.tsx from test-app-01 or test-app-02 gets overridden
// other code of app-shell, test-app-01 and test-app-02.
The solution would be to give all the applications individual package names in the package.json's.