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 fileremoteEntry.js
with the remote entry point.share: shareAll(...)
: This shares all packages found in the dependencies section of yourpackage.json
by default (see remarks below).sharedMappings
: If you skip thesharedMappings
array, all local libs (aka mono repo-internal libs or mapped paths) are shared. Otherwise, only the mentioned libs are shared. This setting replaces theSharedMappings
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 makesng add
to generate themf.manifest.json
and the call toloadManifest
in themain.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.
## 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: Feel free to download it here now!