What’s New in our Module Federation Plugin 14.3?

v14.3 comes with support for Angular 14 and a new and more streamlined configuration. Also, we add support for eager and pinned dependencies.

  1. The Microfrontend Revolution: Module Federation in Webpack 5
  2. The Microfrontend Revolution: Module Federation with Angular
  3. Dynamic Module Federation with Angular
  4. Building A Plugin-based Workflow Designer With Angular and Module Federation
  5. Getting Out of Version-Mismatch-Hell with Module Federation
  6. Using Module Federation with (Nx) Monorepos and Angular
  7. Pitfalls with Module Federation and Angular
  8. Multi-Framework and -Version Micro Frontends with Module Federation: Your 4 Steps Guide
  9. Module Federation with Angular’s Standalone Components
  10. What’s New in our Module Federation Plugin 14.3?

With version 14.3 of our CLI plugin @angular-architects/module-federation, we introduced support for Angular 14 and a new and more streamlined way of configuring Module Federation. Also, we add support for eager and pinned dependencies. This article shows the highlights. 📂 Source Code

Updating to 14.3

This library supports ng update:

ng update @angular-architects/module-federation

If you update by hand (e. g. via npm install), make sure you also install a respective version of ngx-build-plus (version 14 for Angular 14, version 13 for Angular 13, etc.)

Streamlined Configuration

While we are using customer-specific helper methods for quite a time, version 14.3 comes with a new official one called withModuleFederationPlugin. It just takes the configuration data people normally adjust. The old way still works, but you might want to move over to the new more concise approach using the new withModuleFederationPlugin helper function. The updated init schematic and ng add use this new way automatically if you set the --type switch to host, dynamic-host, or remote:

ng add @angular-architects/module-federation --project mfe1 --port 4201 --type remote

If you go with Nx, use npm install and the init schematic instead:

npm i @angular-architects/module-federation -D 
 ng g @angular-architects/module-federation:init --project mfe1 --port 4201 --type remote

This is an example for configuring a remote with the new streamlined form:

const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');

 // Version 14
 module.exports = withModuleFederationPlugin({

   name: 'mfe1',

   exposes: {
     './Module': './projects/mfe1/src/app/flights/flights.module.ts',
   },

   shared: {
     ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
   },

 });

In version 13, the same looked like this:

// Version 13
 const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
 const mf = require("@angular-architects/module-federation/webpack");
 const path = require("path");

 const share = mf.share;

 const sharedMappings = new mf.SharedMappings();
 sharedMappings.register(
   path.join(__dirname, '../../tsconfig.json'),
   ['auth-lib']  
 );

 module.exports = {
   output: {
     uniqueName: "mfe1",
     publicPath: "auto"
   },
   optimization: {
     runtimeChunk: false
   },  
   resolve: {
     alias: {
       ...sharedMappings.getAliases(),
     }
   },
   experiments: {
     outputModule: true
   },  
   plugins: [
     new ModuleFederationPlugin({
         library: { type: "module" },

         // For remotes (please adjust)
         name: "mfe1",
         filename: "remoteEntry.js",  // 2-3K w/ Meta Data
         exposes: {
             './Module': './projects/mfe1/src/app/flights/flights.module.ts',
         },        
         shared: share({
           "@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
           "@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
           "@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
           "@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, 

           // Uncomment for sharing lib of an Angular CLI or Nx workspace
           ...sharedMappings.getDescriptors()
         })

     }),
     // Uncomment for sharing lib of an Angular CLI or Nx workspace
     sharedMappings.getPlugin(),
   ],
 };

