Skip to content

jskits/vite-plugin-federation

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

157 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vite-plugin-federation

CI npm license

vite-plugin-federation - A Vite/Rollup plugin for Module Federation.


Table of Contents


What is Module Federation?

Module Federation lets one frontend application load code from another frontend at runtime. Each frontend can be built and deployed independently.

A typical setup has:

  • a host or shell app, which owns the page and routing
  • one or more remotes, which expose components, routes, or modules
  • shared dependencies such as React, Vue, or design-system packages, negotiated at runtime

This is useful when multiple teams need to ship parts of the same product independently. Each team can update its own remote without rebuilding and redeploying one large frontend every time.

Use this plugin when you want Vite apps to participate in that architecture. It lets you build remotes, consume remotes from a host, and share dependencies safely. It also loads remote manifests in production.

You probably do not need Module Federation if all code is released together in one app. A normal npm package or Vite's built-in code splitting may be enough for simpler cases.


Why this plugin

Need What this plugin gives you
Vite 5 / 6 / 7 / 8 + Rolldown in one plugin First-class compiler adapter, including the Vite 8 module-preload helper rewrite and rolldownOptions handling.
Manifest-first remote loading Builds emit mf-manifest.json + mf-stats.json + mf-debug.json. Hosts consume via curated vite-plugin-federation/runtime helpers.
Real dev HMR for remotes Sidecar dev runtime with classified updates when dev.remoteHmr: true: partial, style, types, full.
SSR (Node) Dedicated ssrRemoteEntry, createServerFederationInstance, asset-preload collection for streaming HTML.
Production-grade host loader Cache TTL, staleWhileRevalidate, retries with jitter, timeouts, fallback URLs, circuit breaker, request collapsing.
Multi-tenant createFederationRuntimeScope(runtimeKey) for isolated manifest cache, breaker, and debug records per tenant on a single page.
Security SRI / SHA-256 integrity verification (multi-mode), private/authenticated manifests, CSP & Trusted Types guidance, and a signed-manifest recipe.
Observability getFederationDebugInfo(), telemetry hooks, dev devtools panel, stable error codes (MFV-001MFV-007).
OriginJS migration shim virtual:__federation__ and __federation_method_* shim enabled by default for common OriginJS migration paths.

A side-by-side feature table against @module-federation/vite and @originjs/vite-plugin-federation lives in COMPARISON.md.

GA Support Scope

vite-plugin-federation 1.0 is generally available for manifest-first Vite remotes, browser hosts, Node SSR hosts, DTS generation and consumption, dev remote HMR, and the curated runtime APIs. Webpack/SystemJS/var remotes remain compatibility paths covered by e2e and should still be validated in each migration. Signed manifests are a documented supply-chain pattern; signature verification is intentionally provided through a custom fetch wrapper rather than built into the default runtime.


Compared to Other Vite Federation Plugins

See COMPARISON.md for the full feature matrix, decision guide, and citations against:

Migration cheatsheets:


Install

pnpm add -D vite-plugin-federation
# or
npm install -D vite-plugin-federation
# or
yarn add -D vite-plugin-federation

Peer dependency: vite@^5 || ^6 || ^7 || ^8 (Rolldown supported). Node: >=20.19.0.

The plugin exact-pins @module-federation/runtime@2.3.3, @module-federation/sdk@2.3.3, and @module-federation/dts-plugin@2.3.3. It also aliases all @module-federation/runtime imports to a single bridge so federation state stays shared across consumers in your app.


Public API Contract

The v1.x compatibility contract covers the plugin option surface, vite-plugin-federation/runtime exports, manifest schema 1.0.0, devtools contract 1.0.0, MFV-* error-code meanings, Node >=20.19.0, and Vite ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0.

Full policy: docs/public-api-contract.md.


Quick Start

