A proof of concept showing Module Federation working across Webpack, Rspack, and Vite 8 — with each producer on a different bundler and the consumer not caring how they were built.
┌──────────────────┐
│ portal-shell │
│ MF v1.5 consumer │
│ rspack │
│ :3000 │
└────────┬─────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ app-counter │ │ app-table │ │ app-people-list │
│ MF v1 producer │ │ MF v2 producer │ │ MF v2 producer │
│ webpack │ │ rspack │ │ vite │
│ (legacy) │ │ (migrated) │ │ (migrated) │
│ :3002 │ │ :3001 │ │ :3003 │
└─────────────────┘ └─────────────────┘ └──────────────────┘
┌──────────────────┐
┌──────────────────┐
│ portal-shell-ssr │ (experimental — MFE loading
│ MF v2 consumer │ broken in dev, see docs)
│ SSR via RR7 │
│ :4000 │
└──────────────────┘
Three producers, three different bundlers, one consumer that fetches remoteEntry.js over HTTP regardless of how it was built. This demonstrates:
- Cross-bundler federation — webpack, rspack, and vite producers all consumed by an rspack consumer using native MF v1.5
- v1/v2 coexistence —
app-counteris still on native webpack MF v1, the consumer handles both v1 and v2 producers - SSR prototype —
portal-shell-ssrdemonstrates the architecture (SSR shell chrome + client-only MFE islands) but MFE loading is broken in dev due to Vite's lack of__webpack_share_scopes__support
| Tool | Purpose |
|---|---|
| Module Federation v2 | Micro-frontend runtime (cross-bundler) |
| Rspack | Consumer bundler + producer (app-table) |
| Webpack 5 | Legacy producer bundler (app-counter) |
| Vite 8 | Producer bundler (app-people-list) + SSR consumer |
| SWC | Transpiler (replaces Babel) |
| React 19 | UI framework |
| React Router 7 | Routing (SSR consumer uses framework mode) |
| Turborepo | Monorepo task runner |
| Vitest | Test runner |
| oxlint + oxfmt | Linter + formatter |
- Node.js (see
.nvmrc— currently v24) - npm
npm installnpm run dev # SPA consumer (:3000) + all producers
npm run dev:ssr # SSR consumer (:4000) + all producers (experimental)dev starts:
- http://localhost:3000 — SPA consumer (rspack, MF v1.5)
:3001— app-table (rspack producer):3002— app-counter (webpack producer):3003— app-people-list (vite producer, build+preview)
Navigate to /table, /counter, /people.
npm run build # All packages (each uses its own bundler)npm test # Vitest
npx oxlint . # LintEach v2 producer has a federation.config.js with the bundler-agnostic MF config (name, exposes, shared deps). The consumer just fetches remoteEntry.js files over HTTP — it doesn't care which bundler produced them.
The v2 runtime from @module-federation/enhanced negotiates shared dependencies (React, react-dom) regardless of bundler. The v1 producer (counter) produces a standard remoteEntry.js that the v2 consumer understands natively.
The SSR consumer (portal-shell-ssr) uses React Router 7 in framework mode. The shell chrome (nav, header, layout) is server-rendered for fast first paint. Federated producers load as client-only islands — the server renders a loading placeholder, then the MF runtime loads the producer's component after hydration.
This uses @module-federation/runtime directly (not a build plugin) to avoid conflicts with React Router 7's Vite plugin.
See docs/COMPATIBILITY.md for the full guide covering shared module configs, migration paths, and cross-bundler interop.