Angular dynamic modules at runtime with Module Federation
Tags: angular, webpack, modulefederation, microfrontend
Published: 2021-05-20 21:54:34

Angular 12 recently launched with the added enhancements of Webpack 5 and opening the door to using module federation. If you are looking for a great deep-dive into module federation and micro-frontends, I suggest reading: https://www.angulararchitects.io/aktuelles/the-microfrontend-revolution-module-federation-in-webpack-5/.

## Micro frontends

Micro frontends and more importantly module federation, allows developers the flexibility of remotely requesting a module on the network and bootstrapping that module into their application. Similar to lazy-loading, remotely loading modules can greatly reduce the bundle size of your application and the network cost to loading modules that end up unused by your users. 

There's other benefits to micro-frontends, including:
- A/B serving features
- Incremental updates
- Independent versioning of features
- Dynamic feature resolutions

---

## Getting started

The Angular Architects package `@angular-architects/module-federation` creates a simple API to request modules and pull them into your application. 

Assuming an NX mono-repo set-up:

To add module federation to your workspace, run:
```
nx add @angular-architects/module-federation@next
```

This will install the necessary dependency, with the schematics needed to add remote apps to be consumed by module federation.

Let's assume you have the following mono-repo:
```
apps/
    shell/
    remote/
```

**Shell** is your consuming application. It is the highest container, responsible for what pieces are pulled in and the composition of features.

**Remote** is the feature set, isolated and decoupled to be pulled in on-demand, by the shell.

To make these apps compatible with module federation, you will need to run the schematic on their projects:
```
nx add @angular-architects/module-federation --project shell --port 5000
nx add @angular-architects/module-federation --project remote --port 6000
```

You can configure the port to be whatever you desire. This only matters for local development.

This schematic will:
- Generate a `webpack.config.js` and `webpack.config.prod.js` with a boilerplate for module federation
- Update `angular.json` for the project definition, to reference the `extraWebpackConfig` and update the project's port to the value specified 
- Split the bootstrap logic of your app from `main.ts` to `bootstrap.ts` and reference the function in `main.ts`.

### Module Federation Plugin

Inside your `webpack.config.js` you will want to get accommodated with the config for module federation.

```js
module.exports = {
    output: {
        uniqueName: 'remote',
        publicPath: 'auto',
    },
    optimization: {
        runtimeChunk: false,
    },
    resolve: {
        alias: {
            ...sharedMappings.getAliases(),
        },
    },
    plugins: [
        new ModuleFederationPlugin({
            name: 'remote',
            filename: 'remoteEntry.js',
            exposes: {
                './Module':
                    './apps/remote/src/app/app.module.ts',
            },
            shared: {
                '@angular/core': {
                    singleton: true,
                    strictVersion: true,
                    requiredVersion: '>= 12.0.0',
                },
                '@angular/common': {
                    singleton: true,
                    strictVersion: true,
                    requiredVersion: '>= 12.0.0',
                },
                '@angular/common/http': {
                    singleton: true,
                    strictVersion: true,
                    requiredVersion: '>= 12.0.0',
                },
                '@angular/router': {
                    singleton: true,
                    strictVersion: true,
                    requiredVersion: '>= 12.0.0',
                },
                ...sharedMappings.getDescriptors(),
            },
        }),
        sharedMappings.getPlugin(),
    ],
};
```

- `name` should align with your `output.uniqueName` and match your shell app's webpack config for the remotes section.
- `fileName` is the name of the generated file's entry point to your remote module. This file name will not be renamed in the build process and is the asset you will be referencing in your shell to request the module.
- `exposes` is the named paths to modules, components, etc. that you want to make accessible to the shell to pull in. I'll explain this further below.
- `shared` the shared dependencies (and rules) between your remote and shell app. This allows tight control for your remote to not re-declare modules/services that you expect to be singleton, or prevent mismatched versions of Angular or other libraries existing in the eco-system. By assigning `strictVersion` to `true`, the build will quick fail if an issue occurs. Removing this option will potentially pass the build, but display warnings in the dev console.

