ModuleFederationPlugin

The ModuleFederationPlugin allows a build to provide or consume modules with other independent builds at runtime.

import webpack from "webpack";

const { ModuleFederationPlugin } = webpack.container;

export default {
  plugins: [
    new ModuleFederationPlugin({
      // options' typings in typescript
      runtime: string | false,
    }),
  ],
};

Options

name

string

The name of the container this build produces. For the default script remote type it is also the global variable the container is assigned to, which is how a consumer reaches it once remoteEntry.js has loaded.

It must be unique among the builds on a page. It is a separate thing from output.uniqueName, which also has to be unique — see Every build needs its own unique name.

filename

string

The filename of the container entry, as a path relative to output.path. Conventionally remoteEntry.js.

This names the file this build emits. It has nothing to do with the filenames of the remotes it consumes — those are whatever each remote chose, and appear in the URLs under remotes.

remotes

string[] object

The containers this build consumes. Each entry maps a request scope — the name you import through — to a container location:

new ModuleFederationPlugin({
  remotes: {
    // request scope        container location
    app1: "app1@http://localhost:3001/remoteEntry.js",
  },
});

which is then imported as:

import Button from "app1/Button";

The two halves of the location string are split at the first @:

PartMeaning
before @app1the remote's name, i.e. the global its container is assigned to
after @http://localhost:3001/remoteEntry.jsthe URL its filename is served from

So a name appearing "twice" in a config is usually two different things that happen to match: the key is what you import through and can be renamed freely, while the part before @ must match what the remote called itself. They are free to differ:

new ModuleFederationPlugin({
  remotes: {
    // imported as "checkout/…", served by a container that calls itself "shop"
    checkout: "shop@http://localhost:3001/remoteEntry.js",
  },
});

Passing an array instead of an object drops the keys and lets webpack infer each request scope from the location.

For a statically configured remote you do not load anything yourself: webpack emits code that injects the <script>, waits for the global to appear, and reports ScriptExternalLoadError if it does not. Loading the container by hand is only needed for dynamic remotes, where the location is not known at build time.

An entry can also be an object with external (the location) and shareScope, when one remote needs a different share scope from the rest.

remoteType

string

How a remote is fetched, as an externalsType.

When unset it falls back to this plugin's own library.type, but only if that is a valid externals type, and otherwise to 'script' — the name@url form described above. Since library itself defaults to { type: 'var', name }, a config that sets neither gets 'script'.

runtime

Create a new runtime chunk with the specified name.

webpack.config.js

import webpack from "webpack";

const { ModuleFederationPlugin } = webpack.container;

export default {
  plugins: [
    new ModuleFederationPlugin({
      runtime: "my-runtime-name",
    }),
  ],
};

exposes

The modules this container makes available to other builds. The property name is the public name a consumer imports (import Button from "remote/Button"), the value the module to expose:

webpack.config.js

new ModuleFederationPlugin({
  name: "remote",
  exposes: {
    "./Button": "./src/components/Button",
  },
});

An entry can also be an object:

  • import (string): the module to expose.
  • name (string): the name of the chunk webpack generates for it. Without it the chunk is named after the module's internal id, which changes as the build changes; setting it gives the file a stable name that can be referenced statically.
new ModuleFederationPlugin({
  name: "remote",
  exposes: {
    "./Button": {
      import: "./src/components/Button",
      name: "button-chunk",
    },
  },
});

Sharing libraries

With the shared key in the configuration you can define libraries that are shared between your federated modules. The package name is the same as the one found in the dependencies section of your package.json. However, by default webpack will only share the root level of a library.

import webpack from "webpack";

const { ModuleFederationPlugin } = webpack.container;

export default {
  plugins: [
    new ModuleFederationPlugin({
      // adds date-fns as shared module
      shared: ["date-fns"],
    }),
  ],
};

So in your application you could do something like

import { format } from "date-fns";

format(new Date(2014, 1, 11), "MM/dd/yyyy");

and webpack will automatically share date-fns between all your federated modules that define date-fns as a shared library. However, if you want to access something that is not located at the root level of the package, for example date-fns/locale/en-GB/index.js, you need to append / to the package name in your shared configuration:

import webpack from "webpack";

const { ModuleFederationPlugin } = webpack.container;

export default {
  plugins: [
    new ModuleFederationPlugin({
      // adds date-fns as shared module
      // all files of the package will be shared
      shared: ["date-fns/"],
    }),
  ],
};

The / syntax allows you to access all files of a package. However, it should be used only where necessary, because it has an impact on performance especially in development mode.

Specify package versions

There are three ways to specify the versions of shared libraries.

Array syntax

This syntax allows you to share libraries with package name only. This approach is good for prototyping, but it will not allow you to scale to large production environment given that libraries like react and react-dom will require additional requirements.

import webpack from "webpack";

const { ModuleFederationPlugin } = webpack.container;

export default {
  plugins: [
    new ModuleFederationPlugin({
      // adds lodash as shared module
      // version is inferred from package.json
      // requiredVersion is inferred from package.json as well
      // (a warning is emitted if it can't be determined)
      // a shared version that doesn't satisfy it falls back to the local copy
      shared: ["lodash"],
    }),
  ],
};
Object syntax

This syntax provides you more control over each shared library in which you can define package name as the key and version (semver) as the value.

export default {
  plugins: [
    new ModuleFederationPlugin({
      shared: {
        // adds lodash as shared module
        // version is inferred from package.json
        // it will use the highest lodash version that is >= 4.17 and < 5
        lodash: "^4.17.0",
      },
    }),
  ],
};
Object syntax with sharing hints

