Seamlessly using Webpack Module Federation with the Angular CLI.
This is a fork from @angular-architects/module-federation with some bugfixes.
The code was not written by me but by Manfred Steyer. Check out his awesome work on explaining Angular and Webpack 5 Module Federation.
Big thanks to the following people who helped to make this possible:
- Tobias Koppers, Founder of Webpack
- Dmitriy Shekhovtsov, Angular GDE
- Angular CLI 11
Module Federation allows loading separately compiled and deployed code (like micro frontends or plugins) into an application. This plugin makes Module Federation work together with Angular and the CLI.
✅ Generates the skeleton for a Module Federation config.
✅ Installs a custom builder to enable Module Federation.
✅ Assigning a new port to serve (ng serve
) several projects at once.
The module federation config is a partial webpack configuration. It only contains stuff to control module federation. The rest is generated by the CLI as usual.
Since Version 1.2, we also provide some advanced features like:
✅ Dynamic Module Federation support
✅ Sharing Libs of a Monorepo
ng add @angular-architects/module-federation
- Adjust the generated
webpack.config.js
file - Repeat this for further projects in your workspace (if needed)
-
You need to use yarn b/c it allows to override dependencies
- Existing Projects:
ng config -g cli.packageManager yarn
- New Projects:
ng new workspace-name --packageManager yarn
- Existing Projects:
-
Add this to your
package.json
(e. g. before thedependencies
section) to force the CLI into webpack 5:"resolutions": { "webpack": "^5.0.0" },
-
Run yarn to install all packages
Please that the CLI's webpack 5 support is experimental in CLI 11. Here, you find a list with unresolved issues in the current version.
Please find here a tutorial that shows how to use this plugin.
Please have a look at this article series about Module Federation.
This example loads a microfrontend into a shell:
Please have a look into the example's readme. It points you to the important aspects of using Module Federation.
While the above-mentioned tutorial and blog articles guide you through using Module Federation, this section draws your attention to some advanced aspects of this plugin and Module Federation in general.
Since version 1.2, we provide helper functions making dynamic module federation really easy. Just use our loadRemoteModule
function instead of a dynamic include
, e. g. together with lazy routes:
import { loadRemoteModule } from '@angular-architects/module-federation';
[...]
const routes: Routes = [
[...]
{
path: 'flights',
loadChildren: () =>
loadRemoteModule({
remoteEntry: 'http://localhost:3000/remoteEntry.js',
remoteName: 'mfe1',
exposedModule: './Module'
})
.then(m => m.FlightsModule)
},
[...]
]
If somehow possible, load the remoteEntry
upfront. This allows Module Federation to take the remote's metadata in consideration when negotiating the versions of the shared libraries.
For this, you could call loadRemoteEntry
BEFORE bootstrapping Angular:
// main.ts
import { loadRemoteEntry } from '@angular-architects/module-federation';
Promise.all([loadRemoteEntry('http://localhost:3000/remoteEntry.js', 'mfe1')])
.catch((err) => console.error('Error loading remote entries', err))
.then(() => import('./bootstrap'))
.catch((err) => console.error(err));
The bootstrap.ts
file contains the source code normally found in main.ts
and hence, it calls platform.bootstrapModule(AppModule)
. You really need this combination of an upfront file calling loadRemoteEntry and a dynamic import loading another file bootstrapping Angular because Angular itself is already a shared library respected during the version negotiation.
Then, when loading the remote Module, just skip the remoteEntry
property:
import { loadRemoteModule } from '@angular-architects/module-federation';
[...]
const routes: Routes = [
[...]
{
path: 'flights',
loadChildren: () =>
loadRemoteModule({
// Skipped - already loaded upfront:
// remoteEntry: 'http://localhost:3000/remoteEntry.js',
remoteName: 'mfe1',
exposedModule: './Module'
})
.then(m => m.FlightsModule)
},
[...]
]
Let's assume, you have an Angular CLI Monorepo or an Nx Monorepo using path mappings in tsconfig.json
for providing libraries:
"shared-lib": [
"projects/shared-lib/src/public-api.ts",
],
You can now share such a library across all your micro frontends (apps) in your mono repo. This means, this library will be only loaded once.
To accomplish this, just register this lib name with the SharedMappings
instance in your webpack config:
const mf = require("@angular-architects/module-federation/webpack");
const path = require("path");
[...]
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(
path.join(__dirname, '../../tsconfig.json'),
['auth-lib']
);
Beginning with version 1.2, the boilerplate for using SharedMappings
is generated for you. You only need to add your lib's name here.
This generated code includes providing the metadata for these libraries for the ModuleFederationPlugin
and adding a plugin making sure that even source code generated by the Angular Compiler uses the shared version of the library.
plugins: [
new ModuleFederationPlugin({
[...]
shared: {
[...]
...sharedMappings.getDescriptors()
}
}),
sharedMappings.getPlugin(),
],
Currently, there is, unfortunately, a bug in the experimental CLI/webpack5 integration causing issues when using shared libraries together with components pointing to styleUrls
. For the time being, you can work around this issue by removing all styleUrls
in your applications and libraries.
If you shared a local library that is not even used, you get the following error:
./projects/shared-lib/src/public-api.ts - Error: Module build failed (from ./node_modules/@ngtools/webpack/src/index.js):
Error: C:\Users\Manfred\Documents\projekte\mf-plugin\example\projects\shared-lib\src\public-api.ts is missing from the TypeScript compilation. Please make sure it is in your tsconfig via the 'files' or 'include' property.
at AngularCompilerPlugin.getCompiledFile (C:\Users\Manfred\Documents\projekte\mf-plugin\example\node_modules\@ngtools\webpack\src\angular_compiler_plugin.js:957:23)
at C:\Users\Manfred\Documents\projekte\mf-plugin\example\node_modules\@ngtools\webpack\src\loader.js:43:31
If you use a shared component without exporting it via your library's barrel (index.ts
or public-api.ts
), you get the following error at runtime:
core.js:4610 ERROR Error: Uncaught (in promise): TypeError: Cannot read property 'ɵcmp' of undefined
TypeError: Cannot read property 'ɵcmp' of undefined
at getComponentDef (core.js:1821)