Configuration
Every option you can pass to initFederation — host entry, import-map implementation, logging, resolution modes and storage.
Everything you pass as the second argument to initFederation lives on this page. Options are grouped into five concerns — host entry, import-map implementation, logging, modes, and storage — and every option ships with a sensible default, so a bare initFederation(manifest) is already a valid call.
- 1. Host configuration
- 2. Import-map implementation
- 3. Logging
- 4. Modes — strictness & resolution profile
- 5. Storage
1. Host configuration
A host remote entry is a remoteEntry.json published by the host itself. Whenever an external appears both in the host entry and in a regular remote, the host version wins — making this the right escape hatch for locking a framework or design-system version globally.
type HostOptions = {
hostRemoteEntry?:
| string
| false
| {
name?: string;
url: string;
cacheTag?: string;
integrity?: string;
};
manifestIntegrity?: string;
};
| Option | Default | Description |
|---|---|---|
hostRemoteEntry | false | Adds a host remoteEntry.json that takes precedence during shared-version resolution. Can be a URL, the full object form, or false to disable. |
hostRemoteEntry.cacheTag | none | Opaque string appended as a query parameter — the orchestrator treats a new cacheTag as a different file and refetches. Use this to bust caches after a host redeploy. |
hostRemoteEntry.integrity | none | SRI hash (sha256-…, sha384-…, sha512-…) verified against the response bytes of the host remoteEntry.json before it is parsed. See Security — Subresource Integrity. |
manifestIntegrity | none | SRI hash for the manifest URL passed as the first argument to initFederation. When set, the orchestrator hashes the response bytes and rejects with NFError on mismatch. |
Example
import { initFederation } from "@softarc/native-federation-orchestrator";
initFederation("http://example.org/manifest.json", {
manifestIntegrity: "sha384-…",
hostRemoteEntry: {
url: "./remoteEntry.json",
cacheTag: "v1.2.3",
integrity: "sha384-…",
},
});
Per-remote pinning lives in the manifest itself — entries can be either the existing string form or a { url, integrity } object, and the two forms can coexist:
{
"team/mfe1": "https://mfe1.example.org/remoteEntry.json",
"team/mfe2": {
"url": "https://mfe2.example.org/remoteEntry.json",
"integrity": "sha384-…"
}
}
See Security — Subresource Integrity for the full trust chain (manifest → remoteEntry.json → modules) and the supported hash algorithms.
2. Import-map implementation
The orchestrator commits a standard import map to the DOM and uses the browser's own import() to load modules. For older browsers — or whenever you need dynamic init — swap in es-module-shims.
type ImportMapOptions = {
loadModuleFn?: (url: string) => Promise<unknown>;
setImportMapFn?: (
importMap: ImportMap,
opts?: { override?: boolean },
) => Promise<ImportMap>;
reloadBrowserFn?: () => void;
preloadModulesFn?: (urls: string[]) => void;
trustedTypesPolicyName?: string | false;
};
| Option | Default | Description |
|---|---|---|
setImportMapFn | replaceInDOM('importmap') | How to commit an import map — by default, replaces any existing <script type="importmap"> in the DOM. |
loadModuleFn | url => import(url) | How a module is actually imported. Override when you need the shim loader or custom instrumentation. |
reloadBrowserFn | () => window.location.reload() | Called when the SSE dev feature detects a rebuilt remote. Override for custom reload UX. |
preloadModulesFn | preloadInDOM('modulepreload') | Receives the URLs to fetch ahead of their import — see Preloading. Supplying loadModuleFn turns the default off; pass preloadModulesFn as well to keep it. |
trustedTypesPolicyName | 'nfo' | Name of the Trusted Types policy that wraps import-map content and dynamic-import URLs in the default setImportMapFn and loadModuleFn. Pass false to opt out (e.g. when the host owns its own Trusted Types pipeline). No effect on browsers that do not support Trusted Types — the wrapper falls back to a transparent pass-through. |
Two ready-made presets
import "es-module-shims";
import { initFederation } from "@softarc/native-federation-orchestrator";
import {
useShimImportMap,
useDefaultImportMap,
replaceInDOM,
} from "@softarc/native-federation-orchestrator/options";
initFederation("http://example.org/manifest.json", {
// Option 1 — native import maps (default)
...useDefaultImportMap(),
// Option 2 — es-module-shims
...useShimImportMap({ shimMode: true }),
// Option 3 — custom
loadModuleFn: (url) => customImport(url),
setImportMapFn: replaceInDOM("importmap"),
// Option 4 — rename the Trusted Types policy to match a stricter CSP allowlist.
// This reaches the default import map only; pass it to a preset instead: useShimImportMap(cfg, 'my-app-nfo')
trustedTypesPolicyName: "my-app-nfo",
});
See Security — Trusted Types for the recommended CSP header and how the orchestrator interacts with a host-defined policy.
With native import maps (useDefaultImportMap()), the orchestrator usually adds its import map after module loading has started, and dynamic init adds more maps later. That needs a browser that supports multiple import maps: Chrome/Edge 133+ and Safari 18.4+; Firefox only behind the dom.multiple_import_maps.enabled preference. There is no feature test for it, so for browsers without support use useShimImportMap({ shimMode: true }). See caniuse.
Preloading
A browser finds a module's imports only after it has fetched and parsed it, so a first load is a chain of requests. The orchestrator shortens it by handing preloadModulesFn the files that are certain to be needed:
- On
loadRemoteModule— the chunks the exposed module imports statically, in the same tick as its import. - On
preloadRemoteModule(remote, exposedModule)— called by the app, for example on hover or when a route is about to open: the exposed module and its chunks. It never rejects; a module it cannot find is logged at debug level. - After the import map is committed, at init and after
initRemoteEntry— every shared external whose served copy was built withpreload: true: its entry files, and every chunk of the bundle it was built in. Only the copy the import map serves counts. A non-singleton package keeps a copy per remote, and each is served, so marking it fetches every remote's copy at init.
const { loadRemoteModule, preloadRemoteModule } = await initFederation(manifest);
button.addEventListener("mouseenter", () => preloadRemoteModule("team/mfe1", "./Dashboard"));
The lists come from remoteEntry.json: the initial rows of exposes[].chunks, shared[].preload, and the chunks per bundle in the top-level chunks. A remote built without those fields is not preloaded. A marked package in the combined shared bundle brings the chunks of every package in it (give it build: 'package'), and without features.denseChunking only its entry files are preloaded. A bundle's list also holds the chunks the package loads lazily, so a library that splits languages, themes or diagrams into lazy chunks preloads all of them.
Call preloadRemoteModule under the same rule as loadRemoteModule: once every remote the module imports is initialized. The browser fixes a preloaded file's imports at that moment, so a module preloaded before initRemoteEntry adds a remote it imports fails to load later.
A custom preloadModulesFn may receive the same URLs more than once, for example on every visit to a route. A preload that throws is logged at debug level and never fails the load. In shim mode, es-module-shims reports a failed modulepreload-shim fetch as an unhandled rejection, so a missing file (after a partial deploy, say) shows up in error trackers even when the page never imports it.
| Preset | preloadModulesFn |
|---|---|
useDefaultImportMap() | preloadInDOM('modulepreload'): one <link> per URL in document.head. |
useShimImportMap({ shimMode: true }) | preloadInDOM('modulepreload-shim'), the link type es-module-shims reuses in shim mode. |
useShimImportMap() (polyfill mode) | preloadInDOM('modulepreload'). |
A custom loadModuleFn | noPreload: a custom loader would not reuse the <link> fetches. |
Node (initNodeFederation) | noPreload. |
To turn preloading off, pass preloadModulesFn: noPreload (exported from /options, like preloadInDOM).
3. Logging
Diagnostics for both development and production. Ships with two built-in loggers and accepts any object that matches the Logger interface.
type LoggingOptions = {
logger?: Logger;
logLevel?: "debug" | "warn" | "error";
sse?: boolean;
};
interface Logger {
debug(step: number, msg: string, details?: unknown): void;
warn(step: number, msg: string, details?: unknown): void;
error(step: number, msg: string, details?: unknown): void;
}
| Option | Default | Description |
|---|---|---|
logger | noopLogger | Where logs go. Use consoleLogger during development, or provide your own for Sentry / Bugsnag / etc. |
logLevel | 'error' | Level threshold. 'warn' emits warn+error; 'debug' emits everything. |
sse | false | Dev feature — listens to server-sent rebuild events from remotes and triggers reloadBrowserFn. See below. |
With sse on, one tab per origin holds each build-notification stream: tabs elect a holder through a Web Lock and it relays rebuild events to the others over a BroadcastChannel, so many open tabs don't exhaust the browser's per-origin connection limit. A tab only reloads for endpoints of remotes it has loaded itself. Streams close when a page enters the back/forward cache and reopen on restore. Where navigator.locks is unavailable, each tab connects directly.
Example
import { initFederation } from "@softarc/native-federation-orchestrator";
import {
noopLogger,
consoleLogger,
} from "@softarc/native-federation-orchestrator/options";
initFederation("http://example.org/manifest.json", {
logLevel: "debug",
logger: consoleLogger, // or noopLogger, or a custom Logger
});
4. Modes — strictness & resolution profile
Mode options are the hyperparameters for the Version Resolver: how strict to be when something unexpected happens, and how aggressively to reuse cached state.
type ModeOptions = {
strict?:
| boolean
| {
strictRemoteEntry?: boolean;
strictExternalCompatibility?: boolean;
strictExternalSameVersionCompatibility?: boolean;
strictExternalVersion?: boolean;
strictImportMap?: boolean;
};
profile?: {
latestSharedExternal?: boolean;
skipInvalidExternalVersions?: boolean;
overrideCachedRemotes?: "always" | "never" | "init-only";
overrideCachedRemotesIfURLMatches?: boolean;
cacheTag?: string;
mergeManifests?: boolean;
enableDebugging?: boolean;
};
feature?: {
convertFlatSharedInfo?: boolean;
};
};
Strictness
All flags default to false, which means "log and continue". Setting strict: true turns them all on at once.
| Option | Effect |
|---|---|
strict | Shortcut — sets every specific strict flag below to true. |
strict.strictRemoteEntry | Throws on malformed remoteEntry.json. When false, the broken remote is skipped and initialization continues. |
strict.strictExternalCompatibility | Throws when two shared externals have incompatible version ranges. When false, the incompatible version is demoted to a scoped external with a warning. |
strict.strictExternalSameVersionCompatibility | Niche edge case — throws when an already-cached shared version is re-submitted with a different requiredVersion range. Otherwise, the cached entry is preserved. |
strict.strictExternalVersion | Throws if a shared external's version is missing or not valid semver. When false, the external is instead coerced to the smallest version matching its requiredVersion range — unless profile.skipInvalidExternalVersions is on, in which case it is skipped. This flag takes precedence over profile.skipInvalidExternalVersions. |
strict.strictImportMap | Throws when the import-map builder encounters corrupt cache state. |
Upgrading from 4.7. Orchestrator 4.8.0 removed strict.strictEntryPointCoverage and profile.scopeUncoveredEntrypoints. Both are still accepted and ignored: a remote now always runs a package from one build, so there is no tear left to throw on or scope away. See Version Resolver — One build per package.
Resolution profile
The profile controls how the resolver picks winners and whether it refreshes cached remotes.
| Option | Default | Description |
|---|---|---|
profile.latestSharedExternal | false | When true, always pick the highest version in the scope. When false (default), pick the version that minimizes extra scoped downloads. |
profile.skipInvalidExternalVersions | false | When true, an external whose version is missing or not valid semver is skipped (never stored) instead of being coerced to the smallest version of its requiredVersion range. Has no effect when strict.strictExternalVersion is set — that throws instead. |
profile.overrideCachedRemotes | 'init-only' | When to refetch a remote that already lives in cache — see below. |
profile.overrideCachedRemotesIfURLMatches | false | By default, a cached remote is only overridden when its URL changed. Set this to true to force refetch even when the URL is identical. Without it, a remote redeployed at the same URL keeps its stored remoteEntry.json over a persisted cache, even with a cacheTag; deploy each build at its own URL, or enable this. Has no effect under overrideCachedRemotes: 'never'. |
profile.cacheTag | none | When set, appended as a ?cacheTag=<value> query parameter to every remote's remoteEntry.json request, letting you bust HTTP caches across all remotes at once. The host's own hostRemoteEntry.cacheTag takes precedence for the host entry when both are set. |
profile.mergeManifests | true | When true, every page's manifest adds to what storage keeps, so remotes from other pages (and ones loaded through initRemoteEntry) stay available. Set false only when every page passes the same complete manifest: an init then drops the stored remotes its manifest doesn't list, so a remote removed from the manifest stops being served on the next page. See Remotes a manifest no longer lists. |
profile.enableDebugging | false | Development only. When true, initFederation ranks a copy built in dev mode right after the host when it picks the basis of a shared version, so the remote you are working on serves a package a static sibling of the same version would otherwise serve. On a reload the dev build serves only when the remotes are refetched (overrideCachedRemotesIfURLMatches) or storage is in globalThis. See Preferring a dev build while debugging. |
overrideCachedRemotes values:
'never'— don't touch cached remotes, ever. Strongest cache, slowest pickup of new versions.'init-only'(default) — allow overrides duringinitFederation. Dynamic init never overrides a remote that is already initialized: the committed import map cannot drop its imports.'always'— deprecated; behaves like'init-only'.
Features
Opt-in behaviors that change how shared externals are processed and stored.
| Option | Default | Description |
|---|---|---|
feature.convertFlatSharedInfo | false | Opts into runtime densification of a remote's shared externals. A core build with denseExternals on emits DenseSharedInfo (a per-package entries map covering primary and secondary entrypoints) natively, and those pass through unchanged. For flat remote builds that emit one flat SharedInfo per entrypoint, enabling this groups secondary entrypoints under their parent package (by npm scope) so they resolve as one shared external. See Version Resolver — Secondary entrypoints. |
Two ready-made profiles
import { initFederation } from "@softarc/native-federation-orchestrator";
import {
defaultProfile,
cachingProfile,
} from "@softarc/native-federation-orchestrator/options";
// defaultProfile
// { latestSharedExternal: false, skipInvalidExternalVersions: false, overrideCachedRemotes: 'init-only', overrideCachedRemotesIfURLMatches: false, mergeManifests: true, enableDebugging: false }
// cachingProfile
// { latestSharedExternal: false, skipInvalidExternalVersions: false, overrideCachedRemotes: 'never', overrideCachedRemotesIfURLMatches: false, mergeManifests: true, enableDebugging: false }
initFederation("http://example.org/manifest.json", {
strict: true,
profile: cachingProfile,
});
The caching profile is tempting but comes with a trade-off: newly deployed remote versions won't be picked up until the cache is explicitly cleared. Pair it with clearStorage: true on cache-busting events, or stick with defaultProfile when remotes change frequently.
5. Storage
The orchestrator keeps its internal caches (remote info, shared externals, scoped externals) inside a storage entry. The default is an in-memory map on globalThis, but any Storage-compatible backend works.
type StorageOptions = {
storage?: StorageEntryCreator;
clearStorage?: boolean;
storageNamespace?: string;
exposeStorageGetter?: boolean;
};
| Option | Default | Description |
|---|---|---|
storage | globalThisStorageEntry | How the cache is persisted. Built-ins: globalThisStorageEntry, sessionStorageEntry, localStorageEntry. Custom implementations are supported; like the built-ins, they must store a copy on set and return a fresh copy from get. |
clearStorage | false | When true, initFederation wipes the namespace before initializing — handy for one-shot cache busts after a deploy. A cache written by another orchestrator version is always cleared once, automatically. |
storageNamespace | '__NATIVE_FEDERATION__' | Namespace prefix for stored keys (e.g. __NATIVE_FEDERATION__.remotes). Change it to run multiple orchestrators on the same origin. |
exposeStorageGetter | true | Since 4.7.0. Publishes a read-only get(key) on globalThis.__NF_ORCHESTRATOR__. Set to false to keep the state of a custom storage unreadable for page scripts. |
A write the storage rejects — a full localStorage or sessionStorage, for example — is logged as a warning. That page keeps its cache in memory from then on, and the next page starts without one.
Example
import { initFederation } from "@softarc/native-federation-orchestrator";
import {
globalThisStorageEntry,
localStorageEntry,
sessionStorageEntry,
} from "@softarc/native-federation-orchestrator/options";
initFederation("http://example.org/manifest.json", {
clearStorage: true,
storageNamespace: "__custom_namespace__",
// Option 1 — in-memory (default)
storage: globalThisStorageEntry,
// Option 2 — persisted for the browser session
storage: sessionStorageEntry,
// Option 3 — persisted across browser restarts
storage: localStorageEntry,
});
Most server-rendered hosts want sessionStorageEntry: it survives navigation-triggered full reloads (the whole point of caching here) but is automatically cleared when the tab closes, so a stale resolution can never live longer than the user's session.
Discovering the storage from tools
Since 4.7.0. Every initFederation call publishes a frozen, read-only descriptor on globalThis.__NF_ORCHESTRATOR__, so devtools, browser extensions and debug scripts can find the cached state without knowing the host's config:
globalThis.__NF_ORCHESTRATOR__ = {
storage: {
// keyed by storageNamespace; every namespace on the page is listed
__NATIVE_FEDERATION__: {
version: "4.10.0", // the orchestrator that published this namespace
type: "localStorage", // 'globalThis' | 'localStorage' | 'sessionStorage' | 'custom'
namespace: "__NATIVE_FEDERATION__",
keys: ["remotes", "shared-externals", "scoped-externals", "shared-chunks", "cache-state"],
get: (key) => {}, // a copy of the stored value; absent when exposeStorageGetter is false
},
},
};
There are two ways to read the state:
get(key)works for every storage, custom ones included. It returns a copy of the last committed value — in-progress work only reaches storage when a flow commits it — orundefinedfor a key that isn't inkeys. Nothing on the descriptor can write to the storage. For the built-in storages this exposes nothing a page script couldn't already read, but a custom storage that keeps its state in a closure becomes readable by any script on the page; passexposeStorageGetter: falseto prevent that.- Reading the location directly suits tools that must not call page functions, such as a passive devtools probe.
typetells you where each key lives:
type | Location of <key> |
|---|---|
globalThis | globalThis[namespace][key] |
localStorage | localStorage.getItem(`${namespace}.${key}`) (JSON) |
sessionStorage | sessionStorage.getItem(`${namespace}.${key}`) (JSON) |
custom | Not directly readable — use get(key) |
Each namespace carries its own version, because separate orchestrator bundles on one page can publish side by side. Publishing never fails initFederation: if the global can't be written, the descriptor is simply missing.
A custom StorageEntryCreator reports 'custom' unless you tag it with a type property — e.g. myStorage.type = 'localStorage' when it wraps localStorage with the same key layout. The published object is typed as NFOrchestratorGlobal, exported from @softarc/native-federation-orchestrator/options.
Putting it together
import { initFederation } from "@softarc/native-federation-orchestrator";
import {
consoleLogger,
sessionStorageEntry,
useShimImportMap,
cachingProfile,
} from "@softarc/native-federation-orchestrator/options";
const { loadRemoteModule } = await initFederation(manifest, {
// Host
hostRemoteEntry: { url: "./host-remoteEntry.json", cacheTag: "2025-04-22" },
// Import map
...useShimImportMap({ shimMode: true }),
// Logging
logLevel: "warn",
logger: consoleLogger,
// Modes
strict: { strictRemoteEntry: true, strictExternalCompatibility: true },
profile: cachingProfile,
// Storage
storage: sessionStorageEntry,
storageNamespace: "__NATIVE_FEDERATION__",
clearStorage: false,
});
See also
- The orchestrator docs — Which shows a bit more in-depth how the orchestrator can be configured.
- Security & Subresource Integrity — CSP setup for the built-in Trusted Types policy and the SRI trust chain (manifest →
remoteEntry.json→ modules). - Version Resolver — how the profile and strictness flags actually shape resolution output.
- Architecture — caches — what lives in the storage namespace.
- Getting Started — worked examples for the quickstart, registry and custom setups.