Intro
I have been using Module Federation Plugin for almost two years and have successfully used them for implementing micro-frontend architectures for many organisations. I decided to start sharing lifehacks related to module federation, which may save some time for other engineers trying to integrate Module Federation into their projects.
If you are not familiar with Module Federation Plugin, you can read my article at Beamery Hacking Talent Blog Module Federation for distributed front ends the best of both worlds?
Problem statement
If you use monorepo for your micro-frontends, coupled with tools such as Yarn Workspaces, NX or Turborepo, you probably use package scope for your monorepo. Scopes are a way of grouping related packages together. For example: @somescope/somepackagename
Let's say you have monorepo with two micro-frontend apps. The name of each micro app in package.json
will be:
-
@my-micro-fe/app1
- host. -
@my-micro-fe/app2
- remote container
And in the host @my-micro-fe/app1
, you want to import the module with the Button component from remote container @my-micro-fe/app2
, example:
const RemoteButton = React.lazy(() => import('@my-micro-fe/app2/Button'));
Sounds good! However, to make it work, we need to configure Module Federation Plugin correctly.
Flowing documentation, we should specify such a Module Federation config for @my-micro-fe/app2
:
new ModuleFederationPlugin({
name: '@my-micro-fe/app2',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button',
},
shared: ['react', 'react-dom'],
}),
If you then run webpack build command, you will receive an error:
Error: Library name base (@my-micro-fe/app2) must be a valid identifier when using a var
declaring library type. Either use a valid identifier (e. g. _my_micro_fe_app2) or use a
different library type (e. g. 'type: "global"', which assign a property on the global scope
instead of declaring a variable). Common configuration options that specific library names
are 'output.library[.name]', 'entry.xyz.library[.name]', 'ModuleFederationPlugin.name' and 'ModuleFederationPlugin.library[.name]'.
Solution
The problem is Module Federation plugin under the hood uses variable declaration for remote container scope. To fix this issue, you need to sanitize your app name to remove /,-,@
special characters and add a few more lines in the configuration with library.name
and library.type
properties:
new ModuleFederationPlugin({
name: '@my-micro-fe/app2',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button',
},
library: {
type: 'global',
name: '_micro_fe_app2'
},
shared: ['react', 'react-dom'],
}),
The same fix you need to do in the host app:
new ModuleFederationPlugin({
name: '@my-micro-fe/app1',
library: {
type: 'global',
name: '_micro_fe_app1'
},
remotes: {
"@my-micro-fe/app2": `promise new Promise(resolve => {
const remoteUrlWithVersion = 'http://localhost:3002/remoteEntry.js'
const script = document.createElement('script')
script.src = remoteUrlWithVersion
script.onload = () => {
// the injected script has loaded and is available on window
// we can now resolve this Promise
const proxy = {
get: (request) => window._micro_fe_app2.get(request),
init: (arg) => {
try {
return window._micro_fe_app2.init(arg)
} catch(e) {
console.log('remote container already initialized')
}
}
}
resolve(proxy)
}
// inject this script with the src set to the versioned remoteEntry.js
document.head.appendChild(script);
})
`
},
shared: ['react', 'react-dom'],
}),
As you can see, for our @my-micro-fe/app2
host app, I have also sanitized the name. Moreover, I am using pormise new Promise
syntax to load remoteEntry.js
file from the remote container @my-micro-fe/app1
. That is why I also use the sensitized name to save the result of a request to the window
object.
In addition
This was just a simple example and you can instead create a base webpack
config where you can sanitize the app name and generate correct remotes config using pormise new Promise
to reduce code duplication.
I hope this tip was helpful for you!
Cheers! 🍺
Top comments (0)