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

Examples

The runnable host and plugin pairs in the library repo, what each demonstrates, and how to run them together.

The library repo ships three example apps under examples/. They are the reference implementations these docs quote — and the set every feature is verified against before release.

All three are built and served as static files on this site, so you can drive the real thing here:

basic-shell — Rspack host, two federated remotesOpen in a tab ↗
Remotes loaded at runtime:basic_plugin (rspack)vite_plugin (vite)

The shell fetches mfes.json, validates every entry, gates them on version and inter-plugin dependencies, then mounts one <InitRemote> per survivor. Everything in the sidebar, the toolbar and the settings page arrived from a registry at runtime — the host imported none of it at build time.

Six manifest entries, and four of them fail on purpose so the guardrails are visible rather than claimed:

EntryOutcomeWhat it proves
basic-pluginreadyRspack remote: route, nav, settings panel, flag-gated route, published component
vite-pluginreadyVite remote under an Rspack host, sharing React, reading the store through the TLA bridge
legacy-reportsversion-blockedDeclares shellApiVersion: "^9.0.0"; skipped before a byte is fetched
invoicesdependency-unmetRequires a billing plugin that isn't in the manifest
tampered-widgetintegrity-violatedremoteEntryHash doesn't match the bytes — checked before registerRemotes, never retried
offline-analyticsload-failedRemote host is down; retried per loadPolicy, then reported

Things worth clicking:

Flip the beta flag

A nav item, a route and a settings entry appear. They were registered at init with flags: { required: ["beta"] } — the registry filters them, so neither the plugin nor the shell's nav component contains a conditional.

Switch org

Both remotes re-render with the new tenant because they read the host's useOrg hook. The event log shows the matching onOrgChange emit — the part a hook can't express.

Open the Vite plugin

It renders basic_plugin:SalesWidget — a component built by the Rspack remote, resolved by id from the component registry, rendered inside a Vite bundle. Neither plugin imports the other.

Toggle basic-plugin off, then on

Its route, nav item, settings panel and published component all leave and come back. The Vite page switches to its fallback and back, live, because it subscribes to that one registry item itself — the host hands out nothing extra.

How a plugin re-renders on another plugin's changes

A remote receives PluginRegistryApi — read and append, no emit / addListener, because the shared event bus is host-only state. But it does get subscribe: being told the listed set changed is a read capability, so a plugin re-renders on its own. Use the hooks rather than calling getItem in render, which is correct on first paint and stale after another plugin is toggled:

import { useRegistryItem } from "@z0devs/mf-plugin-kit";
 
const widget = useRegistryItem(registries.components, "basic_plugin:SalesWidget");
return widget ? <widget.component /> : <Fallback />;

useRegistryItem subscribes to that one item, so an unrelated plugin registering a nav entry re-renders nothing. Earlier versions had no subscribe, which forced every host to hand plugins a revision counter as a value; that workaround is gone.

basic-shell — the host#

An Rspack host that wires the whole stack in four small files.

examples/basic-shell/
├── rspack.config.ts          # exposes ./sharedStore, shares react/react-dom/react-router-dom
├── public/mfes.json          # the manifest — six entries: two that load, four that fail on purpose
└── src/
    ├── main.tsx              # MF runtime init + provider stack
    ├── routes.tsx            # PluginRouteLoader inside the dynamic route tree
    ├── auth/fakeAuth.ts      # stub auth adapter
    └── store/
        ├── sharedStore.tsx   # defineSharedStore + a demo onboarding step
        └── useSharedStore.ts # back-compat flat expose

What it demonstrates:

  • defineSharedStore with values, four registries, features: { onboarding: true, router: true } and a typed events emitter
  • export default sharedStore.useShellSharedStore — the expose contract the plugin bridge expects
  • A render-action onboarding step registered by the shell itself
  • PluginRouteLoader mounted as a route element, with initialRoutes shaped so dynamic routes land in the right place
  • dts.generateTypes so plugins can type shell/sharedStore against the real store
