Module Federation Lifehack  - Custom Container Name

Photo by Kevin Ku on Unsplash

Module Federation Lifehack - Custom Container Name

Exploring how we can specify package name with scope as a container name for webpack module federation config

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! ๐Ÿบ

Did you find this article valuable?

Support Oleksandr (Sasha) Khivrych by becoming a sponsor. Any amount is appreciated!

ย