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

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.

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 exportexport * from '@softarc/native-federation-runtime'Own initFederation bridging to the orchestrator
initFederation resolves toImportMapNativeFederationResult — the loader API
loadRemoteModuleModule-level import, always availableTaken off the resolved result and threaded through DI; the top-level import is deprecated
OptionscacheTag, deployUrlFull NFOptions — logger, storage, modes, SSE, profile
Version handlingOne URL per packageName@version, no range comparisonSemver-range resolution and share scopes
Caching between loadsNonePluggable storage
Adding a remote lateloadRemoteModule({ remoteEntry })initRemoteEntry