A remote

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from 'vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'catalog',
      filename: 'remoteEntry.js', // stable name for examples; default is remoteEntry-[hash]
      exposes: {
        './Button': './src/Button.tsx',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^19.2.4' },
        'react/': { singleton: true, requiredVersion: '^19.2.4' },
      },
      // manifest defaults to true → emits mf-manifest.json + mf-stats.json + mf-debug.json
    }),
  ],
  build: {
    target: 'esnext',
    cssCodeSplit: true,
  },
});

A host

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from 'vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'shell',
      remotes: {
        // String value → manifest-first remote (recommended)
        catalog: 'https://cdn.example.com/catalog/mf-manifest.json',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^19.2.4' },
      },
    }),
  ],
  build: { target: 'esnext' },
});

Load a remote at runtime:

// app.tsx
import { lazy, Suspense } from 'react';
import { loadRemoteFromManifest } from 'vite-plugin-federation/runtime';

const CatalogButton = lazy(async () => {
  const mod = await loadRemoteFromManifest(
    'catalog/Button',
    'https://cdn.example.com/catalog/mf-manifest.json',
    { cacheTtl: 30_000, retries: 2, timeout: 4_000 },
  );
  return { default: mod.default ?? mod };
});

export function App() {
  return (
    <Suspense fallback={null}>
      <CatalogButton />
    </Suspense>
  );
}

If you prefer the classic loadRemote('catalog/Button') style, that works too:

import { loadRemote } from 'vite-plugin-federation/runtime';
const mod = await loadRemote('catalog/Button');

An SSR host

// vite.config.ts
import federation from 'vite-plugin-federation';

export default {
  plugins: [
    federation({
      name: 'ssr-shell',
      remotes: { catalog: 'https://cdn.example.com/catalog/mf-manifest.json' },
      target: 'node', // optional; auto-detected from `build.ssr`
    }),
  ],
};
// server.ts
import {
  createServerFederationInstance,
  fetchFederationManifest,
  loadRemoteFromManifest,
  collectFederationManifestPreloadLinks,
} from 'vite-plugin-federation/runtime';

createServerFederationInstance({ name: 'ssr-shell', remotes: [], shared: {} });

const manifestUrl = process.env.CATALOG_MANIFEST_URL!;
const manifest = await fetchFederationManifest(manifestUrl, { cacheTtl: 30_000 });

const mod = await loadRemoteFromManifest('catalog/Button', manifestUrl, {
  target: 'node',
});

// Stream <link rel="modulepreload"> / <link rel="stylesheet"> hints
const links = collectFederationManifestPreloadLinks(manifestUrl, manifest, './Button');

Node must currently run with --experimental-vm-modules because the underlying MF runtime uses the VM module loader for ESM remote evaluation. See docs/production-runtime.md.


Plugin Options

The full reference lives in docs/plugin-api.md. The most-used options:

Option Type Default Notes
name string required Public application/container name.
filename string remoteEntry-[hash] Browser remote entry filename.
varFilename string Emits an additional var-style entry for legacy hosts.
exposes Record<string, string | ExposeConfig> {} Remote expose map.
remotes Record<string, string | RemoteObjectConfig> {} Manifest URLs or object configs.
shared string[] | Record<string, SharedConfig> {} Shared providers/consumers.
manifest boolean | PluginManifestOptions true Emits mf-manifest.json + mf-stats.json + mf-debug.json.
dts boolean | PluginDtsOptions auto for TS projects Type generation / consumption; set false to disable.
dev boolean | PluginDevOptions enabled Devtools and type hints default on; remote HMR is opt-in.
compat boolean | CompatibilityOptions true OriginJS virtual:__federation__ shim.
shareStrategy 'loaded-first' | 'version-first' 'version-first' Shared provider selection.
shareScope string 'default' Default share scope.
publicPath string Vite base or auto Public path for manifest asset URLs.
bundleAllCSS boolean false Add all CSS to every expose entry.
runtimePlugins Array<string | [string, object]> [] Runtime plugin imports.
target 'web' | 'node' from build Forces browser/server output.
hostInitInjectLocation 'entry' | 'html' 'html' Where host runtime init is injected.
moduleParseTimeout number (s) 10 Total module parse budget.
moduleParseIdleTimeout number (s) Idle parse timeout, reset per module.

