Prototype that uses Webpack Module Federation and iframes. This prototype should have totally independent plugins but while still able to share some common resources. The problem is that i am getting an error: "Shared module is not available for eager consumption". The main gets the plugins from node server that keeps track which are registered and ready for consumption.
I tried a lot of webpack configuration changes (from sharing common libraries (like react) to eager loading and creating bootstrap.js as main entry point for plugins) and different code changes. Main plugin looks like this, it does not look very pretty, but I am currently only trying to make it work and it is a prototype (I have omitted some of the irrelevant methods):
import React from 'react';
import ReactDOM from 'react-dom';
async function fetchPlugins() {
try {
const response = await fetch("http://localhost:5000/plugins");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("An error occurred while fetching plugins:", error);
}
}
async function loadDynamicPlugin(plugin) {
const { url, name } = plugin;
await __webpack_init_sharing__(name);
return new Promise((resolve, reject) => {
const iframe = document.createElement('iframe');
document.getElementById('pluginArea').innerHTML = '';
document.getElementById('pluginArea').appendChild(iframe);
iframe.onload = async () => {
const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
const script = iframeDocument.createElement('script');
script.src = url;
iframeDocument.head.appendChild(script);
script.onload = async () => {
const container = iframe.contentWindow[name];
if (container) {
await container.init(__webpack_share_scopes__.default);
if (name === 'auth') {
const LoginFormModule = await container.get('./bootstrap').then(factory => factory());
renderReactComponent(LoginFormModule, iframeDocument.body);
} else {
const factory = await container.get('./bootstrap');
const Module = factory();
Module.bootstrap(iframeDocument.body);
}
resolve();
} else {
reject(`Could not load plugin: ${name}`);
}
};
};
iframe.src = 'about:blank';
});
}
async function init() {
const navbar = createNavbar();
document.body.insertBefore(navbar, document.body.firstChild);
document.body.appendChild(createPluginArea());
await loadAuthPlugin();
window.addEventListener('message', async (event) => {
if (event.data.type === 'SET_TOKEN' && event.data.token) {
sessionStorage.setItem('jwtToken', event.data.token);
const plugins = await fetchPlugins();
if (plugins) {
updateNavbar(plugins);
}
}
});
}
function createPluginArea() {
const pluginArea = document.createElement('div');
pluginArea.id = 'pluginArea';
return pluginArea;
}
async function loadAuthPlugin() {
const authPlugin = { url: 'http://localhost:3030/remoteEntry.js', name: 'auth' };
await loadDynamicPlugin(authPlugin);
}
function renderReactComponent(Module, container) {
const Component = Module.default;
ReactDOM.render(React.createElement(Component), container);
}
document.addEventListener('DOMContentLoaded', init);
The webpack configuration of main:
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
const packageJson = require('./package.json');
module.exports = {
entry: './main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader'
],
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new MiniCssExtractPlugin({
filename: 'testing.css',
}),
new ModuleFederationPlugin({
name: "host",
library: { type: "var", name: "host" },
filename: "remoteEntry.js",
shared: {
react: {
singleton: true,
requiredVersion: packageJson.dependencies.react,
},
'react-dom': {
singleton: true,
requiredVersion: packageJson.dependencies['react-dom'],
},
},
}),
],
};
The auth plugin's bootstrap.js:
import React from 'react';
import ReactDOM from 'react-dom';
import LoginForm from './LoginForm';
ReactDOM.render(<LoginForm />, document.getElementById('root'));
and the auth plugin's webpack.config.js:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const packageJson = require('./package.json');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new ModuleFederationPlugin({
name: 'auth',
library: { type: 'var', name: 'auth' },
filename: 'remoteEntry.js',
exposes: {
'./bootstrap': './src/bootstrap',
},
shared: {
react: {
singleton: true,
requiredVersion: packageJson.dependencies.react,
eager:true,
},
'react-dom': {
singleton: true,
requiredVersion: packageJson.dependencies['react-dom'],
eager:true,
},
},
}),
],
resolve: {
extensions: ['.js', '.jsx'],
},
devServer: {
static: path.join(__dirname, 'dist'),
compress: true,
port: 3030,
},
};
The index.js of the auth plugin simply has the imported bootstrap.js The react is the same version in both the auth and main package.json files.
This is the exact error I am getting:
bundle.js:1 Uncaught Error: Shared module is not available for eager consumption: 950
at o.m.<computed> (bundle.js:1:7513)
at o (bundle.js:1:3104)
at 220 (bundle.js:1:51)
at o (bundle.js:1:3104)
at bundle.js:1:8609
at bundle.js:1:8617
If anyone can help in any way it would be greatly appreciated, I am currently stumped.