This syntax allows you to provide additional hints to each shared package where you define the package name as the key, and the value as an object containing hints to modify sharing behavior.

import { readFileSync } from "node:fs";

const deps = JSON.parse(
  readFileSync(new URL("./package.json", import.meta.url)),
).dependencies;

export default {
  plugins: [
    new ModuleFederationPlugin({
      shared: {
        // adds react as shared module
        react: {
          requiredVersion: deps.react,
          singleton: true,
        },
      },
    }),
  ],
};

Sharing hints

eager

boolean

This hint will allow webpack to include the provided and fallback module directly instead of fetching the library via an asynchronous request. In other words, this allows to use this shared module in the initial chunk. Also, be careful that all provided and fallback modules will always be downloaded when this hint is enabled.

import

false | string

The provided module that should be placed in the shared scope. This provided module also acts as fallback module if no shared module is found in the shared scope or version isn't valid. (The value for this hint defaults to the property name.)

packageName

string

The package name that is used to determine required version from description file. This is only needed when the package name can't be automatically determined from request.

requiredVersion

false string

This field specifies the required version of the package. It accepts semantic versioning, such as "^1.2.3". Additionally, it retrieves the version if it's provided as a URL, for instance: "git+ssh://git@github.com:foo/bar.git#v1.0.0".

webpack implements the full range grammar, so a required version can be an exact version (1.2.3), a partial one (1.2, which matches any patch), a comparator (>=1.2.3 <2), a caret or tilde range (^1.2.3, ~1.2.3), a hyphen range (1.2.3 - 2.3.4) or several of those joined with ||. Set it to false to accept whatever the scope offers.

A share scope can hold several versions of the same module at once — each build provides the version it has, and a consumer gets the highest one that satisfies its own requiredVersion. That is what lets two builds that need different major versions of a library work on one page, at the cost of shipping both. singleton: true gives up that guarantee to keep one instance, and warns when the chosen version does not satisfy a consumer.

shareKey

string

The requested shared module is looked up under this key from the shared scope. It defaults to the key you used in shared, i.e. the request itself, so shared: ['lodash'] is stored and looked up as lodash.

Set it when the name a build imports and the name it should share under differ. Two builds only share a module when both the key and the scope match, so the key is what lets an application that imports lodash-es reuse what another build provided as lodash:

new ModuleFederationPlugin({
  shared: {
    "lodash-es": {
      shareKey: "lodash",
    },
  },
});
shareScope

string

The name of the shared scope, defaulting to the plugin's own shareScope option and, when that is unset, to 'default'. A scope is one namespace of shared modules: builds put the modules they provide into it and look the ones they need up from it, so builds using different scope names never share with each other even when their keys match.

Change it to keep groups of builds apart — for example when a page hosts two independent sets of remotes that should each get their own copy of a library, or when a widget must not adopt the host's version of one. Both the providing and the consuming side have to name the same scope.

singleton

boolean

This hint only allows a single version of the shared module in the shared scope (disabled by default). Some libraries use a global internal state (e.g. react, react-dom). Thus, it is critical to have only one instance of the library running at a time.

In cases where there are multiple versions of the same dependency in the shared scope, the highest semantic version is used.

strictVersion

boolean

This hint allows webpack to reject the shared module if version is not valid (defaults to true when local fallback module is available and shared module is not a singleton, otherwise false, it has no effect if there is no required version specified). Throws a runtime error if the required version is not found.

version

false | string

The version of the provided module. It allows webpack to replace lower matching versions, but not higher.

By default, webpack uses the version from the package.json file of the dependency.

Additional examples

export default {
  plugins: [
    new ModuleFederationPlugin({
      // adds vue as shared module
      // version is inferred from package.json
      // it will always use the shared version, but print a warning when the shared vue is < 2.6.5 or >= 3
      shared: {
        vue: {
          requiredVersion: "^2.6.5",
          singleton: true,
        },
      },
    }),
  ],
};
export default {
  plugins: [
    new ModuleFederationPlugin({
      // adds vue as shared module
      // there is no local version provided
      // it will emit a warning if the shared vue is < 2.6.5 or >= 3
      shared: {
        vue: {
          import: false,
          requiredVersion: "^2.6.5",
        },
      },
    }),
  ],
};
export default {
  plugins: [
    new ModuleFederationPlugin({
      // adds vue as shared module
      // there is no local version provided
      // it will throw an error when the shared vue is < 2.6.5 or >= 3
      shared: {
        vue: {
          import: false,
          requiredVersion: "^2.6.5",
          strictVersion: true,
        },
      },
    }),
  ],
};
export default {
  plugins: [
    new ModuleFederationPlugin({
      shared: {
        "my-vue": {
          // can be referenced by import "my-vue"
          import: "vue", // the "vue" package will be used as a provided and fallback module
          shareKey: "shared-vue", // under this name the shared module will be placed in the share scope
          shareScope: "default", // share scope with this name will be used
          singleton: true, // only a single version of the shared module is allowed
          strictVersion: true, // don't use shared version when version isn't valid. Singleton or modules without fallback will throw, otherwise fallback is used
          version: "1.2.3", // the version of the shared module
          requiredVersion: "^1.0.0", // the required version of the shared module
        },
      },
    }),
  ],
};

Further Reading

Edit this page·

5 Contributors

XiaofengXie16chenxsanburhanudaychristian24KhaledTaymour