You can now locally run your shell and remote with:
```
nx serve shell -o
nx serve remote -o
```

> `-o` will automatically launch the apps in your default browser

### Exposes (continued)

While the example schematic will generate the `exposes` section with the `AppModule` and `AppComponent` I would **strongly** advise against this.

When serving the remote and shell to develop locally, the sites will be deployed to:
- localhost:5000
- localhost:6000

When you make changes to the `remote` app folder's contents, only `localhost:6000` will live-reload.

This means for local development, consuming the remote into the shell app is not sustainable for development against remote-specific functionality. 

So what do I propose?

The `AppModule` of your remote app should be your "demo" or self-deployed landscape. You will import modules and providers to establish a foundation to locally test your remote app in isolation. The `AppModule` should have a separate module of the cohesive functionality you are wanting to expose, i.e: `LoginModule`. 

With this approach, exposing and pulling in `AppModule` has the potential to pulling in duplicate root providers; as well as pulling duplicate assets and styles. 

Instead with:
```
exposes: {
    './Module':
        './apps/remote/src/app/login/login.module.ts',
},
```

> `./Module` is nomenclature you can define as you please. I would recommend being more specific in a diverse system.

The shell app still can access the shared functionality to pull in, but doesn't pull in more than it needs to. 

I can locally develop on `localhost:6000`, having an accurate test bed for my application and live-dev against the changes with ease.

Now that the foundation of module federation have been set, let's jump into dynamically swapping modules at runtime.

---

## Dynamic Runtime modules

All of the top resources available for module federation show statically referencing the modules in your shell app's route definition.

```ts
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)
    },
    [...]
];
```

This serves a purpose when your application wants to independently build and manage known features. This doesn't however allow you conditionally serve features or create an application that does not have context of what features exist at build time. 

![Remote module diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c4tbpdw8mba4h33hh2y5.png)

### Dynamic module federation

Dynamic module federation attempts to resolve this by allowing you independently request modules before bootstrapping Angular:

```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));

```

![Dynamic module federation diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o7zhk5bbe7z8a7mbdzjw.png)

Better... but still has a few drawbacks:
- What if my remote module is routable? Will it recognize the route when I navigate directly to it?
- How does this impact lazy loading?
- Remote entries are still hard-coded

### Dynamic runtime module federation

We need the ability to have a decoupled shell, that can dynamically request federated modules at runtime. 

#### A real use case?

On our team, we want to dynamically serve separate authentication experiences for customers. Some customers use our platform's stock username/password authentication. Others have their own corporate SSO. All of them have strict branding standards that aren't compatible with each other. 

We do however, want all customers to share the primary functionality of our platform - content management and learning delivery. Once they login to the application, they only need branding for their corporate logo and primary brand color; they can use all the existing interfaces.

![Dynamic runtime modules](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/564dnws571d4nhkoy7mg.png)

#### Less rigid example?

Feature toggles in an application. Some customers have "X" others have "Y". You want to serve one app that can respond to "X" and "Y".

#### Getting started

Authentication deals with routing and we need to allow our users to navigate to `/authentication/login` and get served the correct federated module for their company.

We will be using an injection token to store our route definitions as they relate to module federation.

```ts 
export const PLATFORM_ROUTES = new InjectionToken<Routes>('Platform routes for module federation');
```

If you used the the schematic discussed above, you should have a `bootstrap.ts` file. Prior to bootstrapping Angular, we need to request the registry of the modules that should exist for this user. This can be any network call, for this demo we will use a local JSON asset called `platform-config.json`

Platform config is going to describe all the modules, the location of the modules, the module name to bootstrap and the route to register in the shell app for the remote module.

```
{
    "authentication": {
        "path": "authentication",
        "remoteEntry": "http://localhost:5001/remoteEntry.js",
        "remoteName": "coreAuthentication",
        "exposedModule": "./LoginModule",
        "exposedModuleName": "LoginModule"
    }
}
```

