lunx.docs
DocsTutorialsModule Federation

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

typescriptshell/lunx.config.ts
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

typescriptdashboard/lunx.config.ts
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.

typescriptshell/src/App.tsx
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.

jsonshell/tsconfig.json
{
  "compilerOptions": {
    "paths": { "*": ["./@mf-types/*"] }
  }
}
typescriptshell/src/App.tsx
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.js is an ES module exporting the webpack 5 container API (init, get). An mf-manifest.json lists the exposed modules, shared versions and CSS. Remotes can be lunx containers or webpack containers (name@url with 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

FieldTypeDefaultDescription
singletonbooleanfalseOnly one version is loaded across all apps.
requiredVersionstring | falsepackage.json rangeSemver range this app accepts; false accepts any.
strictVersionbooleanfalseFail instead of warning when no shared version satisfies requiredVersion.
eagerbooleanfalseBundle 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).

typescriptlunx.config.ts
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().

javascriptwebpack.config.js
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.

typescriptshell/lunx.config.ts
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'
      }`,
    },
  },
}))
Independent deployments
Each remote app has its own CI/CD pipeline and deploys independently. Teams own their micro-frontend end-to-end. The host app loads the latest version automatically — no coordinated release needed.
Singleton shared modules
Always mark 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.