Module Federation
Build scalable micro-frontend architectures with Lunx's first-class Module Federation support. Share code, components, and state across independently deployed applications.
What is Module Federation?
Module Federation (MFE) allows multiple independently-deployed JavaScript applications to share code at runtime. With Lunx, you declare which modules each application exposes and which remotes it consumes — the Rust bundler handles everything else.
- Shell / Host app — the main application that loads remote modules
- Remote app — independently deployed app that exposes components or utilities
- Shared modules — singletons like React that must have only one instance at runtime
Setting up a Host application
import { defineConfig } from 'lunx-dev' export default defineConfig({ framework: 'react', server: { port: 3000 }, federation: { name: 'shell', remotes: { // Name: 'remoteName@remoteEntry URL' auth: 'auth@http://localhost:3001/remoteEntry.js', dashboard: 'dashboard@http://localhost:3002/remoteEntry.js', profile: 'profile@http://localhost:3003/remoteEntry.js', }, shared: { react: { singleton: true, requiredVersion: '^18.0.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.0.0' }, 'react-router-dom': { singleton: true }, }, }, })
Setting up a Remote application
import { defineConfig } from 'lunx-dev' export default defineConfig({ framework: 'react', server: { port: 3002 }, federation: { name: 'dashboard', filename: 'remoteEntry.js', // entry point consumed by host exposes: { // './LocalName': './path/to/component' './Widget': './src/components/Widget.tsx', './StatCard': './src/components/StatCard.tsx', './useStats': './src/hooks/useStats.ts', }, shared: { react: { singleton: true, requiredVersion: '^18.0.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.0.0' }, }, }, })
Consuming a remote component
Use React.lazy to import remote components. Lunx resolves the dashboard/Widget import to the configured remote URL at build time.
import { lazy, Suspense } from 'react' // These are loaded from the remote application at runtime const DashboardWidget = lazy(() => import('dashboard/Widget')) const DashboardStatCard = lazy(() => import('dashboard/StatCard')) export default function App() { return ( <div className="p-8"> <h1>Shell Application</h1> <Suspense fallback={<div>Loading dashboard...</div>}> <DashboardWidget title="Revenue" value="$124,340" /> <DashboardStatCard metric="Active Users" count={8_291} /> </Suspense> </div> ) }
TypeScript type sharing
Types cross app boundaries in @module-federation's @mf-typesformat, so lunx, webpack and Rspack apps read each other's. A TypeScript remote's build writes @mf-types.zip next to remoteEntry.js (its dev server serves it too); a host downloads each remote's into @mf-types/<remote>/ when it starts or builds. Nothing to configure on either side.
{ "compilerOptions": { "paths": { "*": ["./@mf-types/*"] } } }
import Widget from 'dashboard/Widget' // typed from the remote's @mf-types.zip import type { WidgetProps } from 'dashboard/Widget'
federation: { dts: false } turns it off; { generateTypes: false } or { consumeTypes: false } turns off one side.
How it works
remoteEntry.jsis an ES module exporting the webpack 5 container API (init,get). Anmf-manifest.jsonlists the exposed modules, shared versions and CSS. Remotes can be lunx containers or webpack containers (name@urlwith a global).- Shared packages go through a webpack-format share scope. Each app's own copy is a separate chunk, downloaded only if the scope picks it, so a remote that uses the host's React never fetches its own.
- Dev and built apps mix freely: a dev host can load a built remote and the other way round. In dev, editing a remote fast-refreshes it inside the host page, with state kept.
- Both static (
import Button from 'remote/Button') and lazy (import('remote/Button')) imports of remote modules work.
Shared options
| Field | Type | Default | Description |
|---|---|---|---|
| singleton | boolean | false | Only one version is loaded across all apps. |
| requiredVersion | string | false | package.json range | Semver range this app accepts; false accepts any. |
| strictVersion | boolean | false | Fail instead of warning when no shared version satisfies requiredVersion. |
| eager | boolean | false | Bundle this app's copy into the entry instead of a lazy chunk. |
Mixing lunx with webpack and Rspack
lunx apps federate with webpack 5 and Rspack apps in both directions, with shared singletons working across all of them. npm run test:mf-interop checks every pairing, and a shell mixing all three, in Chromium.
A lunx host with a webpack / Rspack remote: give the remote as they write it, name@url (a script that sets a global).
federation: { name: 'shell', remotes: { checkout: 'checkout@https://checkout.example.com/remoteEntry.js', // webpack or Rspack profile: 'https://profile.example.com/remoteEntry.js', // lunx }, shared: { react: { singleton: true }, 'react-dom': { singleton: true } }, }
A webpack / Rspack host with a lunx remote: lunx's remoteEntry.js is an ES module, so the host loads it with import(): remoteType: 'import', or an import prefix on that remote when the others are script remotes. webpack also needs a target that has import().
module.exports = { target: ['web', 'es2020'], // webpack only: its default target assumes no import() plugins: [ new ModuleFederationPlugin({ name: 'shell', remotes: { profile: 'import https://profile.example.com/remoteEntry.js', // lunx checkout: 'checkout@https://checkout.example.com/remoteEntry.js', // webpack / Rspack }, shared: { react: { singleton: true }, 'react-dom': { singleton: true } }, }), ], }
Production deployment
Each micro-frontend is deployed independently. The host application only needs the URL of each remote's remoteEntry.js — update a remote without redeploying the host.
export default defineConfig(({ mode }) => ({ framework: 'react', federation: { name: 'shell', remotes: { // Use environment variables for production URLs dashboard: `dashboard@${ mode === 'production' ? 'https://dashboard.example.com/remoteEntry.js' : 'http://localhost:3002/remoteEntry.js' }`, }, }, }))
react, react-dom, and routing libraries as { singleton: true }. Multiple React instances cause context failures and hooks to behave incorrectly. Lunx will warn you at startup if duplicates are detected.