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-pluginYou're prompted for:
| Prompt | Used for |
|---|---|
| Dev port | The plugin's dev server and output.publicPath |
| Host shell port | The remotes: { shell: … } entry the bridge imports |
| Vendor name | manifest-entry.json → vendor |
| Description | manifest-entry.json → description |
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:3500What 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.mdThe 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#
| Script | What it does |
|---|---|
pnpm dev | Dev server, federated mode — the bridge imports shell/sharedStore |
pnpm dev:standalone | IS_STANDALONE=true — the bridge uses LocalContext |
pnpm build | Production bundle, dist/remoteEntry.js + dist/@mf-types |
pnpm typecheck | tsc --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.