# @angular-architects/module-federation

Seamlessly using Webpack Module Federation with the Angular CLI.

## Thanks

Big thanks to the following people who helped to make this possible:

- [Tobias Koppers](https://twitter.com/wSokra), Founder of Webpack
- [Dmitriy Shekhovtsov](https://twitter.com/valorkin), Angular GDE 

## Prequisites

- Angular CLI 12 

## Motivation 💥

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.

## Features 🔥

✅ 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

## What's new in Version 12.0.0-beta.x?

✅ Works with CLI 12.0.0-rc.1 that brings webpack 5 out of the box

✅ Issues with sharing libs in monorepos are resolved (always worked with multiple repos)

## Upgrade from Version 1.x

After updating the libs, you need to adjust the ``webpack.conf.js`` a bit:


```diff
module.exports = {
  output: {
    uniqueName: "delme3",
+    publicPath: "auto"
  },
  optimization: {
    runtimeChunk: false
  },   
+  resolve: {
+    alias: {
+      ...sharedMappings.getAliases(),
+    }
+  },
  [...]
}
```

## Usage 🛠️

1. ``ng add @angular-architects/module-federation@next``
2. Adjust the generated ``webpack.config.js`` file
3. Repeat this for further projects in your workspace (if needed)

## Getting Started 🧪

Please find here a [tutorial](https://github.com/angular-architects/module-federation-plugin/blob/12.0.0/libs/mf/tutorial/tutorial.md) that shows how to use this plugin.

![Microfrontend Loaded into Shell](https://github.com/angular-architects/module-federation-plugin/raw/main/libs/mf/tutorial/result.png)

[>> Start Tutorial](https://github.com/angular-architects/module-federation-plugin/blob/main/libs/mf/tutorial/tutorial.md)

## Documentation 📰

Please have a look at this [article series about Module Federation](https://www.angulararchitects.io/aktuelles/the-microfrontend-revolution-part-2-module-federation-with-angular/).

## Example 📽️

This [example](https://github.com/manfredsteyer/module-federation-plugin-example)
 loads a microfrontend into a shell:

![Microfrontend Loaded into Shell](https://github.com/angular-architects/module-federation-plugin/raw/main/packages/mf/tutorial/result.png)

Please have a look into the example's **readme**. It points you to the important aspects of using Module Federation.


## Advanced Features

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. 

### Dynamic Module Federation

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:

```typescript
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:

```typescript
// 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:

```typescript
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)
    },
    [...]
]
```

### Sharing Libs of a Monorepo

Let's assume, you have an Angular CLI Monorepo or an Nx Monorepo using path mappings in ``tsconfig.json`` for providing libraries:

```json
"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:

```javascript
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. 

```javascript
plugins: [
    new ModuleFederationPlugin({
        [...]
        shared: {
            [...]
            ...sharedMappings.getDescriptors()
        }
    }),
    sharedMappings.getPlugin(),
],
```

### Share Helper

The helper function share adds some additional options for the shared dependencies:

```json
shared: share({
    "@angular/common": { 
        singleton: true, 
        strictVersion: true,
        requiredVersion: 'auto',
        includeSecondaries: true
    },
    [...]
})
```

The added options are ``requireVersion: 'auto'`` and ``includeSecondaries``.

#### requireVersion: 'auto'

If you set ``requireVersion`` to ``'auto'``, the helper takes the version defined in your ``package.json``. 

This helps to solve issues with not (fully) met peer dependencies and secondary entry points (see Pitfalls section below).

By default, it takes the ``package.json`` that is closest to the caller (normally the ``webpack.config.js``). However, you can pass the path to an other ``package.json`` using the second optional parameter. Also, you need to define the shared libray within the node dependencies in your ``package.json``.

Instead of setting requireVersion to auto time and again, you can also skip this option and call ``setInferVersion(true)`` before:

```typescript
setInferVersion(true);
```

#### includeSecondaries

If set to ``true``, all secondary entry points are added too. In the case of ``@angular/common`` this is also ``@angular/common/http``, ``@angular/common/http/testing``, ``@angular/common/testing``, ``@angular/common/http/upgrade``, and ``@angular/common/locales``. This exhaustive list shows that using this option for ``@angular/common`` is not the best idea because normally, you don't need most of them.

However, this option can come in handy for quick experiments or if you want to quickly share a package like ``@angular/material`` that comes with a myriad of secondary entry points. 

Even if you share too much, Module Federation will only load the needed ones at runtime. However, please keep in mind that shared packages can not be tree-shaken.

To skip some secondary entry points, you can assign a configuration option instead of ``true``:

```typescript
shared: share({
    "@angular/common": { 
        singleton: true, 
        strictVersion: true,
        requiredVersion: 'auto',
        includeSecondaries: {
            skip: ['@angular/http/testing']
        }
    },
    [...]
})
```

#### shareAll

The ``shareAll`` helper shares all your dependencies defined in your ``package.json``. The ``package.json`` is look up as described above:

```json
shared: {
  ...shareAll({ 
      singleton: true, 
      strictVersion: true, 
      requiredVersion: 'auto' 
  }),
  ...sharedMappings.getDescriptors()
}
```

The options passed to shareAll are applied to all dependencies found in your ``package.json``.

This might come in handy in an mono repo scenario and when doing some experiments/ trouble shooting.

### Nx Integration

If the plugin detects that you are using Nx (it basically looks for a ``nx.json``), it uses the builders provided by Nx.

### Angular Universal (Server Side Rendering)

Since Version 12.4.0 of this plugin, we support the new _jsdom_-based Angular Universal API for Server Side Rendering (SSR). Please note that SSR *only* makes sense in specific scenarios, e. g. for customer-facing apps that need SEO.  

To make use of SSR, you should enable SSR for **all** of your federation projects (e. g. the shell and the micro frontends).

#### Adding Angular Universal BEFORE adding Module Federation

If you start with a new project, you should add Angular Universal BEFORE adding Module Federation:

```
ng add @nguniversal/common --project yourProject
ng add @angular-architects/module-federation --project yourProject
```

Then, adjust the port in the generated ``server.ts``:

```typescript
const PORT = 5000;
```

After this, you can compile and run your application:

```
ng build yourProject && ng run yourProject:server
node dist/yourProject/server/main.js
```

#### Adding Angular Universal to an existing Module Federation project

If you already use ``@angular-architects/module-federation``, you can add Angular Universal this way:

1. Update ``@angular-architects/module-federation`` to the latest version (>= 12.4).

    ```
    npm i @angular-architects/module-federation@latest 
    ```

2. Now, we need to disable asynchronous bootstrapping temporarily. While it's needed for Module Federation, the schematics provided by Angular Universal assume that Angular is bootstrapped in an traditional (synchronous) way. After using these Schematics, we have to enable asynchronous bootstrapping again:

    ```
    ng g @angular-architects/module-federation:boot-async false --project yourProject

    ng add @nguniversal/common --project yourProject
    
    ng g @angular-architects/module-federation:boot-async true --project yourProject
    ```

3. As now we have both, Module Federation and Angular Universal, in place, we can integrate them with each other:

    ```
    ng g @angular-architects/module-federation:nguniversal --project yourProject
    ```

4. Adjust the used port in the generated ``server.ts`` file:

    ```typescript
    const PORT = 5000;
    ```

5. Now, you can compile and run your application:

    ```
    ng build yourProject && ng run yourProject:server
    node dist/yourProject/server/main.js
    ```

#### Example

Please find an [example](https://github.com/manfredsteyer/module-federation-plugin-example/tree/ssr
) here in the branch ``ssr``.

#### Trying it out

To try it out, you can checkout the ``main`` branch of our [example](https://github.com/manfredsteyer/module-federation-plugin-example). After installing the dependencies (``npm i``), you can repeat the steps for adding Angular Universal to an existing Module Federation project described above twice: Once for the _project shell and the port 5000_ and one more time for the _project mfe1 and port 3000_.

Please find a [brain dump](https://github.com/angular-architects/module-federation-plugin/blob/main/libs/mf/tutorial/braindump-ssr.md) for this [here](https://github.com/angular-architects/module-federation-plugin/blob/main/libs/mf/tutorial/braindump-ssr.md).

### Pitfalls when sharing libraries of a Monorepo

#### Warning: No required version specified

If you get the warning _No required version specified and unable to automatically determine one_, Module Federation needs some help with finding out the version of a shared library to use. Reasons are not fitting peer dependencies or using secondary entry points like ``@angular/common/http``.

To avoid this warning you can specify to used version by hand:

```json
shared: { 
    "@angular/common": { 
        singleton: true, 
        strictVersion: true, 
        requireVersion: '12.0.0  
    },
    [...]
},
```

You can also use our ``share`` helper that infers the version number from your ``package.json`` when setting ``requireVersion`` to ``'auto'``:

```json
shared: share({
    "@angular/common": { 
        singleton: true, 
        strictVersion: true,
        requireVersion: 'auto'  
    },
    [...]
})
```



#### Not exported Components

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)
```


## Angular Trainings, Workshops, and Consulting 👨‍🏫

- [Angular Trainings and Workshops](https://www.angulararchitects.io/en/angular-workshops/)
- [Angular Consulting](https://www.angulararchitects.io/en/consulting/)