Exposes

exposes: {
  './Button': './src/Button.tsx',
  './ManualCssButton': {
    import: './src/ManualCssButton.tsx',
    css: { inject: 'manual' }, // 'head' | 'manual' | 'none' | true | false
  },
}

'manual' writes CSS hrefs to the global CSS bucket instead of appending styles to document.head. This is useful for Shadow DOM or staged migration. dontAppendStylesToHead: true is accepted as the OriginJS-compatible alias.

Remotes

remotes: {
  // Recommended: manifest-first
  catalog: 'https://cdn.example.com/catalog/mf-manifest.json',

  // Object form for legacy / mixed setups
  legacy: {
    name: 'legacyRemote',
    entry: 'https://cdn.example.com/legacy/remoteEntry.js',
    type: 'module',
    entryGlobalName: 'legacyRemote',
    from: 'vite',     // 'vite' | 'webpack'
    format: 'esm',    // 'esm' | 'systemjs' | 'var'
    shareScope: 'default',
  },
}

Shared

shared: {
  react: {
    singleton: true,
    strictSingleton: true,
    requiredVersion: '^19.2.4',
    strictVersion: true,
    allowNodeModulesSuffixMatch: true, // pnpm/symlinked layouts
  },
  'react/': { // trailing slash → matches package subpaths
    singleton: true,
    requiredVersion: '^19.2.4',
    allowNodeModulesSuffixMatch: true,
  },
  // Host-only share — host must provide; remote must not bundle a fallback
  'design-system': { import: false, requiredVersion: '^2.0.0' },
}

Runtime API

Imported from vite-plugin-federation/runtime. Full reference: docs/runtime-api.md.

import {
  // Instances
  createFederationInstance,
  createServerFederationInstance,
  createFederationRuntimeScope,

  // Manifest
  fetchFederationManifest,
  registerManifestRemote,
  registerManifestRemotes,
  loadRemoteFromManifest,
  refreshRemote,

  // Loading
  loadRemote,
  loadShare,
  loadShareSync,

  // Preload & warmup
  warmFederationRemotes,
  preloadRemote,
  createFederationManifestPreloadPlan,
  collectFederationManifestPreloadLinks,

  // Security
  verifyFederationManifestAssets,

  // Observability
  getFederationDebugInfo,
  clearFederationRuntimeCaches,

  // Re-exports of @module-federation/runtime
  registerPlugins,
  registerRemotes,
  registerShared,
} from 'vite-plugin-federation/runtime';

The most useful production helper is loadRemoteFromManifest(). It fetches and validates the manifest, picks the correct entry for the target (web or node), optionally verifies integrity, registers the remote with the MF runtime, and loads the expose. The helper also applies cache TTL, retries, and circuit breaker support.


Manifest Protocol

Every manifest-enabled build emits three artifacts under the configured manifest.filePath:

Artifact Purpose
mf-manifest.json Primary contract: name, version, exposes, shared, remotes, build metadata, optional ssrRemoteEntry, optional integrity / content hashes, optional preload hints.
mf-stats.json Build diagnostics, shared resolution metadata, asset analysis.
mf-debug.json Plugin options snapshot, capability flags, diagnostics. Useful in CI.

Schema and field definitions: docs/manifest-protocol.md. JSON Schemas: docs/schemas/.


Dev Experience

The dev pipeline runs the federation virtual modules through a sidecar runtime. That lets the host see real updates from a remote without a rebuild cycle. See docs/dev-hmr.md for the full strategy table.

federation({
  // ...
  dev: {
    remoteHmr: true, // opt in to host <-> remote HMR wiring
    devtools: true, // default; set false to disable the devtools endpoint/overlay
    disableLiveReload: true, // DTS worker default: avoid page reloads for type-only changes
    disableHotTypesReload: false,
    disableDynamicRemoteTypeHints: false,
  },
});

Update classification:

