The goal of this small guide is to show the major differences between the previous Native Federation major (v3, Angular ≤ 21) and the new ESM-based major (Angular 22+). It expects a (monorepo) setup that contains 1 or multiple Angular micro frontends.
Tip
Prefer to let the tooling do the work? Running ng update applies most of these changes automatically — its update22 migration converts your federation config to ESM and updates the builder references for you:
ng update @angular-architects/native-federationAlready updated to the newest version? Just run the schematics!
ng update @angular-architects/native-federation --migrate-only update22The migration involves changing 4 files:
📁 /
├── 📄 package.json // Enabling ESM
├── 📄 angular.json // Updating the builder options
└── 📁 projects/
└── 📁 <your-project>/
├── 📄 federation.config.mjs // Renamed to federation.config.mjs & switch from commonJS to ESM
└── 📁 src/
└── 📄 main.ts // optionally: switching to the orchestrator
Just to be sure, delete these folders to avoid corrupted caches:
📁 /
├── 📁 .angular/ // Angular cache
├── 📁 dist/ // Previously bundled artifacts
└── 📁 node_modules/
└── 📁 .cache/ // Native federation cache
The first step is to update the package.json to install the new packages:
{
"name": "mfe-test",
"version": "1.2.3",
"scripts": {
"ng": "ng"
},
"private": true,
"dependencies": {
// [...] Your dependencies
},
"devDependencies": {
"@angular-architects/native-federation": "~22.0.0",
"@softarc/native-federation": "~4.1.0",
"@softarc/native-federation-orchestrator": "^4.2.2"
}
}The federation.config.js contains all native-federation related configuration. The update22 migration renames it to federation.config.mjs and switches it from CommonJS to ESM for consistency. The builder still falls back to federation.config.js if no .mjs file is present.
Before:
// Notice the require? we're going to change that for import!
const { withNativeFederation, share, shareAll } = require('@angular-architects/native-federation/config');
module.exports = withNativeFederation({
name: 'mfe1',
exposes: {
'./Component': './projects/mfe1/src/bootstrap.ts',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
// OLD: This example is only for setups that currently have a share after the shareAll.
...share({ "@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' }});
},
skip: [
'rxjs/ajax',
'rxjs/fetch',
'rxjs/testing',
'rxjs/webSocket',
// Add further packages you don't need at runtime
]
// Please read our FAQ about sharing libs:
// https://shorturl.at/jmzH0
});After:
// Our well-known ESM importing types
import { withNativeFederation, shareAll } from '@angular-architects/native-federation/config';
// change this line to the default export.
export default withNativeFederation({
name: 'team/mfe1',
exposes: {
'./Component': './projects/mfe1/src/bootstrap.ts',
},
shared: {
// This still works! But how about overrides?
// ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
// Here's an alternative, you can merge the overrides _into_ the shareAll!
...shareAll(
{ singleton: true, strictVersion: true, requiredVersion: 'auto' },
{
overrides: {
'@angular/core': {
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
includeSecondaries: { keepAll: true },
},
},
}
),
},
skip: ['rxjs/ajax', 'rxjs/fetch', 'rxjs/testing', 'rxjs/webSocket'],
features: {
ignoreUnusedDeps: true, // Now enabled by default
denseChunking: true, // Opt-in: groups chunks in remoteEntry.json for smaller file size
versionMapping: true, // Now enabled by default
},
});In the new version we're moving to an opt-in setup where the user (you) can customize and choose whatever features you prefer! All these options will be defined in the angular.json:
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"mfe1": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "projects/mfe1",
"sourceRoot": "projects/mfe1/src",
"prefix": "app",
"architect": {
"serve": {
"builder": "@angular-architects/native-federation:build",
"options": {
"target": "mfe1:serve-original:development",
"cacheExternalArtifacts": true, // Cache and re-use external bundled artifacts that don't change (e.g. RxJs) across builds
"rebuildDelay": 500, // Allows for a grace period between builds when you develop; within this period it can cancel previous builds to save time (500/1000 is good)
"integrity": true, // (optional) Adds Subresource Integration
"dev": true,
"port": 0
}
}
}
}
}
}Note: Code-splitting (
chunks) and dense chunking (denseChunking) are now configured infederation.config.mjsinstead of the angular.json builder options. See the README for details.
And that's it! Your micro frontend is migrated to the new major! We do have some optional improvements that can be nice:
Here's the projects/<your-project>/src/main.ts you've been used to for the last couple of years (before v4). It now automatically bootstraps the new orchestrator:
import { initFederation } from '@angular-architects/native-federation';
initFederation()
.catch(err => console.error(err))
.then(_ => import('./bootstrap'))
.catch(err => console.error(err));However, some projects still use the "legacy runtime" that did the job. But it lacks some modern features like dependency sharing based on a range, shareScopes, in-browser caching etc etc.
import { initFederation } from '@softarc/native-federation-runtime'; // Default native-federation runtime
initFederation()
.catch(err => console.error(err))
.then(_ => import('./bootstrap'))
.catch(err => console.error(err));For optimal performance, from now on we recommend using the orchestrator directly:
import { initFederation, NativeFederationResult } from '@softarc/native-federation-orchestrator';
const manifest = {
mfe1: 'http://localhost:4201/remoteEntry.json',
};
initFederation(manifest)
.catch(err => console.error(err))
.then(_ => import('./bootstrap'))
.catch(err => console.error(err));Not a lot of changes right? Sure, now you need to explicitly define the location of the manifest (or the object), but for the rest it's basically the same!
Note: Since v4, the default
initFederationlibrary is the orchestrator, not the previously mentioned runtime.
Now, the big difference is that the new orchestrator is a lot more customizable:
import { initFederation, NativeFederationResult } from '@softarc/native-federation-orchestrator';
import {
useShimImportMap,
consoleLogger,
globalThisStorageEntry,
} from '@softarc/native-federation-orchestrator/options';
const manifest = {
mfe1: 'http://localhost:4201/remoteEntry.json',
};
initFederation(manifest, {
...useShimImportMap({ shimMode: true }),
logger: consoleLogger,
storage: globalThisStorageEntry,
hostRemoteEntry: './remoteEntry.json',
logLevel: 'debug',
})
.catch(err => console.error(err))
.then(_ => import('./bootstrap'))
.catch(err => console.error(err));You see that? Now you can choose which logger you want, and if you want to use the "shimImportMap" instead of the browser-native importmap (spoiler alert: 90% chance you do).
There's a nice list of all the options you can choose from in the docs: https://github.com/native-federation/orchestrator/blob/main/docs/config.md
The biggest change is that now, the loadRemoteModule is provided by initFederation. So it's not a global export anymore. That does mean that you now need to pass it around your micro frontends:
(host) main.ts
import { initFederation, NativeFederationResult } from '@softarc/native-federation-orchestrator';
const manifest = {
mfe1: 'http://localhost:4201/remoteEntry.json',
};
initFederation(manifest)
.then(({ loadRemoteModule }: NativeFederationResult) => {
return import('./bootstrap').then((m: any) => m.bootstrap(loadRemoteModule));
})
.catch(err => console.error(err));Now you can set up a bootstrap.ts that exposes a method "bootstrap" that accepts this function.
(mfe1) bootstrap.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
import { LoadRemoteModule } from '@softarc/native-federation-orchestrator';
export const bootstrap = (loadRemoteModule: LoadRemoteModule) =>
bootstrapApplication(AppComponent, appConfig(loadRemoteModule)).catch(err => console.error(err));And ofcourse the app.config.ts:
import {
ApplicationConfig,
InjectionToken,
provideZonelessChangeDetection,
} from '@angular/core';
import { provideRouter, Routes } from '@angular/router';
import { LoadRemoteModule, NativeFederationResult } from '@softarc/native-federation-orchestrator';
export const MODULE_LOADER = new InjectionToken<LoadRemoteModule>(
'loader',
);
const routes = (loadRemoteModule: LoadRemoteModule): Routes => [
{
path: 'mfe3',
loadComponent: () =>
loadRemoteModule('mfe3', './Component')
.then((m:any) => m.AppComponent),
}
];
export const appConfig = (loadRemoteModule: LoadRemoteModule): ApplicationConfig => ({
providers: [
{ provide: MODULE_LOADER, useValue: loadRemoteModule },
provideZonelessChangeDetection(),
provideRouter(routes(loadRemoteModule)),
],
});While this does create a bit more boilerplate and complexity, the nice benefit is a controlled flow in which the loadRemoteModule is only available after federation is initialized.
We've been scratching the surface here, but these are the essentials to migrate your codebase to the new major!
Feel free to open an issue if you come across any problems or if we've missed anything.