pnpm --filter @z0devs/basic-shell dev      # http://localhost:3400

basic-plugin — the remote#

An Rspack remote that runs federated and standalone.

examples/basic-plugin/
├── rspack.config.ts          # exposes ./init + ./HomePage, DefinePlugin for IS_STANDALONE, dts on
├── manifest-entry.json       # what the host appends to mfes.json
├── public/env-config.js      # runtime env injection demo
└── src/
    ├── init.tsx              # registers a route + nav item through namespaced registries
    ├── HomePage.tsx          # reads the host store
    ├── main.tsx              # standalone entry
    └── core/
        ├── LocalContext.ts   # createLocalStubContext — fail-loud
        └── useShared.tsx     # createPluginStoreBridge + top-level `await bridge.ready`

What it demonstrates:

  • The self-initializing bridge: await bridge.ready at module scope, so no provider, HOC or Suspense is needed anywhere
  • Registering routes and nav through props.registries with short local ids
  • Cleanup on unmount, so host re-inits don't accumulate stale contributions
  • Standalone mode via IS_STANDALONE=true, with a stub that throws on unstubbed reads
  • Cross-remote type emission (dist/@mf-types)
pnpm --filter @z0devs/basic-plugin dev              # federated, port 3401
pnpm --filter @z0devs/basic-plugin dev:standalone   # no host required

vite-plugin — the cross-bundler case#

A Vite-built remote (@module-federation/vite) mounted inside the Rspack host, sharing the host's React and reading the host's store through the top-level-await bridge. Its mfes.json entry ships enabled: true: both remotes reach ready on a cold boot.

pnpm --filter @z0devs/vite-plugin-example build
pnpm --filter @z0devs/vite-plugin-example preview        # 3402 — must be preview, not dev
pnpm --filter @z0devs/basic-shell dev                    # 3400
pnpm --filter @z0devs/basic-plugin dev                   # 3401

http://localhost:3400 then shows a Vite Plugin nav item, and /vite-plugin renders the remote with the host's user and Mode: federated.

CommandPortBrowse it directlyWorks as the shell's remote
preview3402no — federated buildyes, the supported path
dev:standalone3412yesn/a
dev3402no — blank pageno — RUNTIME-015: module is not defined

What it demonstrates:

  • Symmetric share sets including the subpath entrypoints (react/jsx-runtime, react/jsx-dev-runtime, react-dom/client) — the root cause of duplicate React in this setup
  • import: false on the remote's shares, conditional on federated mode
  • A var container (varFilename) registered by the host with type: "global", because an ESM container self-initializes its share scope before the host can hand over its own
  • Serving the built output — the Vite dev server injects a Node-only module into the entry

The full reasoning for each, with the failure mode you get without it, is in cross-bundler settings.

Two ports on purpose

dev:standalone runs on 3412, not 3402, so it can't be mistaken for the preview server the shell consumes. Point the shell at the standalone dev server and you get $RefreshSig$ is not defined — that mode enables @vitejs/plugin-react, whose react-refresh instrumentation needs a Vite-served page.

Running host + plugin together#

Install once at the repo root

pnpm install
pnpm build          # builds the packages the examples consume

Start the plugin

pnpm --filter @z0devs/basic-plugin dev

It serves http://localhost:3401/remoteEntry.js with Access-Control-Allow-Origin: *.

Start the shell

pnpm --filter @z0devs/basic-shell dev

public/mfes.json already lists the plugin. Open http://localhost:3400.

Confirm the wiring

The nav shows Basic Plugin; /basic-plugin renders the federated page with the user's email from the host's useAuth; the onboarding overlay appears until you flip FAKE_USER.onboarded in src/auth/fakeAuth.ts.

Using them as a starting point#

The plugin example is the same source create-z0-plugin ships as its template — so pnpm create z0-plugin my-plugin gets you basic-plugin with your ports and metadata already substituted. For the host, copy basic-shell's four files and replace fakeAuth with your real auth adapter and the demo values with your own.

Next#