Update kind What happens
partial server.reloadModule() for the affected expose, falls back to full.
style Refresh the affected stylesheet(s); no reload.
types Sync .d.ts to the host; no reload.
full Full host reload.

Dev endpoints exposed by the dev server:

  • /mf-manifest.json (or your configured manifest.filePath / manifest.fileName)
  • /mf-debug.json (derived from the manifest file name)
  • /remoteEntry.js or the configured filename in dev
  • /__mf_hmr when dev.remoteHmr: true
  • /__mf_devtools unless dev.devtools: false
  • dist/.dev-server.zip and dist/.dev-server.d.ts when dev DTS generation is active

SSR

  • target: 'node' is auto-detected from build.ssr. It emits a separate ssrRemoteEntry.js per remote and tree-shakes browser-only branches via the ENV_TARGET define.
  • The runtime exposes createServerFederationInstance() with inBrowser: false and the Node remote-entry loader. Manifest registration prefers ssrRemoteEntry for node targets. It falls back to remoteEntry if no SSR entry is declared.
  • collectFederationManifestPreloadLinks() returns deduplicated <link> descriptors for streaming HTML. It dedupes modulepreload and stylesheet links across multiple expose hits in the same render pass.
  • Vite's module-preload helper is patched to short-circuit when document / window are absent, so an expose evaluated during SSR cannot crash the renderer.

End-to-end flow and operational tips: docs/production-runtime.md.


TypeScript / DTS

federation({
  // ...
  dts: {
    generateTypes: { abortOnError: true },
    consumeTypes: { typesOnBuild: true, abortOnError: true },
  },
});
  • Wraps @module-federation/dts-plugin@2.3.3.
  • Remote builds emit @mf-types.zip and a per-expose @mf-types.d.ts.
  • Hosts discover type URLs from the manifest (metaData.types.path / .api) with optional remoteTypeUrls override.
  • Dev hosts receive live type updates via the dev WebSocket channel without a page reload.
  • typesOnBuild: true fails the host build if remote types cannot be fetched.

Workflows: docs/dts-workflows.md.


Production Runtime

Manifest-first loading with operational guardrails:

import { loadRemoteFromManifest } from 'vite-plugin-federation/runtime';

const mod = await loadRemoteFromManifest('catalog/Button', catalogManifestUrl, {
  target: 'web', // or 'node'
  cacheTtl: 30_000, // ms; 0 = always refetch
  staleWhileRevalidate: true, // serve cached, refresh in the background
  force: false, // bypass cache for this call
  fallbackUrls: [backupManifestUrl],
  retries: 2,
  retryDelay: 200, // ms, with jitter
  timeout: 4_000, // ms, per attempt
  circuitBreaker: { failureThreshold: 3, cooldownMs: 30_000 },
  integrity: { mode: 'prefer-integrity' }, // or true / { mode: 'integrity' | 'content-hash' | 'both' }
  fetch, // bring your own
  fetchInit: { credentials: 'include' },
  shareScope: 'default',
  hooks: {
    manifestFetch: (e) => log(e),
    remoteRegister: (e) => log(e),
    remoteLoad: (e) => log(e),
    remoteRefresh: (e) => log(e),
  },
});

Concurrent calls for the same manifest URL collapse into a single in-flight fetch. Cached entries record sourceUrl (which fallback URL actually served), latency, integrity mode used, and breaker state for each call.

See docs/production-runtime.md and docs/runtime-api.md for the full option reference.


Multi-Tenant Deployments

If you serve multiple tenants on the same page (per-customer remote sets, A/B variants, etc.), isolate them with a runtime scope:

import { createFederationRuntimeScope } from 'vite-plugin-federation/runtime';

const tenant = createFederationRuntimeScope(tenantId);
await tenant.registerManifestRemote('shop', tenantManifestUrl, { shareScope: tenantId });
const Page = await tenant.loadRemoteFromManifest('shop/Page', tenantManifestUrl);

Each scope owns its own manifest cache, in-flight registrations, circuit breaker, debug records, and load metrics — so a noisy tenant cannot affect any other tenant's failure budget or cache state. See docs/multi-tenant.md.