The passed configuration is a superset of the settings provided by webpack's ModuleFederationPlugin. Also, it uses some smart defaults. Hence, consumers can build on their existing knowledge. These defaults are:

  • library: { type: "module" }: This is what you need for Angular >= 13 as CLI 13 switched over to emitting "real" EcmaScript modules instead of just ordinary JavaScript bundles.
  • filename: 'remoteEntry.js': This makes Module Federation emit a file remoteEntry.js with the remote entry point.
  • share: shareAll(...): This shares all packages found in the dependencies section of your package.json by default (see remarks below).
  • sharedMappings: If you skip the sharedMappings array, all local libs (aka mono repo-internal libs or mapped paths) are shared. Otherwise, only the mentioned libs are shared. This setting replaces the SharedMappings class used in the classic configuration (actually, it's now used under the hoods).

Remarks on shareAll

As mentioned above, withModuleFederationPlugin uses shareAll by default. This allows for a quick first setup that "just works". However, it might lead to too much shared bundles. As shared dependencies cannot be tree shaken and by default end up in separate bundles that need to be loaded, you might want to optimize this behavior by switching over from shareAll to the share helper:

// Import share instead of shareAll:
 const { share, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');

 module.exports = withModuleFederationPlugin({

     // Explicitly share packages:
     shared: share({
         "@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, 
         "@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' }, 
         "@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' },                     
         "@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
     }),

     // Explicitly share mono-repo libs:
     sharedMappings: ['auth-lib'],

 });

Eager and Pinned Dependencies

Big thanks to Michael Egger-Zikes, who came up with these solutions.

Module Federation allows to directly bundle shared dependencies into your app's bundles. Hence, you don't need to load an additional bundle per shared dependency. This can be interesting to improve an application's startup performance, when there are lots of shared dependencies. One possible usage for improving the startup times is to set eager to true just for the host. The remotes loaded later can reuse these eager dependencies although they've been shipped via the host's bundle (e. g. its main.js). This works best, if the host always has the highest compatible versions of the shared dependencies. Also, in this case, you don't need to load the remote entry points upfront. While the eager flag is an out of the box feature provided by module federation since its very first days, we need to adjust the webpack configuration generated by the Angular CLI a bit to avoid code duplication in the generated bundles. The new withModuleFederationPlugin helper is the basis for the new streamlined configuration, does this by default. The config just needs to set eager to true.

module.exports = withModuleFederationPlugin({

   shared: {
     ...shareAll({ singleton: true, eager: true, pinned: true, strictVersion: true, requiredVersion: 'auto' }),
   },

 });

As shown in the last example, we also added another property: pinned. This makes sure, the shared dependency is put into the application's (e. g. the host's) bundle, even though it's not used there. This allows to preload dependencies that are needed later but subsequently loaded micro frontends via one bundle.

Dynamic Configuration and "Registry" Services

This feature was inspired by Nx we use a lot together with Module Federation. It's about helper functions for loading a configuration file with the Micro Frontend's urls and remote entry points:

{
     "mfe1": "http://localhost:4201/remoteEntry.js"
 }

We go with the term introduced by Nx and also call this the Module Federation Manifest. A new helper function called loadManifest allows to load the manifest:

import { loadManifest } from '@angular-architects/module-federation';

 loadManifest('assets/mf.manifest.json')
     .catch(err => console.error('Error loading remote entries', err))
     .then(() => import('./bootstrap'))
     .catch(err => console.error(err));

By default, loadManifest also loads all the remote entry points. However, there is a second optional parameter called skipRemoteEntries you can set to true to avoid this automatism:

loadManifest('assets/mf.manifest.json', true)

Loading a remote configured by the manifest looks like this:

{
     path: 'flights',
     loadChildren: () =>
         loadRemoteModule({
             type: 'manifest',
             remoteName: 'mfe1',
             exposedModule: './Module'
         })
         .then(m => m.FlightsModule)
 },

The ng add command discussed initially also provides an option --type dynamic-host. This makes ng add to generate the mf.manifest.json and the call to loadManifest in the main.ts.

Automatically Adding Secondary Entry Points

Since version 14.3, our share helpers (share, shareAll) add secondary entry points by default. Hence, if @angular/common is shared, @angular/common/http is shared too. While this feature was an opt-in in previous versions, it's now on by default. If you want to turn it off or fine-tune it, you can use the includeSecondaries flag in our share helpers. For instance, the following setting turns this feature off:

shared: share({
     "@angular/common": { 
         singleton: true, 
         strictVersion: true,
         requiredVersion: 'auto',
         includeSecondaries: false
     },
     [...]
 })

Another option is to activate it using a list with libraries that are skipped:

shared: share({
     "@angular/common": { 
         singleton: true, 
         strictVersion: true,
         requiredVersion: 'auto',
         includeSecondaries: {
             skip: ['@angular/common/http/testing']
         }
     },
     [...]
 })

While skipping libraries reduce the amount of generated bundles, it does not necessarily increase the overhead at runtime as Module Federation only loads the needed dependencies.

Secondary Entry Points and the Angular Package Format 14

Before Angular 14, we needed to search the node_modules folders of the shared dependencies for secondary entry points. Fortunately, the Angular Package Format 14 defines that each Angular library needs to declare its secondary entry points in its package.json. Also, ng-packagr used by the CLI when building a library has been adjusted to this. Since version 14.3, we use this meta data to purposefully get information about secondaries. If this meta data is not there, we are still probing the node_modules folder.

run:all With Parameters

The last one is a quite small feature. The init and ng-add schematics add an npm script run:all since several versions. This script starts all apps in the repository. Now, you can specify which apps to start by adding their names as command line arguments. If no app is specified that way, all applications are still started. However, there is one exception: End-2-End test projects are always skipped.

run:all## What's next? More on Architecture!

This article has shown several new features of the Module Federation plugin. However, when dealing with Module Federation, Micro Frontenends, and huge architectures, several advanced questions come in mind, e. g.:

  • According to which criteria can we subdivide a huge application into sub-domains?
  • How can we make sure, the solution is maintainable for years or even decades?
  • Which options from Micro Frontends are provided by Module Federation?

Our free eBook (about 120 pages) covers all these questions and more: freeFeel free to download it here now!