Private beta — the @z0devs packages are available to design partners while we harden for general availability.Try the live demoRequest access

Package reference

Ten packages — what each owns, its key exports, its peers, and whether you need it.

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, a meta package and a CLI — ten in all. Install a slice or the whole thing.

pnpm add @z0devs/module-federation \
  react react-dom react-router-dom eventemitter3 react-error-boundary \
  @module-federation/runtime @tanstack/react-query

@z0devs/mf-context#

Cross-bundle singleton React context and a typed event bus.

import { createMFSharedContext, createEventsEmitter, useListenerContext } from "@z0devs/mf-context";
 
const shared = createMFSharedContext({ useAuth, useGlobalStore });
export const { MFSharedContextProvider, useMFSharedContext } = shared;
 
type Events = { "user:login": { id: string }; "user:logout": void };
const bus = createEventsEmitter<Events>();
bus.addListener("user:login", (e) => console.log(e.id));
bus.emit("user:login", { id: "u_1" });

useListenerContext(bus, ["tick"]) re-renders a component when any listed event fires.

Peers: react >=18, eventemitter3 ^5 · Standalone: yes · Tier: stable

@z0devs/mf-router#

Dynamic React Router host. Routes live in a registry; the provider rebuilds the router when it changes.

import {
  RouterContextProvider,
  Router,
  RouteRegistry,
  routeRegistryApi,
  useRouteRegistry,
  useRouterContext,
  type RouteItem,
} from "@z0devs/mf-router";

RouteItem is a react-router RouteObject plus id (required) and optional order / flags, so loader, action, children, errorElement and lazy all work. The provider injects registered routes at initialRoutes[0].children[0].children.

Peers: react >=18, react-dom >=18, react-router-dom >=6.4 <8, @z0devs/mf-registry · Standalone: yes · Tier: stable

@z0devs/mf-registry#

Namespaced contribution registries — a factory plus five built-ins.

import {
  createRegistryContext,
  withRemoteNamespace,
  toPluginRegistry,
  NavRegistry,
  SettingsRegistry,
  ToolbarRegistry,
  ComponentRegistry,
  OnboardingStepsContext,
  onboardingStepsApi,
  useOnboardingStepsRegistry,
  type RegistryApi,
  type PluginRegistryApi,
  type RegistryItem,
  type NavItem,
  type SettingsItem,
  type ToolbarItem,
  type DynamicComponent,
  type OnboardingStep,
} from "@z0devs/mf-registry";

Each built-in exports { Context, api, useRegistry } — pass .api to defineSharedStore. Full RegistryApi: addItem, removeItem, getItem, getItems, getActiveFlags, subscribe, setActiveFlags, clearRemote, addListener, removeListener, emit. Remotes get the PluginRegistryApi subset — everything except setActiveFlags, clearRemote and the emitter.

For rendering, use the hooks rather than getItems() in render, which is stale after any mutation:

import { useRegistryItem, useRegistryItems } from "@z0devs/mf-registry";
 
const items = useRegistryItems(NavRegistry.api);              // re-renders on any listed change
const one = useRegistryItem(ComponentRegistry.api, "a:Chart"); // only when THAT item changes

Two properties of the read API worth knowing: gating is uniform (getItem applies the same flag filter as getItems, so a hidden item is absent through every path), and getItems() returns a shared snapshot — the same array every caller and every subscriber reads. Copy before sorting, [...getItems()].sort(...); sorting it in place silently reorders the registry for everyone else and leaves the reference unchanged, so React never re-renders to correct it. It is frozen in development only, so that mistake throws while you're building and costs nothing per-mutation in production — don't rely on the TypeError to catch it after you ship.

Peers: react >=18, eventemitter3 ^5 · Standalone: yes · Tier: evolving

@z0devs/mf-core#

The Module Federation layer: remote-loading components, manifest schema, integrity, telemetry, prefetch.

import {
  BaseRemote, Remote, InitRemote, OnReady,
  MFEnvProvider, InitPropsProvider,
  createRuntimePlugin,
  validateManifestEntry, resolvePluginDependencies,
  PluginErrorProvider, usePluginErrorHandler, safeEmitPluginError, pluginErrorSeverity,
  usePluginRuntime, getAllPluginRecords, subscribeToPluginRuntime, getPluginMetrics,
  prefetchEntry, usePluginPrefetch,
  SHELL_API_VERSION,
  type MFEManifestEntry, type PluginErrorRecord, type RemoteLoadPolicy,
} from "@z0devs/mf-core";
ComponentRole
BaseRemoteLow-level loader: integrity → register → optional env-config → React.lazy render, with retry, timeout and Suspense fallback
RemoteBaseRemote inside a react-error-boundary; pass errorFallback
InitRemoteLoads a remote's init, computing props from InitPropsProvider's useInitProps(name). Silent error boundary
OnReadyFires onReady once children mount (used internally)
ProviderRole
MFEnvProviderSets the env namespace (default "__MFE_ENV__") and an optional shellConfigLoader deep-merged under each remote's env-config
InitPropsProviderSupplies useInitProps(remoteName) to InitRemote

Peers: react >=18, react-dom >=18, react-error-boundary ^4 || ^5, @module-federation/runtime >=0.6.0 <3.0.0 · Standalone: yes · Tier: evolving

@z0devs/mf-shell-kit#

The opinionated host side.