- `path` is the Angular route namespace to load the remote module under.
- `remoteEntry` is the served location of your remote module. This would be replaced with the served location (CDN, CloudFoundry, S3 asset, etc.) in a built environment. This currently references where we will be serving our Angular apps for local development.
- `exposedModule` is the key in your remote app's `webpack.config.js` for the exposed module (your nomenclature)
- `exposedModuleName` is the name of the Angular module that was exposed, this is leveraged for lazy loading.

In `bootstrap.ts` we will consume this asset and build the injection token value:

```ts
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { Routes } from '@angular/router';
import { loadRemoteModule } from '@angular-architects/module-federation';

import { AppModule } from './app/app.module';
import { PLATFORM_ROUTES } from './app/platform-routes';
import { environment } from './environments/environment';

if (environment.production) {
    enableProdMode();
}
fetch('/assets/platform-config.json').then(async (res) => {
    const config = await res.json();

    const platformRoutes: Routes = [];
    for (const [key, value] of Object.entries<any>(config)) {
        platformRoutes.push({
            path: value.path,
            loadChildren: () =>
                loadRemoteModule({
                    remoteEntry: value.remoteEntry,
                    remoteName: value.remoteName,
                    exposedModule: value.exposedModule,
                }).then((m) => m[value.exposedModuleName]),
        });
    }

    platformBrowserDynamic([
        {
            provide: PLATFORM_ROUTES,
            useValue: platformRoutes,
            multi: true,
        },
    ])
        .bootstrapModule(AppModule)
        .catch((err) => console.error(err));
});
```

By passing the providers to `platformBrowserDynamic`, we are setting a static provider value prior to bootstrap, that can be used on bootstrap.

In the module responsible for your shell app's router module declaration (typically `app-routing.module.ts`), update as follows:

```ts
import { NgModule } from '@angular/core';
import { RouterModule, ROUTES, Routes } from '@angular/router';

import { PLATFORM_ROUTES } from './platform-routes';

@NgModule({
    imports: [
        RouterModule.forRoot(
            [
                /* Declare root routes in the factory below */
            ],
            { initialNavigation: 'enabled' }
        ),
        {
            ngModule: RouterModule,
            providers: [
                {
                    provide: ROUTES,
                    useFactory: (
                        staticRoutes: Routes = [],
                        dynamicRoutes: Routes = []
                    ) => {
                        let rootRoutes: Routes = [];

                        if (Array.isArray(staticRoutes)) {
                            rootRoutes = [...staticRoutes];
                        }
                        if (Array.isArray(dynamicRoutes)) {
                            rootRoutes = [...rootRoutes, ...dynamicRoutes];
                        }
                        rootRoutes.push({
                            path: '**',
                            redirectTo: '/authentication/login',
                        });
                        return rootRoutes;
                    },
                    deps: [ROUTES, PLATFORM_ROUTES],
                },
            ],
        },
    ],
    exports: [RouterModule],
})
export class AppRoutingModule {}

```

Let's explain a bit...

`RouterModule.forRoot([])` establishes a lot of necessary providers and functionality required for routing. Under the hood, all router modules roll-up the route definition to an injection token named `ROUTES`. We can bootstrap the module and immediately provide a new value on-top for the `ROUTES` value.

To allow our shell app to have it's own built-in routes as well as the dynamic runtime routes, we use a factory to concat `rootRoutes` and the dynamicRoutes (from our injection token `PLATFORM_ROUTES`). 

Lastly, we have a fallback route, as routes will execute first-to-last, to handle global redirect behavior for unhandled routes.

## Conclusion

At this point, we are rolling. We can now change our config while serving the different remotes and shell and see it swap out the served bundle. In a real environment, the config data would come from an endpoint.

![Swapping different login screens](https://media4.giphy.com/media/jiEjayc2G3MijKTMTa/giphy.gif)

If you read this far I appreciate it. Module federation in Angular is a very new concept and I welcome feedback and questions on this topic!