A host and its remotes do not have to share a bundler. A Vite-built remote loads under an Rspack
host, and it is verified end to end in examples/vite-plugin.
It needs four non-default settings, plus one plausible-looking setting you must avoid. Skipping any of them produces a blank page rather than an error, so it is worth doing all four before debugging.
Prerequisite
Everything here assumes the subpath entrypoints are already shared on every participant, as in
Installation.
That is the requirement most likely to be the actual cause: an unshared react/jsx-runtime sends
the Vite remote to its own React, and two Reacts fail in ways that look nothing like a config
problem.
The Vite remote sets `import: false` — but only when federated
import { z0Federation } from "@z0devs/mf-config";
const IS_STANDALONE = process.env["IS_STANDALONE"] === "true";
federation(
z0Federation({
target: "vite",
name: "vite_plugin",
exposes: { "./init": "./src/init.tsx" },
shellEntry: SHELL_ENTRY,
router: true,
standalone: IS_STANDALONE, // gates `import: false`
}),
);target: "vite" emits all four interdependent settings — the swapped filename/varFilename container
pair, manifest: true, the { type: "var" } shell remote, and import: false on the shares unless
standalone. Equivalent to writing this by hand:
const sharedLib = { singleton: true, ...(IS_STANDALONE ? {} : { import: false as const }) };
const shared = { react: sharedLib, "react-dom": sharedLib, "react-router-dom": sharedLib };Note import: false is applied only to the React set and react-router-dom — never to arbitrary extras,
because a consume-only share the host does not provide hard-fails at load with
Shared module '<pkg>' must be provided by host.
"No local fallback — the host must provide these." This is the one place Vite genuinely differs from
Rspack: an Rspack remote that ships a fallback still resolves the host's copy at runtime, while
@module-federation/vite binds its own fallback even when the host's is in the share scope and
unified. Removing the fallback is what forces it onto the host's React — and it drops the dead
weight too (the react-router-dom share chunk went 89 kB → 2.5 kB).
It must be conditional: standalone has no host to provide the shares, so leaving import: false on
parks an unresolved entry in pendingShareLoads, which the entry bootstrap awaits before importing
your app. Blank page, no error.
Emit a `var` container and register that one
federation({ filename: "esmRemoteEntry.js", varFilename: "remoteEntry.js", manifest: true });{ "service": { "id": "vite_plugin", "remoteEntry": ".../remoteEntry.js", "type": "global" } }An ESM container evaluates its own module body on import(), so it self-initializes its share
scope before a foreign host can hand over its own. A Webpack/Rspack container is inert until the host
calls init(shareScope) — which is why those remotes never hit this. The var entry is a ~600-byte
shim that dynamically imports the real ESM container, so you still ship exactly one container.
type: "global" on the manifest entry is what tells the host to treat it as a var container. Point
the URL at remoteEntry.js, not esmRemoteEntry.js or mf-manifest.json.
Serve the built output, never the Vite dev server
The dev server injects @module-federation/dts-plugin's type-hints module into remoteEntry.js.
That's Node code — it references module — so a foreign host's container init dies with
RUNTIME-015: module is not defined. Use vite preview (or any static host) for the federated
entry, and a separate port for dev:standalone.
Keep the share sets symmetric
Whatever you share on the remote, share on the host, and vice versa. A subpath shared on only one side is worse than sharing it nowhere: the two sides disagree about who provides React, and which one wins depends on load order.
Don't set shareStrategy: 'loaded-first'
Combined with import: false it lets the Vite remote win provider status for react-dom — which it
has just declared it cannot provide — breaking the other remotes instead.
Why a federated build looks blank on its own port#
Opening the Vite remote's own URL shows nothing, and that is correct. A federated build exposes
modules for a host to consume; it has no app to mount. Use dev:standalone on its own port when you
want to look at the plugin's UI directly — see
Standalone development.