Runtime
On v3 the adapter re-exported initFederation and
loadRemoteModule from the classic
@softarc/native-federation-runtime. On v4 the adapter
ships its own initFederation (and a deprecated top-level
loadRemoteModule) from
@angular-architects/native-federation that bridge to
the orchestrator runtime by
default. The generated main.ts goes one step further and
calls the orchestrator directly. This page covers how these integrate
with an Angular bootstrap.
The Bootstrap Split
Native Federation must wire the import map before Angular
evaluates any module that depends on a shared external. The schematic
enforces this by splitting main.ts in two:
// projects/<project>/src/main.ts
import { initFederation } from '@angular-architects/native-federation';
initFederation('/assets/federation.manifest.json')
.catch(err => console.error(err))
.then(_ => import('./bootstrap'))
.catch(err => console.error(err));
// projects/<project>/src/bootstrap.ts
// ← whatever your original main.ts contained, e.g.
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig)
.catch(err => console.error(err));
The dynamic import('./bootstrap') is mandatory: it forces
the bundler to put your Angular code in a separate chunk that's only
loaded once the import map is live.
initFederation
On v4 the adapter's initFederation wraps the orchestrator
with sensible defaults (shim import map, console logger,
globalThis storage, hostRemoteEntry:
'./remoteEntry.json', logLevel: 'debug') and
resolves to a NativeFederationResult:
initFederation(
remotesOrManifestUrl?: Record<string, string> | string,
options?: { cacheTag?: string; logging?: LogType },
): Promise<NativeFederationResult>
-
Host (dynamic). Pass the manifest URL:
initFederation('/assets/federation.manifest.json'). -
Host (static). Pass the remote map inline:
initFederation({ mfe1: 'http://localhost:4201/remoteEntry.json' }). -
Remote. Pass a self-map:
initFederation({ mfe1: './remoteEntry.json' }). This lets the remote's runtime register its own shared modules so the host can match versions.
The resolved NativeFederationResult carries the
loadRemoteModule you should use (see below). The
init schematic emits the right call for the project type
you chose — and on v4 it imports initFederation straight
from @softarc/native-federation-orchestrator. See
Schematics → init.
loadRemoteModule
loadRemoteModule(remoteName, exposedKey): Promise<unknown>
Once initFederation resolves, you can lazy-load any
exposed module from any registered remote. In an Angular shell this is
normal lazy-loading:
// projects/shell/src/app/app.routes.ts
import { Routes } from '@angular/router';
import { loadRemoteModule } from '@angular-architects/native-federation';
export const APP_ROUTES: Routes = [
{
path: 'flights',
loadComponent: () =>
loadRemoteModule('mfe1', './Component').then(m => m.AppComponent),
},
{
path: 'orders',
loadChildren: () =>
loadRemoteModule('mfe2', './Routes').then(m => m.ORDERS_ROUTES),
},
];
remoteName matches the name in the remote's
federation.config.mjs (or .js on v3 / legacy
projects) and the key in the host's manifest. exposedKey
matches the key under
exposes. The promise resolves to the module's exports —
whatever you'd get from a regular dynamic import().
Deprecated on v4. This top-level
loadRemoteModule import is kept for backwards
compatibility but is deprecated — it resolves against a
module-scoped instance from the most recent
initFederation call, which is brittle in tests and
multi-host setups. Prefer the loadRemoteModule returned
by the initFederation promise and thread it through
Angular's DI (see below).
The Federation Manifest
For dynamic hosts, the manifest is just a JSON object mapping remote
name → remoteEntry.json URL:
{
"mfe1": "http://localhost:4201/remoteEntry.json",
"mfe2": "https://cdn.example.com/orders/remoteEntry.json"
}
Swap it per environment by deploying a different
federation.manifest.json alongside the shell — no rebuild
required. The schematic places it under public/ if the
project has a public folder, otherwise under src/assets/.
Manifest URLs may be absolute (production CDN) or relative (local
dev or same-origin deploys). For Angular SSR the same manifest is
consumed server-side by
@softarc/native-federation-node (or, on v4, by the
orchestrator's /node entry); see SSR & Hydration.
The Orchestrator Runtime
The orchestrator is the default runtime on v4 — it
adds range-based version selection, share scopes, in-browser caching,
configurable storage, and pluggable loggers over the legacy
@softarc/native-federation-runtime (the v3 default). The
adapter's own initFederation already bridges to it, and
the init schematic generates a bootstrap that calls the
orchestrator directly.
A freshly scaffolded (or hand-written) orchestrator bootstrap looks like this:
// projects/shell/src/main.ts
import { initFederation } from '@softarc/native-federation-orchestrator';
import {
useShimImportMap,
consoleLogger,
globalThisStorageEntry,
} from '@softarc/native-federation-orchestrator/options';
initFederation('/assets/federation.manifest.json', {
...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));
The biggest behavioural change is that
loadRemoteModule is no longer a global export — it's
returned from the resolved initFederation promise. That
nudges your bootstrap into a controlled flow:
// projects/shell/src/main.ts
import { initFederation, NativeFederationResult } from '@softarc/native-federation-orchestrator';
initFederation('/assets/federation.manifest.json')
.then(({ loadRemoteModule }: NativeFederationResult) =>
import('./bootstrap').then((m: any) => m.bootstrap(loadRemoteModule)))
.catch(err => console.error(err));
// projects/shell/src/bootstrap.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
import { LoadRemoteModule } from '@softarc/native-federation-orchestrator';
export const bootstrap = (loadRemoteModule: LoadRemoteModule) =>
bootstrapApplication(AppComponent, appConfig(loadRemoteModule))
.catch(err => console.error(err));
And then pass the loader through Angular's DI so routes can use it:
// projects/shell/src/app/app.config.ts
import { ApplicationConfig, InjectionToken, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter, Routes } from '@angular/router';
import { LoadRemoteModule } 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)),
],
});
Slightly more boilerplate, but the loader is guaranteed to exist by the time anything tries to use it. The full list of orchestrator options lives in the runtime docs.
Related
- Runtime overview — the orchestrator's full feature set.
- SSR & Hydration — initialising federation on the Node side.
- Migration to v4 — switching from the legacy runtime to the orchestrator.