Module Federation for Vite & Rollup — with true dev-mode HMR
A maintained, modernised fork of originjs/vite-plugin-federation —
rebuilt for Vite 8+, Rolldown, Node 22+, and true dev-mode federation with React Fast Refresh.
This fork extends the original plugin with capabilities that didn't exist before — most notably, full dev-mode federation where both HOST and REMOTE run Vite dev servers, with instant cross-origin React Fast Refresh.
The original plugin required the remote side to run vite build (or vite build --watch) even during development. This fork introduces a dev expose server — the remote's Vite dev server serves remoteEntry.js and exposed modules directly via middleware, with full CORS support.
- No build step needed for the remote during development
- Shared modules (React, Redux, etc.) are bridged via CJS shim files generated at startup
- Shims use
resolve.aliaswith exact regex matching soreactdoesn't matchreact-dom - ESM packages that can't be enumerated in Node.js (e.g. packages referencing
windowat top level) get automatic CJS fallback shims - The host's share scope provides module instances via
import()by bare specifier, with intelligent unwrapping for CJS-only deps
This is the headline feature. When a remote MFE file changes, the update appears instantly in the host SPA — no page reload, full React Fast Refresh with state preservation.
How it works under the hood:
-
Re-export stubs — Exposed modules are served as thin re-export stubs (
export * from '/src/index.ts') instead of transformed snapshots. The browser follows the import to the real source file on the remote's Vite dev server, which Vite tracks in its module graph. -
Patched
@vite/client— The remote's/@vite/clientis intercepted and itsbasevariable is patched from"/"to the absolute remote origin (e.g."http://localhost:6001/"). This ensures HMR module re-imports resolve to the remote dev server, not the host page origin. -
Shared React Refresh runtime — The host's
/@react-refreshstores itself as a global singleton. The remote's/@react-refreshdetects this and re-exports the host's singleton, ensuring all component families, mounted roots, and renderer references are tracked in one place. In standalone mode, the remote's own runtime is used instead.
The result: editing a component in the remote MFE triggers React Fast Refresh in the host SPA — often faster than standalone mode, because Fast Refresh only re-renders the changed component leaf without reconciling the full provider tree.
Fixes top-level await deadlocks that occur with Rolldown's code-splitting. When multiple async chunks depend on each other through shared federation modules, the original plugin could produce circular TLA dependencies. This fork restructures the async resolution to prevent deadlocks.
- Node.js 22+ minimum (dropped legacy Node support)
- Vite 8+ and Rolldown support
- Updated all dependencies to latest versions
- Fixed CI pipeline for modern Node.js and npm
npm install @hugs7/vite-plugin-federation --save-dev// vite.config.js
import federation from '@hugs7/vite-plugin-federation';
export default {
plugins: [
federation({
name: 'remote-app',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button.vue',
},
shared: ['vue'],
}),
],
};// vite.config.js
import federation from '@hugs7/vite-plugin-federation';
export default {
plugins: [
federation({
name: 'host-app',
remotes: {
remote_app: 'http://localhost:5001/assets/remoteEntry.js',
},
shared: ['vue'],
}),
],
};// Vue
const RemoteButton = defineAsyncComponent(() => import('remote_app/Button'));
// React
const RemoteButton = React.lazy(() => import('remote_app/Button'));Both host and remote run vite dev. The remote serves its exposed modules directly from its dev server — no build step required.
// Remote vite.config.js — just run `vite dev`
federation({
name: 'my-mfe',
filename: 'remoteEntry.js',
exposes: {
'./pages': './src/index.ts',
},
shared: ['react', 'react-dom', 'react-redux'],
});// Host vite.config.js — also `vite dev`
federation({
name: 'my-spa',
remotes: {
'my-mfe': {
external: 'http://localhost:6001/remoteEntry.js',
format: 'esm',
from: 'vite',
},
},
shared: ['react', 'react-dom', 'react-redux'],
});Edit a component in the remote → it updates instantly in the host via React Fast Refresh. ⚡
The plugin generates CJS bridge shims in node_modules/.federation-shims/ for each shared module. These shims check globalThis.__federation_shared_modules__ at runtime:
- If the host has provided the module (federation mode) → use the host's instance
- If not (standalone mode) →
require()the local copy
This ensures singleton guarantees for React, Redux contexts, and other shared state — the same instance is used by both host and remote.
# Build the plugin
pnpm build
# Link it
cd packages/lib && npm link
cd /path/to/your-mfe && npm link @hugs7/vite-plugin-federation
cd /path/to/your-spa && npm link @hugs7/vite-plugin-federation
# Clear caches before restarting dev servers
rm -rf node_modules/.vite node_modules/.federation-shimsRequired. The module name of the remote.
Entry file of the remote module. Default: remoteEntry.js
Components exposed by the remote:
exposes: {
// Basic
'./Button': './src/Button.vue',
// With options
'./Button': {
import: './src/Button.vue',
name: 'button',
dontAppendStylesToHead: true,
},
}Remote modules consumed by the host:
remotes: {
// Basic
remote_app: 'http://localhost:5001/assets/remoteEntry.js',
// With options
remote_app: {
external: 'http://localhost:5001/assets/remoteEntry.js',
format: 'esm', // 'esm' | 'var' | 'systemjs'
from: 'vite', // 'vite' | 'webpack'
},
}Dependencies shared between host and remote:
// Simple
shared: ['vue', 'pinia']
// With version control
shared: {
vue: { version: '3.x', requiredVersion: '^3.0.0' },
react: { singleton: true },
}Additional file types for the plugin to process. Defaults: ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.vue', '.svelte']
This plugin is compatible with Webpack Module Federation. You can consume Webpack-exposed modules in Vite or vice versa.
remotes: {
webpack_app: {
external: 'http://localhost:5001/remoteEntry.js',
format: 'var',
from: 'webpack',
},
}
⚠️ Mixing Vite and Webpack in React projects is not recommended due to differences in how they bundle CommonJS modules.
Add remotes dynamically at runtime via virtual:__federation__:
import {
__federation_method_setRemote,
__federation_method_getRemote,
__federation_method_unwrapDefault,
} from 'virtual:__federation__';
// Register a remote at runtime
__federation_method_setRemote('remote_app', {
url: 'http://localhost:5001/assets/remoteEntry.js',
format: 'esm',
from: 'vite',
});
// Load a module
const module = await __federation_method_getRemote('remote_app', './Button');
const Button = __federation_method_unwrapDefault(module);TypeScript declarations
declare module 'virtual:__federation__' {
interface IRemoteConfig {
url: (() => Promise<string>) | string;
format: 'esm' | 'systemjs' | 'var';
from: 'vite' | 'webpack';
}
export function __federation_method_setRemote(name: string, config: IRemoteConfig): void;
export function __federation_method_getRemote(name: string, exposedPath: string): Promise<unknown>;
export function __federation_method_unwrapDefault(module: unknown): unknown;
export function __federation_method_wrapDefault(module: unknown, need: boolean): unknown;
export function __federation_method_ensure(remoteName: string): Promise<unknown>;
}Set build.target to esnext:
build: {
target: 'esnext',
}Explicitly declare server.host and server.port in the remote's Vite config to ensure the plugin can resolve dependency addresses correctly.
Add a declaration file:
declare module 'remote_app/*' {}This project is a fork of originjs/vite-plugin-federation, originally created by the Origin.js team under the Mulan PSL v2 license. Their foundational work on Module Federation for Vite made this project possible.
This fork (@hugs7/vite-plugin-federation) extends the original with dev-mode remote serving, cross-origin React Fast Refresh, Rolldown compatibility, and modern Node.js support.
Made with ☕ by @hugs7