Runtime
The runtime side of the v3 Angular adapter — a straight re-export of the classic runtime: initFederation, loadRemoteModule, lazy routes and lazy remote registration.
On v3, @angular-architects/native-federation adds nothing of its own to the runtime. Its entry point is one line:
export * from "@softarc/native-federation-runtime";
So every runtime symbol you import from the adapter — initFederation, loadRemoteModule, fetchAndRegisterRemote, the ImportMap types — is the classic runtime's, unchanged. This page documents that surface as Angular hosts use it; the package's own pages are under Runtime.
On v4 the adapter bridges to the orchestrator instead, and its API is deliberately different — see Runtime (v4) and Migration to v4. A v3 host can also opt into the orchestrator on its own; see Orchestrator.
On this page
- The bootstrap split
- initFederation
- loadRemoteModule
- Lazy remote registration
- The federation manifest
- What v4 changes
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 — the same split v4 uses:
// 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
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.
Your index.html also needs es-module-shims on the page — the runtime injects its map as <script type="importmap-shim">, which browsers ignore on their own. The schematic adds it for you.
initFederation
function initFederation(
remotesOrManifestUrl?: Record<string, string> | string,
options?: InitFederationOptions,
): Promise<ImportMap>;
interface InitFederationOptions {
cacheTag?: string;
deployUrl?: string;
}
Note what it resolves to: the merged ImportMap that was written to the DOM — not a loader object. On v3 loadRemoteModule is a module-level import you call independently; there is nothing to thread through DI. That is the single biggest difference from v4.
- 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 an empty map:
initFederation({}). This registers the remote's own shared dependencies at the root of the import map, so its code resolves@angular/coreand friends to the bundled copies when the remote is served directly.
The remotes-map key is the name you will pass to loadRemoteModule. It does not have to match the name field inside the remote's remoteEntry.json — the key you supply here wins.
Cache busting with cacheTag
Set cacheTag and the runtime appends a t=<cacheTag> query parameter to every metadata request — the manifest, the host's remoteEntry.json, and each remote's:
initFederation("/assets/federation.manifest.json", { cacheTag: BUILD_HASH });
Use a deployment-stable value — a build hash, a git SHA, a CI run ID. It only affects metadata fetches; module bundles are loaded through the import map and carry their own hashed filenames.
Serving the host's bundles elsewhere: deployUrl
By default the host's own remoteEntry.json is fetched from ./remoteEntry.json, relative to the page. Set deployUrl when the shell's build output lives somewhere else — a CDN or a sub-path — and it becomes both the fetch location and the prefix for the host's shared-dependency URLs. See initFederation → deployUrl.
There is no persistent cache, no logger injection and no storage layer on this runtime. Those arrived with the orchestrator.
Remote load errors
If a single remote fails (network error, 404, invalid JSON), initFederation does not reject. It logs to console.error and continues with the remotes that did load, on the rationale that one flaky remote shouldn't take the host down. For strict behaviour, call processRemoteInfos yourself:
import { processRemoteInfos } from "@angular-architects/native-federation";
await processRemoteInfos(
{ mfe1: "http://localhost:4201/remoteEntry.json" },
{ throwIfRemoteNotFound: true },
);
That only covers remote failures. If the host's own remoteEntry.json cannot be fetched or parsed, initFederation rejects with no fallback.
Hot reload
When a remote's remoteEntry.json contains a buildNotificationsEndpoint, the runtime opens an EventSource on it and calls window.location.reload() on federation-rebuild-complete. That is how ng serve reloads the host after a remote rebuilds. Production builds omit the field, so no connection is opened — nothing to configure on the runtime side.
loadRemoteModule
function loadRemoteModule<T = any>(
remoteName: string,
exposedModule: string,
): Promise<T>;
function loadRemoteModule<T = any>(
options: LoadRemoteModuleOptions<T>,
): Promise<T>;
interface LoadRemoteModuleOptions<T = any> {
remoteEntry?: string; // for lazy registration
remoteName?: string;
exposedModule: string;
fallback?: T;
}
Once initFederation resolves, lazy-load any exposed module from any registered remote. In an Angular shell that's normal lazy-loading — and because loadRemoteModule is a plain import, routes need no wiring:
// 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 key from initFederation's remotes map; exposedModule matches the key under exposes in the remote's federation.config.js.
Fallbacks
loadRemoteModule can fail three ways: unknown remote, unknown exposed module, or a failed dynamic import. All three reject by default. Pass a fallback and the first two resolve with it instead, logging the error to console.error — a failing import() still rejects, so wrap the call if that case must degrade too:
loadRemoteModule({
remoteName: "mfe1",
exposedModule: "./Component",
// stands in for the module, so give it the module's shape
fallback: { AppComponent: DefaultComponent },
});
Fallbacks suit non-critical widgets — a recommendations panel, an A/B variant — where a missing remote should degrade quietly. For main navigation, let it throw and handle it at the router's error boundary.
Lazy Remote Registration
To load a remote that was not in the initFederation manifest, pass its remoteEntry URL. The runtime fetches it, registers it, appends its import map to the DOM, then imports the exposed module:
const mod = await loadRemoteModule({
remoteEntry: "http://localhost:4203/remoteEntry.json",
remoteName: "mfe3",
exposedModule: "./Component",
});
This is the v3 answer to plugin-style hosts where the remote list is only known after user interaction. Registration happens once per base URL; later calls reuse the entry. Omit remoteName and the runtime derives it from the registry lookup by base URL, falling back to the name in the fetched remoteEntry.json; if neither resolves it throws unexpected arguments: Please pass remoteName or remoteEntry.
On v4 this single call is replaced by the more explicit initRemoteEntry, which also handles semver re-resolution for the late remote's shared dependencies.
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. URLs may be absolute (production CDN) or relative (local dev, same-origin deploys). This part is unchanged on v4.
What v4 Changes
| v3 (this page) | v4 | |
|---|---|---|
| Adapter runtime export | export * from '@softarc/native-federation-runtime' | Own initFederation bridging to the orchestrator |
initFederation resolves to | ImportMap | NativeFederationResult — the loader API |
loadRemoteModule | Module-level import, always available | Taken off the resolved result and threaded through DI; the top-level import is deprecated |
| Options | cacheTag, deployUrl | Full NFOptions — logger, storage, modes, SSE, profile |
| Version handling | One URL per packageName@version, no range comparison | Semver-range resolution and share scopes |
| Caching between loads | None | Pluggable storage |
| Adding a remote late | loadRemoteModule({ remoteEntry }) | initRemoteEntry |
Related
- Runtime — the runtime package itself, in full.
- Runtime (v4) — the v4 adapter's very different runtime surface.
- Migration to v4 — the upgrade path, including the bootstrap rewrite.
- v3 vs v4 — every breaking change between the lines, in one place.