Preload & Performance

import {
  createFederationManifestPreloadPlan,
  fetchFederationManifest,
  warmFederationRemotes,
} from 'vite-plugin-federation/runtime';

// Warm a set of remotes at app boot. Set preload: false to register only.
await warmFederationRemotes({
  catalog: {
    manifestUrl: catalogManifestUrl,
    preload: { resourceCategory: 'sync' },
  },
  checkout: {
    manifestUrl: checkoutManifestUrl,
    preload: false,
  },
});

// Build a route-aware preload plan
const catalogManifest = await fetchFederationManifest(catalogManifestUrl, { cacheTtl: 30_000 });
const plan = createFederationManifestPreloadPlan(
  catalogManifestUrl,
  catalogManifest,
  { '/product/:id': './Button' },
  {
    asyncChunkPolicy: 'css', // 'none' | 'js' | 'css' | 'all'
  },
);
// plan.links → [{ rel: 'modulepreload', href }, { rel: 'stylesheet', href }, ...]

// Per-remote runtime metrics surface through getFederationDebugInfo()
//   .runtime.remoteLoadMetrics

Background and tuning: docs/preload-performance.md.


Security

Capability Where
Subresource integrity in manifest (SRI + SHA-256) docs/security.md
verifyFederationManifestAssets() (multi-mode: prefer-integrity, integrity, content-hash, both) docs/runtime-api.md
Private / authenticated manifests via fetchInit + custom fetch docs/security.md
Signed manifest workflow (detached signature verified in fetch wrapper) docs/security.md
CSP / Trusted Types guidance (manifest-first ESM, no inline scripts) docs/security.md

OriginJS Compatibility & Migration

The OriginJS virtual:__federation__ API is enabled by default for migration. All five common legacy methods work unchanged:

import {
  __federation_method_setRemote,
  __federation_method_getRemote,
  __federation_method_ensure,
  __federation_method_unwrapDefault,
  __federation_method_wrapDefault,
} from 'virtual:__federation__';

Legacy paths like ./Button are normalized to remote/Button and routed through @module-federation/runtime. For CSS migration, dontAppendStylesToHead: true is preserved as a css.inject: 'manual' synonym. The end-to-end examples/originjs-compat-host validates this against a real remoteEntry.js. It also validates a webpack library.type: 'system' remote.

Compatibility is intentionally scoped: browser CommonJS remotes are not supported, systemjs remotes require globalThis.System.import, and unsupported from values produce MFV-007 warnings.

Turn the shim off when you've fully migrated:

federation({
  // ...
  compat: { originjs: false, virtualFederationShim: false },
});

Migration walkthrough: docs/originjs-migration.md. Compatibility matrix: docs/compatibility-matrix.md.


Examples

The repo ships working examples for the main supported frameworks and production scenarios:

Path What it covers
examples/react-remote & examples/react-host Baseline React remote ↔ host.
examples/react-ssr-host React SSR host consuming a remote.
examples/vue-remote & examples/vue-host Vue 3.
examples/svelte-remote & examples/svelte-host Svelte.
examples/lit-remote & examples/lit-host Lit / web components.
examples/multi-remote-host Single host loading multiple remotes.
examples/workspace-shared-{lib,remote,host} pnpm workspace + symlinked shared dependency.
examples/dts-remote & examples/dts-host DTS auto-generation + host consumption.
examples/shared-host-only-{remote,host} Host-only shares (import: false).
examples/shared-negotiation-{remote,host} Cross-remote shared version negotiation.
examples/shared-strict-fallback-app Strict-singleton fallback policy.
examples/originjs-compat-host virtual:__federation__ shim against real remotes.
examples/webpack-systemjs-remote Vite host consuming a webpack library.type: 'system' remote.

Useful local commands:

