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

CLI — create-z0-plugin

Scaffold a runnable federated plugin, what lands on disk, and how to customise it afterwards.

create-z0-plugin generates a working Module Federation remote from the basic-plugin template: Rspack config with @module-federation/enhanced, a store bridge, a fail-loud standalone stub, a manifest entry and DTS generation.

The scaffolder itself is public and MIT-licensed on npmjs — the commands below work with no registry setup and no token. The @z0devs/* packages the generated project depends on are the gated part: installing them needs beta access (request it here), and the scaffold writes the .npmrc that routes them to the right registry for you.

pnpm create z0-plugin my-plugin

You're prompted for:

PromptUsed for
Dev portThe plugin's dev server and output.publicPath
Host shell portThe remotes: { shell: … } entry the bridge imports
Vendor namemanifest-entry.jsonvendor
Descriptionmanifest-entry.jsondescription

When stdin isn't a terminal (CI, Docker, piped scripts) the CLI skips the prompts and uses the defaults shown — pass the plugin name as the argument.

Then:

cd my-plugin
pnpm install            # needs Z0_NPM_TOKEN set — see the note above
pnpm dev:standalone     # → http://localhost:3500

What you get#

my-plugin/
├── package.json              # @z0devs/mf-plugin-kit + @z0devs/mf-registry + rspack + react
├── .npmrc                    # routes @z0devs/* to ZoneZero's registry; token from ${Z0_NPM_TOKEN}
├── rspack.config.ts          # z0Federation() config on your port; exposes every page below
├── tsconfig.json
├── manifest-entry.json       # the host appends this to its mfes.json
├── public/
│   └── env-config.js         # per-environment config the host loads for this remote
├── src/
│   ├── main.tsx              # standalone entry (unused under the host)
│   ├── init.tsx              # registers routes, nav, settings, a flag-gated page + a shared component
│   ├── HomePage.tsx          # your federated page
│   ├── BetaPage.tsx          # flag-gated route (visible when the host activates the "beta" flag)
│   ├── PluginSettings.tsx    # settings-registry contribution
│   ├── SalesWidget.tsx       # component published for OTHER plugins to render
│   ├── env.d.ts
│   ├── index.html
│   └── core/
│       ├── LocalContext.ts   # fail-loud standalone stub
│       └── useShared.tsx     # createPluginStoreBridge wire-up
└── README.md

The generated rspack.config.ts already includes the three things people forget:

import { z0Federation } from "@z0devs/mf-config";
 
const plugins = [
  // 1. The whole federation block from `z0Federation`, so the share map is right by construction —
  //    including the react/jsx-runtime, react/jsx-dev-runtime and react-dom/client SUBPATHS, which every
  //    participant must agree on. Sharing only the package roots is the classic cause of
  //    "Cannot read properties of null (reading 'useEffect')". `z0Federation` also emits the `dts` block
  //    below, so cross-remote types are generated for consumers.
  new ModuleFederationPlugin(
    z0Federation({
      name: "my_plugin",
      exposes: { "./init": "./src/init.tsx", "./HomePage": "./src/HomePage.tsx" },
      shellEntry: SHELL_ENTRY,
      router: true,
    }),
  ),
 
  // 2. The standalone flag inlined (process doesn't exist in a browser)
  new DefinePlugin({
    "process.env.IS_STANDALONE": JSON.stringify(process.env["IS_STANDALONE"] ?? "false"),
  }),
];
 
// 3. …which expands the types block to:
const generatedDts = {
  generateTypes: { tsConfigPath: "./tsconfig.json", abortOnError: false },
  consumeTypes: false,
};

Scripts#

ScriptWhat it does
pnpm devDev server, federated mode — the bridge imports shell/sharedStore
pnpm dev:standaloneIS_STANDALONE=true — the bridge uses LocalContext
pnpm buildProduction bundle, dist/remoteEntry.js + dist/@mf-types
pnpm typechecktsc --noEmit

First customisations#

Point the stub at the real host contract

Replace the hand-written LocalShellShape in src/core/LocalContext.ts with your host's actual store type:

import type useShellSharedStore from "shell/sharedStore";
import type { SelectSharedStoreSlice } from "@z0devs/mf-plugin-kit";
 
type Slice = SelectSharedStoreSlice<
  ReturnType<typeof useShellSharedStore>,
  { values: { useAuth: true }; registries: { components: true } }
>;

Expose more pages

Add entries to exposes in rspack.config.ts, then register their routes in src/init.tsx.

Fill in the manifest entry

Set a real remoteEntry URL for each environment, plus version, shellApiVersion, permissions and — for partner-hosted remotes — remoteEntryHash.

Wire the host's published types

Point tsconfig paths at the shell's emitted @mf-types so import type … from "shell/sharedStore" resolves to the real thing.

Next#