The library is a batteries-included host + plugin toolkit layered on the official Module Federation runtime. React-only by design, aimed at trusted plugins loaded from a host-controlled manifest.
Layer model#
Four primitives that stay out of each other's way — mf-context, mf-registry and mf-core depend on
none of the others, and mf-router depends only on mf-registry, because routes are a registry. Then
two kits that compose them, a build-time config helper, a test harness and a meta package:
┌──────────────────────────────────────────────┐
meta install ───▶ │ @z0devs/module-federation (re-exports all) │
└──────────────────────────────────────────────┘
host side plugin side
┌────────────────────────┐ ┌─────────────────────────┐
│ @z0devs/mf-shell-kit │ │ @z0devs/mf-plugin-kit │
│ PluginRouteLoader │ │ createPluginStoreBridge│
│ defineSharedStore │ │ createLocalStubContext │
│ OnboardingHost │ │ SelectSharedStoreSlice │
│ createShellApp │ │ createProxyClient │
└───────────┬────────────┘ └───────────┬─────────────┘
│ composes the primitives │
┌───────────┴───────────────────────────────────────────┴───────────┐
│ mf-core mf-router mf-registry mf-context │
│ BaseRemote RouterContext createRegistry… createMFShared…│
│ InitRemote RouteRegistry Nav/Settings/… createEvents… │
│ integrity Router withRemoteNamespace useListener… │
│ PluginRuntime… (react-router) flags + scoping (singletons) │
│ prefetch │
└───────────────────────────┬─────────────────────────────────────────┘
│
@module-federation/runtime (official, bundler-agnostic)| Package | Role | Key exports |
|---|---|---|
mf-context | cross-bundle singleton context + event bus | createMFSharedContext, createEventsEmitter, useListenerContext |
mf-router | dynamic React Router host | RouterContextProvider, Router, RouteRegistry, routeRegistryApi, useRouterContext |
mf-registry | namespaced contribution registries | createRegistryContext, built-ins, withRemoteNamespace, toPluginRegistry |
mf-core | MF runtime + remote components | BaseRemote, Remote, InitRemote, OnReady, createRuntimePlugin, validateManifestEntry, resolvePluginDependencies, PluginRuntimeRegistry, prefetchEntry, SHELL_API_VERSION, share-scope preflight (analyzeShareScope, diagnoseLoadError) |
mf-shell-kit | host orchestrator | PluginRouteLoader, defineSharedStore, OnboardingHost, createShellApp, PluginDebugPanel, useLifecycle |
mf-plugin-kit | remote-side bridge | createPluginStoreBridge, createLocalStubContext, SelectSharedStoreSlice, createProxyClient, re-exports Z0InitProps + the registry hooks |
mf-config | build time, not runtime | z0Shared, z0Federation — the share map and ModuleFederationPlugin options as plain data. Zero deps, no React, never in a browser bundle |
mf-testing | plugin test harness | createMockShellStore, createMockAuthAdapter, mockManifestEntry, renderWithMockShell |
Because the primitives don't depend on each other, you can install exactly one of them. The opinionated pieces live in the two kits.
Boot sequence#
loadManifest() ← host fetches MFEManifestEntry[] from a trusted source
│
▼ PluginRouteLoader
validateManifestEntry(entry) ← malformed → skipped + `manifest-invalid` telemetry
│
▼
enabled? + shellApiVersion compat ← versionPolicy block/warn/off → `disabled` / `version-blocked`
│
▼
resolvePluginDependencies(entries) ← inter-plugin semver → `dependency-unmet`
│
▼ per surviving entry
<InitRemote name={service.id} …> ← integrity check → registerRemotes → loadRemote(`${id}/init`)
│ (retry + timeout per loadPolicy)
▼ the plugin's init renders once
registries.routes.addItem(...) ← auto-namespaced via withRemoteNamespace(remoteName)
registries.nav.addItem(...) ← router rebuilds; nav re-renders
│
▼
onReady → PluginRuntimeRegistry: ready ← timings + status → usePluginRuntime / PluginDebugPanel
│
▼
loader unmounts, <Outlet/> renders ← optionally behind <OnboardingHost/>Shared library versions (React, your design system) are negotiated by Module Federation's own
shared config — singleton, requiredVersion, strictVersion. The library does not re-implement
that; it adds the plugin-level layer MF doesn't model.
The host↔plugin contract#
Four channels, each with one direction and one owner:
| Channel | Direction | Mechanism | Namespaced? |
|---|---|---|---|
| Values (auth, fetch, stores) | host → plugin | defineSharedStore({ values }) → createPluginStoreBridge | n/a |
| Contributions (routes, nav, settings, toolbar, components, onboarding) | plugin → host | registries via init props | yes, per remote |
| Lifecycle events | host ↔ plugin | typed emitter on the store | n/a |
| Compatibility | host declares, plugin matches | apiVersion ↔ shellApiVersion | n/a |
The store is a federation singleton, so a single instance survives across bundles. So are the
registries and the router context — all backed by a globalThis registry keyed per context, which is
what lets two separately-built bundles see the same object.
Typed remotes, and the one thing DTS can't do#
Plugins emit cross-remote types with @module-federation/enhanced's dts.generateTypes (on by
default in the scaffold) → dist/@mf-types. A consumer that statically references a remote gets
typed imports by wiring tsconfig:
{
"compilerOptions": { "paths": { "*": ["./@mf-types/*"] } },
"include": ["src/**/*", "./@mf-types/*"]
}That covers a plugin importing shell/sharedStore. It cannot cover the host loading plugins,
because the host loads them dynamically — loadRemote(`${id}/init`) with a runtime-built id, which
no literal-union augmentation can type. Those are typed at the call site instead:
<BaseRemote<typeof MyPage> name="billing_mfe" componentId="billing_mfe/MyPage" />Deliberate seams#
Two files exist purely so future majors are a one-file change:
mf-core/src/mfRuntime.ts— every@module-federation/runtimecall routes through here. The runtime's plugin type is extracted structurally (NonNullable<Parameters<typeof registerPlugins>[0]>[number]) rather than imported by name, so it tracks the rename fromFederationRuntimePlugin(0.x) toModuleFederationRuntimePlugin(2.x) without caring.mf-router/src/reactRouter.tsandmf-shell-kit/src/reactRouter.ts— allreact-router-domimports funnel through one re-export, which is why>=6.4 <8is satisfiable by one build.
Not a portability shim
Building on @module-federation/runtime is the standard, portable choice — it's the official
bundler-agnostic runtime that Webpack, Rspack and Vite all bind to. The seams above are hygiene for
clean major swaps, not an escape hatch from lock-in.
Where state lives#
| State | Owner | Survives a plugin reload? |
|---|---|---|
| Auth / tenant context | Host, exposed as values | Yes |
| Registered routes | RouteRegistry (globalThis singleton) | Removed by the plugin's cleanup, re-added on re-init |
| Contributions | Per-registry singletons | Same |
| Plugin lifecycle records | PluginRuntimeRegistry | Yes — that's the point |
| Plugin-local UI state | The plugin | No |
| Env config | window.__MFE_ENV__[remoteName] | Yes, until reload |