Runtime

Applies to

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>

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