import {
  PluginRouteLoader,
  defineSharedStore,
  OnboardingHost,
  createShellApp,
  PluginDebugPanel,
  useLifecycle,
  defineSubmodule,
  type SharedInitProps, type PluginInitProps, type DefinedSharedStore,
  type StoreFeatures, type AuthAdapter, type ManifestLoader,
} from "@z0devs/mf-shell-kit";

features: { router: true, onboarding: true } on defineSharedStore injects the routes and onboardingSteps registries so you don't wire them by hand.

Peers: react >=18, react-dom >=18, react-router-dom >=6.4 <8, @tanstack/react-query >=5, @z0devs/mf-context >=0.1.0, @z0devs/mf-core >=0.3.0, @z0devs/mf-registry >=0.1.0, @z0devs/mf-router >=0.1.0 · Standalone: needs siblings · Tier: evolving

@z0devs/mf-plugin-kit#

The plugin side.

import {
  createPluginStoreBridge,
  createLocalStubContext,
  createProxyClient,
  type SelectSharedStoreSlice,
} from "@z0devs/mf-plugin-kit";

The bridge returns { useShellSharedStore, ready, CombinedStoreProvider, withCombinedStore }. ready never rejects, which is what makes top-level await safe. Legacy ExtractStateFromHook, StoreSelectionConfig and SelectStoresDetailed remain exported for pre-{ values, registries } consumers.

useRegistryItem / useRegistryItems and the plugin-facing types are re-exported here from @z0devs/mf-registry, so a remote gets them from the one package it already depends on. The hooks are a runtime re-export, which is why mf-registry is a peer and not just a type-level reference.

Peers: react >=18, @z0devs/mf-registry >=0.2.0 · Standalone: yes · Tier: stable

@z0devs/mf-testing#

import {
  createMockShellStore,
  createMockAuthAdapter,
  mockManifestEntry,
  renderWithMockShell,
} from "@z0devs/mf-testing";

@testing-library/react is declared an optional peer, so you only need it installed if you use renderWithMockShell; the rest are required.

Peers: react >=18, @z0devs/mf-context >=0.1.0, @z0devs/mf-core >=0.2.0, @z0devs/mf-registry >=0.2.0, @z0devs/mf-shell-kit >=1.0.0, @testing-library/react ^14.0.0 || ^15.0.0 || ^16.0.0 · Tier: experimental — pin exact versions in CI

@z0devs/mf-config#

Build time, not runtime. Emits the shared map and the ModuleFederationPlugin options as plain data you spread into your own bundler config. Zero dependencies, no React, never reaches a browser bundle.

import { z0Shared, z0Federation } from "@z0devs/mf-config";
 
new ModuleFederationPlugin(
  z0Federation({
    name: "my_plugin",
    exposes: { "./init": "./src/init.tsx" },
    shellEntry: process.env.SHELL_ENTRY,
    router: true,          // also share react-router-dom
  }),
);

The value is the subpath entrypoints: alongside react / react-dom it emits react/jsx-runtime, react/jsx-dev-runtime and react-dom/client, which every participant must agree on. Code compiled with the automatic JSX runtime imports those directly, so sharing only the package roots leaves a remote with its own React — the classic Cannot read properties of null (reading 'useEffect').

It returns data, never a plugin instance, so nothing is a one-way door: { ...z0Federation({…}), shareStrategy: "loaded-first", dts: false } overrides any field, and taking only z0Shared() while hand-writing the rest of the block is a supported path. target: "vite" emits the interdependent settings an rspack host needs from a Vite remote — see Cross-bundler.

Peers: none · Standalone: yes · Tier: stable

@z0devs/module-federation#

Meta package. One install, everything re-exported, plus subpaths for tighter tree-shaking:

import { PluginRouteLoader, BaseRemote, createPluginStoreBridge } from "@z0devs/module-federation";
 
import { Router } from "@z0devs/module-federation/router";
import { BaseRemote } from "@z0devs/module-federation/core";
import { createPluginStoreBridge } from "@z0devs/module-federation/plugin-kit";

Subpaths: /context, /router, /registry, /core, /shell-kit, /plugin-kit, /config.

/config is the build-time one and is deliberately absent from the root barrel — folding bundler-config code into index would pull it into every app bundle:

import { z0Federation } from "@z0devs/module-federation/config";

The meta package re-exports the others rather than bundling them, so it carries their peers as its own — this is the same list the "Everything" install command at the top of this page installs.

Peers: react >=18, react-dom >=18, react-router-dom >=6.4 <8, eventemitter3 ^5.0.0, react-error-boundary ^4.0.0 || ^5.0.0, @module-federation/runtime >=0.6.0 <3.0.0, @tanstack/react-query >=5 · Standalone: yes · Tier: evolving

create-z0-plugin#

pnpm create z0-plugin my-plugin

See the CLI reference.

Picking a set#

You are…Install
Writing a plugin for someone else's shellmf-plugin-kit (+ mf-testing in dev)
Building a shell from scratchthe meta package, or the five host packages
Only after a runtime-registerable routermf-router + mf-registry
Only after contribution registriesmf-registry
Only loading a remote component or twomf-core
Any of the aboveadd mf-config as a devDependency — it only runs in your bundler config

Start with the meta package

Ship first, then split to individual packages once you know which ones you actually touch — the subpath exports mean bundle size isn't the reason to decide early.

Next#