Manage Library Versions

Federated modules are bundled and packaged independently with all the dependencies they need to run smoothly in federated applications called remotes. This means that if you have a federated module that depends on a library, the library will be bundled with the federated module within a remote. This independence provides much flexibility, allowing individual federated modules to function without relying on external resources.

A challenge arises when these federated modules are integrated into a host or other remotes. Given that each federated module carries its own dependencies, the host application may have inadvertently downloaded multiple copies of the same dependency. This redundancy does two things:

  1. Multiple copies of the same dependency can create bottlenecks and conflicts, resulting in unexpected behaviour.
  2. With redundant dependencies, the host application can become bloated, increasing the bandwidth and consuming more memory and resources on the user's device.

To mitigate these issues, Module Federation has a shared API. Its primary function is to act as a gatekeeper, ensuring that only one copy of a dependency is downloaded, regardless of how many federated modules request it.

How it works

The Shared API maintains a registry of all the downloaded dependencies. When a federated module requests a dependency, the Shared API checks the registry. If the dependency already exists, the module is directed to use the existing copy. If not, the dependency is downloaded and added to the registry.

How Shared API works

Lost?

If you are not familiar with the concepts of federated modules, remotes, and hosts, please read the Faster builds with module federation for an introduction.

Our Approach

Although the Shared API is a powerful tool, it can be challenging to manage. The Shared API is configured in the Module Federation Config File, which is a JavaScript or TypeScript file. This file is not part of the build process, so should you want to use a different version of a workspace dependency, you would have to manually record the change outside of the build process which can be a tedious and error-prone.

Nx follows the Single-Responsibility Principle (SRP) for managing library versions. Each library is responsible for its version. This approach is beneficial because it allows you to update a library without updating the entire application. Additionally, with this approach, consumers of the library (remotes/hosts) can dictate which version of the library they want to use, which can be further predicated by versioning schemes like SemVer.

How are library versions managed?

With Nx there are two ways to manage how library versions are shared / managed with Module Federation:

1. Opt in to sharing library versions

This is the default behaviour for Nx. All dependencies are singletons and will be shared between remotes and hosts.

2. Opt out from sharing library versions

This means that the library will not be shared between remotes and hosts. Each remote and host will load its own version of the library. A common use-case for this is if you want to enable tree-shaking for a library like lodash. If you share this library, it will be bundled with the remote and host, and tree-shaking will not be possible.

How are library versions determined?

Nx determines the version of a library by looking at a package.json. If the library is an npm package, the version is determined by the version declared in the workspace package.json. If the library is a workspace library, the version is determined by the version in the package.json of the project that consumes the shared library. RemoteA consumes Counter, which is a workspace library exposed and shared by RemoteB. The version of Counter is determined by the version in RemoteB's package.json. If the package.json does not exist or the library is not declared, Nx will use the version in the package.json of the workspace library.

How Nx determines library versions

There are twos ways to manage library versions with Nx:

remote/module-federation.config.ts
1import { ModuleFederationConfig } from '@nx/webpack'; 2 3const config: ModuleFederationConfig = { 4 name: 'remote', 5 exposes: { 6 './Module': './src/remote-entry.ts', 7 }, 8 // Determine which libraries to share 9 shared: (packageName: string) { 10 // I do not want to share this package and I will load my own version 11 if(packageName === '@acme/utils') return false; 12 } 13}; 14export default config; 15
Nx 15 and lower use @nrwl/ instead of @nx/

This would result in the following webpack config:

webpack.config.js
1module.exports = { 2 plugins: [ 3 new ModuleFederationPlugin({ 4 // additional config 5 name: 'remote', 6 shared: { 7 react: { singleton: true, eager: true }, 8 // acme/utils will not be shared 9 }, 10 }), 11 ], 12}; 13