Skip to content

Truly dynamic modules at runtime with Module Federation

License

Notifications You must be signed in to change notification settings

LoaderB0T/ng-dynamic-mf

Repository files navigation

npm Sonar Quality Gate bundle size

ng-dynamic-mf

Truly dynamic modules at runtime with Module / Native Federation!

Motivation πŸ’₯

ng-dynamic-mf works with both module federation and native federation. In the following, I will refer to both as "module federation" for simplicity.

Module Federation builds upon the concepts of Module Federation without the need for webpack. For Angular, there is the excellent Module Federation Plugin. ng-dynamic-mf now leverages Module Federation, bringing modularity in Angular to the next level by providing methods to easily load modules at runtime.

Features πŸ”₯

βœ… Dynamic loading of modules at runtime

βœ… The host/shell application does not need to be aware of the modules it loads

βœ… Modules can be loaded from any location (e.g., from a CDN) that can be unknown to the developer

βœ… Works with Webpack & ESBuild

βœ… Modules can be deployed on demand without modifying the host/shell application

βœ… Assets are resolved correctly at runtime (both for running the app standalone and in a shell) with a provided pipe.

βœ… Support for custom routing logic (Register module routes as a child of any other module)

βœ… Support for global styles (Load global styles from any module)

βœ… Use with Nx with nx-dynamic-mf

This project aims to improve the general experience of developers using Module Federation in Angular. Even if you don't need truly dynamic modules, you can still benefit from this project.

Built With πŸ”§

Getting Started πŸš€

If you want to check out a working example, take a look at my portfolio project awdware.

If you are using Nx, you should also check out nx-dynamic-mf

Module Federation Plugin

This project uses the Module Federation Plugin to load modules at runtime. Please follow the instructions in the README.md file of the plugin to get started.

Bootstrapping the host/shell application

ng-dynamic-mf provides an initializeApp method that can be used to bootstrap the shell app.

Example:

// This example uses native federation!

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { initializeApp } from 'ng-dynamic-mf/nf';
import { environment } from 'ng-dynamic-mf/environment';
// Due to an issue in the Module Federation Plugin, we need to import loadRemoteModule from the plugin directly and pass it to ng-dynamic-mf. This is only required for the native federation use case.
import { loadRemoteModule } from '@angular-architects/native-federation';

initializeApp({
  type: 'host', // Specify that this is the host/shell application
  loadRemoteModule,
})
.then(() => {
  if (environment.production) { // This is the environment variable provided by ng-dynamic-mf
    enableProdMode();
  }
  return import('./app/app.module');
})
.then(x => x.AppModule)
.then(x => {
  platformBrowserDynamic()
    .bootstrapModule(x)
    .catch(err => console.error(err));
});

Note: ng-dynamic-mf does also export the environment variable. This replaces Angulars environment system. You should remove the environment.*.ts files generated by Angular and switch to the runtime version of ng-dynamic-mf. more info here.

ng-dynamic-mf expects a modules.json file to be present in the src folder of the host/shell application. Examples of modules.jsons:

{
  "$schema": "https://raw.githubusercontent.com/LoaderB0T/ng-dynamic-mf/main/schema.json",
  "modules": [
    {
      "url": "http://localhost:4201/",
      "name": "home",
      "ngModuleName": "RemoteEntryModule"
    },
    {
      "url": "http://localhost:4202/",
      "name": "projects",
      "ngModuleName": "RemoteEntryModule"
    }
  ]
}

or

{
  "$schema": "https://raw.githubusercontent.com/LoaderB0T/ng-dynamic-mf/main/schema.json",
  "modules": [
    {
      "url": "https://example.com/assets/modules/home/",
      "name": "home",
      "ngModuleName": "RemoteEntryModule"
    },
    {
      "url": "https://example.com/assets/modules/projects/",
      "name": "projects",
      "ngModuleName": "RemoteEntryModule"
    }
  ]
}

(Mind that the urls must be absolute, not relative and end with a slash).

You also need to provide an environment.json file in the src folder of the host/shell application. Example of environment.jsons:

{
  "apiUrl": "http://localhost:5555",
  "production": false
}

or

{
  "apiUrl": "https://api.example.com/",
  "production": true
}

bootstrapping a module

Bootstrapping a module app is similar to a regular Angular app. The only difference is importing the environment from the ng-dynamic-mf module instead of the usual path.

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { environment } from 'ng-dynamic-mf/environment';

import { AppModule } from './app/app.module';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch(err => console.error(err));

Assets resolution

When using static assets such as images in a module, you need to teach your app how to resolve the asset. This is due to the fact that the assets are loaded at runtime and not at build time and the shell app is not aware of the assets that a module provides.

Example of how to resolve assets in a template of the home module:

<img [alt]="contact.name" [src]="'assets/img/logo.svg' | resourceMap:'home'" />

When just serving the app this resolves to ${location.origin}/assets/img/logo.svg. When loaded by the host this will resovle to https://example.com/assets/modules/home/assets/img/logo.svg (If this is the configured URL of the module).

You can also do this mapping in code:

import { resourceMapper } from 'ng-dynamic-mf';

const path = 'assets/img/logo.svg';
const url = resourceMapper('home', path);

Handling translations (ngx-translate)

When using translations in a module, they need to be loaded at runtime when the module is loaded.

Example of how to register german and english translations in the home module:

import { DynamicTranslationService } from 'ng-dynamic-mf/translate';

export class HomeModule {
  constructor(dynamicTranslationService: DynamicTranslationService) {
    // Specify that assets/i18n/[..].json is the path to a translation file for each language. Other syntaxes are also supported (See inline documentation).
    // (Resolved with the resource mapper to the home module's assets folder) -> Works for standalone and shell use cases
    dynamicTranslationService.registerTranslationSet('home-i18n', {
      de: {
        moduleName: 'home',
        path: 'assets/i18n/home.de.json',
      },
      en: {
        moduleName: 'home',
        path: 'assets/i18n/home.en.json',
      }}
    );
  }
}

Routing

When a module provides routes that should be nested inside of another route of the shell, they need to be configured accordingly. You dont need to do anything here, if the routes of your modules should all be resolved relative to the root of the shell app (no nesting). Example: Route config of the shell:

import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: '',
    children: [
      {
        path: '#module-entry-point#',
        redirectTo: '/'
      }
    ]
  },
  {
    path: '**',
    redirectTo: ''
  }
];

We want all modules to be registered where #module-entry-point# is located.

import { RouterEntryService } from 'ng-dynamic-mf';

const routes: Routes = [
  {
    path: 'home',
    loadChildren: () => import('./home.module').then(m => m.HomeModule),
    data: { activePage: 'home' }
  },
  {
    path: '',
    pathMatch: 'full',
    redirectTo: '/home'
  }
];

export class HomeEntryModule {
  constructor(routerEntryService: RouterEntryService) {
    routerEntryService.registerRoutes(routes);
    // - OR - You can also register the routes under a specific entry point.
    // This defaults to '#module-entry-point#'
    routerEntryService.registerRoutes(routes, 'details');
  }
}

Environment variables

ng-dynamic-mf provides a way to specify variables in an environment.json file (after the app has been built). This is different from the environment.ts file that is used by Angular CLI, as this will be baked into the app during the build. The environment.json file can be edited at any time and the change will be reflected in the app after a reload.

You do not have to use the environment.json file, you can also keep using the environment.ts file. (However you will need to create an empty environment.json file to prevent errors).

Global Styles

ng-dynamic-mf provides a way to load global styles into the app. This is useful for loading styles that are defined by a module but should be applied globally (no view encapsulation). To do so, you need to add a styles file (eg global.scss) to your module that defines the styles. Afterwards you need to adjust your modules angular.json file to include the styles file and bundle it separately with the name global-styles:

{
  [...],
  "targets": {
    "build": {
      [...],
      "options": {
        [...],
        "styles": [
          "apps/example-project/src/styles.scss",
          {
            "input": "apps/example-project/src/global.scss",
            "bundleName": "global-styles"
          }
        ],
        [...]
      }
    }
  }
}

Note: The bundle name has to be global-styles for ng-dynamic-mf to recognize it.

Afterwards you can tell ng-dynamic-mf to load the styles by adding "hasGlobalStyles": true to this modules config in the modules.json file.

Note: Don't forget to disable output hashing in the angular.json file or make sure to rename the file after it gets created. (File name has to be global-styles.css in the root of the module, next to the entry point file). Alternatively you can use nx-dynamic-mf which will adjust the module.json file automatically to the hashed file name.

Contributing πŸ§‘πŸ»β€πŸ’»

Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.

If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
  3. Commit your Changes (git commit -m 'Add some AmazingFeature')
  4. Push to the Branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

License πŸ”‘

Distributed under the MIT License. See LICENSE.txt for more information.

Contact πŸ“§

Janik Schumacher - @LoaderB0T - linkedin

Project Link: https://github.com/LoaderB0T/ng-dynamic-module-federation

About

Truly dynamic modules at runtime with Module Federation

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published