pnpm install
pnpm examples:build          # React host/remote + SSR + DTS smoke path
pnpm examples:dev            # React host/remote dev loop
pnpm examples:dts:build
pnpm examples:ssr:build
pnpm build                   # package + all workspaces with build scripts
pnpm --filter vite-plugin-federation test:e2e:browser-matrix
pnpm --filter vite-plugin-federation test:e2e:compat
pnpm --filter vite-plugin-federation test:e2e:shared

Troubleshooting

The plugin uses stable, greppable error codes:

Code Meaning
MFV-001 Plugin configuration error (missing name, etc.)
MFV-002 Alias conflict between a shared key and another resolver.
MFV-003 Shared dependency miss / fallback / strict-singleton conflict.
MFV-004 Manifest fetch / validation failed (with sourceUrl).
MFV-005 Expose missing or unsupported legacy format.
MFV-006 SSR remote entry missing for target: 'node'.
MFV-007 Dynamic import rewrite warning or unsupported legacy from.

Snapshot the runtime in dev:

import { getFederationDebugInfo } from 'vite-plugin-federation/runtime';
console.log(getFederationDebugInfo());
// → instance, registered remotes, manifest cache + fetch timeline,
//   integrity verification log, circuit breaker state, load metrics,
//   shared providers + sharedResolutionGraph

Full guide: docs/troubleshooting.md.


Repository Layout

Path Purpose
packages/vite-plugin-federation/ Published plugin package and runtime implementation.
examples/ Local host/remote apps used by smoke tests and e2e suites.
docs/ API references, production runtime, migration, and operations.
COMPARISON.md Feature matrix against reviewed npm package versions.
.github/workflows/ CI, extended e2e, and release automation.

Development

Requirements: Node ≥ 20.19, pnpm 10.33.0.

pnpm install
pnpm check          # lint + type-check + unit tests for everything
pnpm dev            # turbo dev for the package
pnpm build          # build the package
pnpm lint
pnpm test           # vitest, all workspaces
pnpm format

End-to-end suites (Playwright):

pnpm --filter vite-plugin-federation test:e2e                     # default + compat + shared + dts:dev
pnpm --filter vite-plugin-federation test:e2e:browser-matrix      # Chromium / Firefox / WebKit
pnpm --filter vite-plugin-federation test:e2e:multi-remote
pnpm --filter vite-plugin-federation test:e2e:shared
pnpm --filter vite-plugin-federation test:e2e:ssr
pnpm --filter vite-plugin-federation test:e2e:compat
pnpm --filter vite-plugin-federation test:e2e:dts:dev
pnpm test:vite-matrix:smoke

E2E ports default to the documented local ports. If a local process already owns one of them, set the matching MF_E2E_<NAME>_PORT variable before running the suite, for example MF_E2E_REACT_REMOTE_PORT=5274 pnpm --filter vite-plugin-federation test:e2e:multi-remote.

To run an SSR example by hand:

pnpm --filter example-react-remote preview
pnpm --filter example-react-ssr-host serve

The SSR host launches Node with --experimental-vm-modules. The flag is required because the MF runtime uses the VM module loader for remote ESM evaluation on the server.

Internal docs


Release Flow

Versioning is managed by Changesets:

pnpm changeset             # author a changeset
pnpm version-packages      # bump versions + update generated release files
pnpm release               # build the package and publish via Changesets

The release workflow publishes from a semver tag (v*.*.*) or manual dispatch with a matching tag. Full policy, quality gates, and step-by-step instructions: docs/release-checklist.md.


CI / CD

  • .github/workflows/ci.yml — runs pnpm check on PRs and pushes to main; Node 22 also runs the package smoke test and Vite peer runtime matrix smoke.
  • .github/workflows/extended-e2e.yml — manual and weekly Playwright coverage for compat, shared runtime, multi-remote, browser matrix, SSR, and DTS dev sync on Node 20 and 22.
  • .github/workflows/release.yml — publishes matching v*.*.* tags or a manual tag dispatch.
  • Publishing requires NPM_AUTH_TOKEN / NODE_AUTH_TOKEN to be configured in GitHub secrets.
  • Published packages use provenance: true.

License

MIT

About

A Vite/Rollup plugin for Module Federation.

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors