@angular-architects/module-federation vs @module-federation/nextjs-mf vs import-map-overrides vs single-spa
Architecting Micro-Frontends: Module Federation, Import Maps, and Framework Agnostic Runtimes
@angular-architects/module-federation@module-federation/nextjs-mfimport-map-overridessingle-spaSimilar Packages:

Architecting Micro-Frontends: Module Federation, Import Maps, and Framework Agnostic Runtimes

These four packages address the challenge of building micro-frontends, but they operate at different layers of the stack. @angular-architects/module-federation brings Webpack 5's Module Federation to Angular, allowing teams to split a monolith into independently deployable Angular apps. @module-federation/nextjs-mf adapts this same technology for Next.js, solving the complex server-side rendering (SSR) and shared dependency issues inherent in React server components. single-spa is a mature orchestrator that loads multiple frameworks (React, Vue, Angular) into a single page, managing their lifecycles without relying on build-time federation. Finally, import-map-overrides is a browser-native utility that works with import maps to dynamically swap module versions at runtime, often used to test or hot-fix federated modules without redeploying the shell.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
@angular-architects/module-federation0852195 kB4095 months agoMIT
@module-federation/nextjs-mf02,612400 kB667 days agoMIT
import-map-overrides0362230 kB15a year agoMIT
single-spa013,8792.12 MB642 years agoMIT

Micro-Frontend Architecture: Build-Time Federation vs. Runtime Orchestration

Building large-scale web applications often leads to a breaking point where a single codebase becomes too slow to build and too risky to deploy. The industry response is micro-frontends: splitting the app into smaller, independent pieces. The four packages we are comparingβ€”@angular-architects/module-federation, @module-federation/nextjs-mf, import-map-overrides, and single-spaβ€”solve this problem, but they do it in very different ways.

Some work at build time to share code bundles. Others work at runtime to load entire applications. Some are tied to specific frameworks, while others act as a neutral bridge between them. Let's dive into how they actually work in practice.

πŸ—οΈ Core Architecture: Build-Time Sharing vs. Runtime Loading

The biggest split in this group is between Module Federation (build-time code sharing) and Single-SPA (runtime application orchestration).

@angular-architects/module-federation and @module-federation/nextjs-mf rely on Webpack 5's Module Federation. This allows you to expose specific components from one app so another app can import them as if they were local files. The magic happens during the build. The apps know about each other's existence before they ever reach the browser.

// @angular-architects/module-federation: webpack.config.js
const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation');

module.exports = withModuleFederationPlugin({
  name: 'shell',
  exposes: {
    './Dashboard': './src/app/dashboard/dashboard.component.ts',
  },
  shared: {
    ...shareAll({ singleton: true, strictVersion: true })
  }
});
// @module-federation/nextjs-mf: next.config.js
const { withModuleFederation } = require('@module-federation/nextjs-mf');

module.exports = withModuleFederation({
  name: 'next_shell',
  exposes: {
    './Header': '../components/Header.tsx',
  },
  shared: {
    react: { singleton: true },
    'next': { singleton: true }
  }
});

single-spa takes a different approach. It doesn't care how your apps are built. It only cares that they can register themselves. It acts as a traffic cop in the browser, deciding which app to load based on the URL. The apps are completely independent builds.

// single-spa: root-config.js
import { registerApplication, start } from 'single-spa';

registerApplication({
  name: '@org/angular-app',
  app: () => System.import('@org/angular-app'),
  activeWhen: ['/angular-path'],
});

registerApplication({
  name: '@org/react-app',
  app: () => System.import('@org/react-app'),
  activeWhen: ['/react-path'],
});

start();

import-map-overrides sits on top of these systems. It uses the browser's native Import Maps feature to redirect module requests. If your shell asks for version-1.0.0/app.js, this tool can intercept that request and serve version-1.2.0/app.js instead, all without changing the source code.

<!-- import-map-overrides: index.html -->
<script type="importmap">
  {
    "imports": {
      "@org/shared-utils": "https://cdn.example.com/utils-v1.0.0.js"
    }
  }
</script>
<!-- The override UI allows changing the URL above dynamically -->

βš™οΈ Framework Specifics: Angular and Next.js Challenges

Module Federation is powerful, but it breaks easily with complex frameworks. Angular and Next.js have specific needs that standard Webpack configuration cannot handle alone.

@angular-architects/module-federation solves the Angular CLI problem. Angular's build tool is highly opinionated. It manages TypeScript compilation, Ahead-of-Time (AOT) compilation, and zone.js in a specific order. This package wraps the standard Webpack config to ensure Angular's build steps run correctly while still enabling federation.

// @angular-architects/module-federation: main.ts (Remote App)
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

// Bootstrap logic remains standard, but the build config handles the exposure
platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));

@module-federation/nextjs-mf tackles the Server-Side Rendering (SSR) puzzle. In Next.js, code runs on the server and the client. Standard federation often sends browser-only code to the server, causing crashes. This plugin ensures that shared dependencies like React are handled correctly across the server/client boundary and that Next.js routing still works.

// @module-federation/nextjs-mf: pages/index.tsx (Shell)
import dynamic from 'next/dynamic';

// Dynamically import a remote component with SSR support
const RemoteHeader = dynamic(
  () => import('remote_app/Header'),
  { ssr: true } // Ensures this renders on the server too
);

export default function Home() {
  return <div><RemoteHeader /></div>;
}

single-spa avoids these build-time headaches by keeping builds separate. However, you must manually configure each framework's lifecycle. For Angular, you need an adapter. For React, you need a different one.

// single-spa: angular lifecycle adapter
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import singleSpaAngular from 'single-spa-angular';

const angularLifecycles = singleSpaAngular({
  bootstrapFunction: singleSpaProps => {
    return platformBrowserDynamic().bootstrapModule(AppModule);
  },
  template: '<app-root />',
  domElementGetter: () => document.getElementById('angular-app'),
});

export const bootstrap = angularLifecycles.bootstrap;
export const mount = angularLifecycles.mount;
export const unmount = angularLifecycles.unmount;

πŸ”„ Runtime Flexibility: Hot Fixes and Local Development

One of the hardest parts of micro-frontends is testing a new version of a remote app without deploying the entire system. This is where import-map-overrides shines, regardless of whether you use Federation or Single-SPA.

import-map-overrides provides a UI (usually a floating button in dev mode) that lets you point a module to a localhost URL. This means you can run a remote app on your machine and have the main shell load it instantly.

// import-map-overrides: Usage in browser console or UI
// You don't write this code manually; the tool injects it.
// It modifies the import map dynamically:
{
  "imports": {
    "@org/checkout": "http://localhost:4200/remoteEntry.js" 
  }
}
// Now the shell loads your local checkout app instead of production

single-spa has a similar capability built-in via its own dev tools, but it relies on the SystemJS loader or native import maps. It is slightly more manual to set up for hot-swapping compared to the dedicated UI of import-map-overrides.

// single-spa: Manual override for local dev
// In your root config during development
if (process.env.LOCAL_DEV) {
  registerApplication({
    name: '@org/react-app',
    app: () => System.import('http://localhost:3000/js/app.js'),
    activeWhen: ['/app']
  });
}

Module Federation (@angular-architects and @module-federation/nextjs-mf) supports this via the remotes configuration, but it often requires restarting the dev server or complex WebSocket setups to detect changes in remote apps automatically. import-map-overrides is often added to these projects to make the DX smoother.

// @module-federation/nextjs-mf: Dynamic Remote Loading
// You can define remotes as promises to load them dynamically
remotes: {
  checkout: 'checkout@http://localhost:4200/remoteEntry.js',
}
// Changing this usually requires a rebuild unless paired with import maps

πŸ“¦ Dependency Management: The Singleton Problem

Sharing libraries like React or Angular across apps is critical to keep bundle sizes small. If every app loads its own copy of React, performance tanks.

@angular-architects/module-federation and @module-federation/nextjs-mf handle this via the shared config. They enforce "singleton" rules. If the shell has React 18, the remote cannot load React 17. It forces them to use the shell's version.

// Shared config in both Angular and Next.js federation plugins
shared: {
  react: {
    singleton: true, // Only one instance of React allowed
    requiredVersion: '^18.0.0',
    eager: true // Load immediately to avoid async issues
  }
}

single-spa does not enforce this automatically. Since apps are built separately, you must ensure your build tools (Webpack, Vite, Rollup) are configured to externalize shared dependencies. If you fail to do this, you will end up with multiple versions of frameworks running at once, which can cause memory leaks and state bugs.

// single-spa: Webpack config for a React child app
// You must manually ensure React is not bundled if you want to share it
externals: {
  react: 'React',
  'react-dom': 'ReactDOM'
},
// Or use webpack provide plugin to inject global instances

import-map-overrides helps debug these issues. If you suspect a version mismatch, you can use the override tool to force all apps to point to a specific CDN version of a library to see if the bug disappears.

<!-- import-map-overrides: Forcing a specific React version -->
<script type="importmap">
  {
    "imports": {
      "react": "https://esm.sh/react@18.2.0",
      "react-dom": "https://esm.sh/react-dom@18.2.0"
    }
  }
</script>

🌐 Real-World Scenarios

Scenario 1: Migrating a Monolithic Angular App

You have a massive Angular app. You want three teams to work independently.

  • βœ… Best choice: @angular-architects/module-federation
  • Why? It keeps your existing Angular CLI workflow. You just split the repo into a shell and remotes. No need to rewrite routing or learn a new runtime.
// Shell app routing
const routes: Routes = [
  { path: 'dashboard', loadChildren: () => import('remote/DashboardModule') }
];

Scenario 2: Building a Polyglot Portal (React + Vue + Angular)

Your company has legacy AngularJS, a new React dashboard, and a marketing site in Vue.

  • βœ… Best choice: single-spa
  • Why? Module Federation struggles with such diverse build chains. Single-SPA treats them all as black boxes that just need to mount and unmount.
// single-spa registration
registerApplication('legacy', () => System.import('legacy-app'), () => pathPrefix('/legacy'));
registerApplication('react', () => System.import('react-app'), () => pathPrefix('/react'));

Scenario 3: Next.js E-commerce with Independent Product Teams

You need SSR for SEO, but the Checkout team deploys daily, while the Home page team deploys weekly.

  • βœ… Best choice: @module-federation/nextjs-mf
  • Why? It is the only option that safely shares Next.js server components and handles the complex hydration requirements of React SSR across micro-frontends.
// Next.js Shell
const RemoteCheckout = dynamic(() => import('checkout/CheckoutPage'), { ssr: true });

Scenario 4: Testing a Critical Fix in Production

A bug is found in the "Checkout" micro-frontend. You need to verify a fix immediately without waiting for the full deployment pipeline.

  • βœ… Best choice: import-map-overrides
  • Why? You deploy the fix to a staging URL. You (or a QA tester) open the override UI, point the Checkout module to the staging URL, and verify the fix in the live production shell.
// No code change needed. Just use the browser UI provided by the library
// to map 'checkout-v1.2' -> 'https://staging.example.com/checkout-v1.3.js'

πŸ“Š Summary Table

Feature@angular-architects/mf@module-federation/nextjs-mfsingle-spaimport-map-overrides
Primary GoalAngular Micro-frontendsNext.js Micro-frontendsFramework Agnostic OrchestrationRuntime Module Swapping
Build ToolWebpack (Angular CLI)Webpack (Next.js)Any (Webpack, Vite, etc.)None (Browser Native)
SSR SupportLimited (Client-side focus)βœ… Full SSR/SSG Support❌ Client-side only (mostly)N/A (Runtime only)
FrameworkAngular OnlyReact/Next.js OnlyAny (React, Vue, Angular, etc.)Any
IntegrationBuild-time (Tight coupling)Build-time (Tight coupling)Runtime (Loose coupling)Runtime Utility
Learning CurveMedium (Angular specific)High (Next.js internals)High (Lifecycle management)Low

πŸ’‘ Final Recommendation

The choice here is not about which tool is "best," but about where your constraints lie.

If you are all-in on Angular, @angular-architects/module-federation is the clear winner. It respects the Angular way of doing things and minimizes friction for your team.

If you are building on Next.js, do not try to force standard Webpack federation. Use @module-federation/nextjs-mf. The complexity of syncing server and client bundles is too high to solve manually.

If you are running a mixed environment (the "polyglot" reality of many enterprises), single-spa is your safest bet. It isolates failures and allows teams to use whatever tool they prefer, at the cost of more boilerplate code for mounting and unmounting.

Finally, treat import-map-overrides as a mandatory utility for any of these setups. The ability to override modules in the browser is the single biggest productivity booster for debugging and testing micro-frontends. It turns a painful deployment cycle into a instant refresh.

The Big Picture: Module Federation (@angular-architects and @module-federation/nextjs-mf) gives you tighter integration and better performance by sharing code at build time. single-spa gives you maximum freedom and isolation by loading apps at runtime. import-map-overrides makes both approaches manageable for humans.

How to Choose: @angular-architects/module-federation vs @module-federation/nextjs-mf vs import-map-overrides vs single-spa

  • @angular-architects/module-federation:

    Choose this if your ecosystem is primarily Angular and you need to split a large application into independently deployable units. It is the official community standard for implementing Module Federation in Angular, handling the specific compilation quirks of the Angular CLI. Avoid it if you are building a polyglot system with many non-Angular apps, as it is specialized for the Angular build pipeline.

  • @module-federation/nextjs-mf:

    Select this package when you are building a micro-frontend architecture on top of Next.js. Standard Module Federation often breaks Next.js features like SSR and Image Optimization; this plugin bridges that gap. It is essential if you need to share server-rendered components between Next.js applications while maintaining performance and SEO benefits.

  • import-map-overrides:

    Use this tool as a companion to your build system (like Module Federation) for local development and production hot-fixes. It allows developers to override specific module versions in the browser without changing code or rebuilding the shell application. It is not a standalone architecture but a critical utility for managing the runtime behavior of federated modules.

  • single-spa:

    Opt for single-spa if you need to integrate multiple different frameworks (e.g., legacy AngularJS, modern React, and Svelte) into one cohesive application. It is ideal for gradual migrations where build-time integration is impossible. Choose this over Module Federation if your teams cannot agree on a single build tool or if you need to load applications from completely different domains without shared build configurations.

README for @angular-architects/module-federation

@angular-architects/module-federation

Seamlessly using Webpack Module Federation with the Angular CLI.

Thanks

We are standing on the shoulders of giants. Big thanks to the following people who helped to make this project possible:

Prequisites

  • Angular CLI 12 or higher

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.

Supporting Several Technologies

βœ… Supports webpack, rsbuild (experimental, nextgen), esbuild

βœ… Supports Module Federation and Native Federation

βœ… Supports the Angular CLI and Nx

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

Which Version to use?

We are going to provide at least one major per Angular major to keep track with the Angular ecosystem and its innovations:

  • Angular 12: @angular-architects/module-federation: ^12.0.0
  • Angular 13: @angular-architects/module-federation: ~14.2.0
  • Angular 14: @angular-architects/module-federation: ^14.3.0
  • Angular 15: @angular-architects/module-federation: ^15.0.0
  • Angular 16: @angular-architects/module-federation: ^16.0.0
  • Angular 17: @angular-architects/module-federation: ^17.0.0
  • Angular 18: @angular-architects/module-federation: ^18.0.0
  • Angular 19: @angular-architects/module-federation: ^19.0.0
  • Angular 20: @angular-architects/module-federation: ^20.0.0
  • Angular 21: @angular-architects/module-federation: ^21.2.0

Beginning with Angular 13, we had to add some changes to adjust to the Angular CLI. Please see the next section for this.

webpack, rsbuild, and esbuild

Since version 19, the plugin's ng-add schematic asks whether you want to use the traditional Webpack-based builder, the (currently experimental, fast, next-generation) rsbuild builder, or esbuild (fast, Angular CLI's new default).

The first two are supported via Module Federation. For the rsbuild integration, we are using Colum Ferry's awesome community project, @ng-rsbuild/plugin-angular. So, all credit for making rsbuild work with Angular goes to Colum.

The esbuild integration is technically a wrapper around the Angular CLI's new default builder, the ApplicationBuilder. It is powered by Native Federation, our bundler-agnostic implementation based on web standards like ECMAScript modules and Import Maps.

Update

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 15 for Angular 15, version 14 for Angular 14, version 13 for Angular 13, etc.)

Upgrade from Angular 12 or lower

Beginning with Angular 13, the CLI generates EcmaScript modules instead of script files. This affects how we work with Module Federation a bit.

Please find information on migrating here:

Migration Guide for Angular 13+

If you start from the scratch, ng add will take care of these settings.

Usage πŸ› οΈ

Angular CLI

  1. ng add @angular-architects/module-federation
  2. Adjust the generated webpack.config.js file
  3. Repeat this for further projects in your workspace (if needed)

Nx

  1. npm install --save-dev @angular-architects/module-federation
  2. nx g @angular-architects/module-federation:init
  3. Adjust the generated webpack.config.js file
  4. Repeat this for further projects in your workspace (if needed)

πŸ†•πŸ”₯ Version 14+: Use the --type switch to get the new streamlined configuration

With version 14, we've introduced a --type switch for ng add and the init schematic. Set it to one of the following values to get a more streamlined configuration file:

  • host
  • dynamic-host
  • remote

A dynamic host reads the micro frontend's URLs from a configuration file at runtime.

Getting Started πŸ§ͺ

Please find here a tutorial that shows how to use this plugin.

Microfrontend Loaded into Shell

>> Start Tutorial

Documentation πŸ“°

Please have a look at this article series about Module Federation.

Example πŸ“½οΈ

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.

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:

import { loadRemoteModule } from '@angular-architects/module-federation';

[...]
const routes: Routes = [
    [...]
    {
        path: 'flights',
        loadChildren: () =>
            loadRemoteModule({
                type: 'module',
                remoteEntry: 'http://localhost:3000/remoteEntry.js',
                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({
    type: 'module',
    remoteEntry: 'http://localhost:3000/remoteEntry.js',
  }),
])
  .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, you set to mention the remoteEntry property anyway, as it also acts as an internal identifier for the remote:

import { loadRemoteModule } from '@angular-architects/module-federation';

[...]
const routes: Routes = [
    [...]
    {
        path: 'flights',
        loadChildren: () =>
            loadRemoteModule({
                type: 'module',
                remoteEntry: 'http://localhost:3000/remoteEntry.js',
                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:

"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.

New streamlined configuration in version 14+

Beginning with version 14, we use a more steamlined configuration, when using the above mentioned --type switch with one of the following options: remote, host, dynamic-host.

This new configuration automatically shares all local libararies. Hence, you don't need to do a thing.

However, if you want to control, which local libraries to share, you can use the the sharedMappings array:

module.exports = withModuleFederationPlugin({
  shared: {
    ...shareAll({
      singleton: true,
      strictVersion: true,
      requiredVersion: 'auto',
    }),
  },

  sharedMappings: ['shared-lib'],
});

Please don't forget that sharing in Module Federation is always an opt-in: You need to add this setting to each micro frontend that should share it.

Legacy-Syntax and version 12-13

In previous versions, you registered the 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 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(),
],

Share Helper

The helper function share adds some additional options for the shared dependencies:

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:

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.

Since version 14.3, includeSecondaries is true by default.

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:

shared: share({
    "@angular/common": {
        singleton: true,
        strictVersion: true,
        requiredVersion: 'auto',
        includeSecondaries: {
            skip: ['@angular/common/http/testing']
        }
    },
    [...]
})

shareAll

The shareAll helper shares all your dependencies defined in your package.json. The package.json is look up as described above:

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.

Eager and Pinned

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 alothough 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 used by the Angular CLI a bit to avoid code duplication in the generated bundles. The new withModuleFederationPlugin helper that has been introduced with this plugin's version 14 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.

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:

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:

    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 here in the branch ssr.

Trying it out

To try it out, you can checkout the main branch of our 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 for this here.

Pitfalls when sharing libraries of a Monorepo

Schematics don't work anymore (e. g. ng add @angular/material or @angular/pwa)

In order to make module federation work, we need to bootstrap the app asynchronously. Hence, we need to move the bootstrap logic into a new bootstrap.ts and import it via a dynamic import in the main.ts. This is a typical pattern when using Module Federation. The dynamic import makes Module Federation to load the shared libs.

However, some schematics (e. g. ng add @angular/material or @angular/pwa) assume that bootstrapping directly happens in main.ts. For this reason, there is a schematic, that helps you turning async bootstrapping on and off:

ng g @angular-architects/module-federation:boot-async false --project yourProject

ng add your-libraries-of-chioce --project yourProject

ng g @angular-architects/module-federation:boot-async true --project yourProject

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:

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':

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 πŸ‘¨β€πŸ«