Module Federation v2

Tabla de Contenidos


Comparativa de versiones (v1 vs v1.5 vs v2)

Module Federation ha evolucionado de forma significativa desde su introduccion en webpack 5. Entender la genealogia es fundamental al evaluar documentacion, posts y ejemplos de la comunidad: los consejos para v1 a menudo no aplican a v2.

Caracteristicav1 (webpack 5 nativo)v1.5 (comunidad / @module-federation/enhanced)v2 (nativo en Rspack, sucesor oficial)
BundlerSolo webpack 5webpack 5 (basado en plugin)Rspack / Rsbuild (first-class), webpack 5 soportado
Remote EntryremoteEntry.js estatico con URL fija en build timeEstatico o dinamico; descubrimiento basado en manifestDinamico via mf-manifest.json; fallback estatico soportado
Runtime APINinguna; puramente declarativa via configuracion del plugin@module-federation/enhanced/runtimeinit(), loadRemote(), registerRemotes()Misma runtime API, estabilizada y extendida
Negociacion de dependencias compartidasResolucion interna de webpack basada en scopeNegociacion en runtime via metadatos del manifestNegociacion en runtime; mejor matching de rangos de version
Type HintsNingunoPlugin dts experimental; tipos generados en build timePlugin dts estable; tipos servidos junto al manifest y descargados automaticamente por el host
DevToolsNingunoLogging basicoPlugin de Chrome DevTools para inspeccionar el grafo de federation, dependencias compartidas y remotes cargados
Protocolo de manifestN/A — solo remoteEntry.jsSe introduce mf-manifest.jsonmf-manifest.json estandarizado; incluye modulos expuestos, dependencias compartidas y URLs de tipos
Registro dinamico de remotesNo soportado nativamenteregisterRemotes() en runtimeregisterRemotes() con soporte de hot-update
Rendimiento de buildVelocidades de webpack 5Velocidades de webpack 5Rspack (basado en Rust) — builds 5-10x mas rapidos
HMR para remotesNo soportadoParcial; frecuentemente requiere recarga completaMejorado significativamente en 2.0; HMR intra-remote funciona de forma fiable, HMR cross-remote todavia requiere recarga completa de pagina en algunas configuraciones
Soporte monorepoConfiguracion manualMejores defaults del pluginFirst-class con project references de Rsbuild

Conclusion clave: v2 no es una ruptura total — es la estabilizacion del trabajo comunitario de v1.5 bajo el paquete @module-federation/enhanced, con Rspack como bundler principal. El nombre del paquete npm sigue siendo @module-federation/enhanced; "v2" se refiere al protocolo y a la generacion de runtime, no a un paquete separado.

Nota sobre versionado: La etiqueta "Module Federation v2" (o "MF v2") se refirio historicamente a la version arquitectonica (el protocolo y la generacion de runtime), mientras que el paquete npm @module-federation/enhanced usaba numeros de version 0.x. A partir de la release 2.0.1, la version major del paquete npm se alinea con la version arquitectonica — ambas son "v2". Cuando este documento dice "v2", se refiere a la arquitectura y a @module-federation/enhanced@^2.0.1.

Flujo de carga en runtime de Module Federation v2 Flujo de carga en runtime de MF v2 Host App(Shell) mf-manifest.jsonConfig de version CDN / R2Descarga de chunks Shared DepsNegociar / omitir Mount 1. init() Inicializar runtime de MF con config compartida 2. registerRemotes() URLs de remotes dinamicas desde config de version 3. loadRemote() Obtener manifest, resolver shared deps, cargar chunks 4. El componente se monta Dentro de <Suspense> + <ErrorBoundary> en el arbol React del Shell

Configuracion de Rsbuild

Configuracion del Host (Shell App)

La aplicacion host (shell) es el punto de entrada al que navegan los usuarios. Carga los MFEs remotos en runtime segun la configuracion de rutas o feature flags. El host declara que remotes puede consumir, pero resuelve sus URLs reales de forma dinamica.

// apps/shell/rsbuild.config.ts

import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';

export default defineConfig({
  plugins: [pluginReact()],

  server: {
    port: 3000,
  },

  dev: {
    // Required for loading remotes from different dev server ports
    assetPrefix: 'http://localhost:3000',
  },

  output: {
    // In production, assets are served from a CDN via Cloudflare Workers
    assetPrefix: process.env.CDN_BASE_URL || '/',
  },

  tools: {
    rspack: (config, { appendPlugins }) => {
      appendPlugins(
        new ModuleFederationPlugin({
          name: 'shell',

          // Remotes are NOT statically declared here for production.
          // They are registered at runtime via `registerRemotes()` using
          // version config fetched from Cloudflare KV.
          //
          // For local development, static remotes can be declared:
          remotes:
            process.env.NODE_ENV === 'development'
              ? {
                  mfe_dashboard: 'mfe_dashboard@http://localhost:3001/mf-manifest.json',
                  mfe_settings: 'mfe_settings@http://localhost:3002/mf-manifest.json',
                  mfe_analytics: 'mfe_analytics@http://localhost:3003/mf-manifest.json',
                }
              : {},

          // Shared dependencies — must match remote configs exactly
          shared: {
            react: {
              singleton: true,
              requiredVersion: '^19.2.4',
              eager: false,
            },
            'react-dom': {
              singleton: true,
              requiredVersion: '^19.2.4',
              eager: false,
            },
            'react-router-dom': {
              singleton: true,
              requiredVersion: '^7.13.1',
              eager: false,
            },
            // State management — shared to ensure single store instance
            zustand: {
              singleton: true,
              requiredVersion: '^5.0.11',
            },
          },

          // Enable type generation fetching from remotes
          dts: {
            consumeTypes: {
              // Directory where fetched remote types are written
              remoteTypesFolder: '@mf-types',
            },
          },

          // Runtime plugins for telemetry, error reporting, etc.
          runtimePlugins: ['./src/mf-runtime-plugins/telemetry.ts'],
        })
      );
    },
  },
});

Notas sobre la configuracion del host:

  • remotes esta intencionalmente vacio en produccion. El shell obtiene una configuracion de versiones desde Cloudflare KV al arrancar y registra los remotes dinamicamente a traves de la runtime API. Esto desacopla el despliegue de MFEs del despliegue del shell.
  • eager: false en React/ReactDOM es critico. La carga eager en el host fuerza una inicializacion sincrona y anula el patron de bootstrap asincrono. La unica excepcion es cuando el host debe renderizar antes de que se cargue cualquier remote (ver Requisito de bootstrap asincrono).
  • dts.consumeTypes indica al build que descargue los tarballs .d.ts publicados por cada remote, dando al host type safety en tiempo de compilacion para los modulos remotos.

Configuracion del Remote (MFE)

Cada MFE es una aplicacion independiente que puede ejecutarse de forma autonoma durante el desarrollo, pero se carga dentro del shell en produccion. La configuracion del remote declara que modulos expone y como comparte dependencias con el host.

// apps/mfe-dashboard/rsbuild.config.ts

import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
import packageJson from './package.json';

export default defineConfig({
  plugins: [pluginReact()],

  server: {
    port: 3001,
    // Allow the shell to load this remote during local development
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    },
  },

  dev: {
    assetPrefix: 'http://localhost:3001',
  },

  output: {
    // Each MFE deploys to its own CDN path
    // e.g., https://cdn.example.com/mfe-dashboard/v1.2.3/
    assetPrefix: process.env.CDN_BASE_URL || '/',

    // Clean output directory on each build
    cleanDistPath: true,
  },

  tools: {
    rspack: (config, { appendPlugins }) => {
      appendPlugins(
        new ModuleFederationPlugin({
          name: 'mfe_dashboard',

          // Modules exposed to the host and other remotes
          exposes: {
            './App': './src/App.tsx',
            './DashboardWidget': './src/components/DashboardWidget.tsx',
            './hooks/useDashboardData': './src/hooks/useDashboardData.ts',
          },

          // Shared dependencies — must be compatible with host config
          shared: {
            react: {
              singleton: true,
              requiredVersion: '^19.2.4',
              eager: false,
            },
            'react-dom': {
              singleton: true,
              requiredVersion: '^19.2.4',
              eager: false,
            },
            'react-router-dom': {
              singleton: true,
              requiredVersion: '^7.13.1',
              eager: false,
            },
            zustand: {
              singleton: true,
              requiredVersion: '^5.0.11',
            },
          },

          // Generate mf-manifest.json for runtime discovery
          manifest: {
            filePath: 'static',
          },

          // Generate TypeScript declarations for exposed modules
          dts: {
            generateTypes: {
              // Exposed modules will have .d.ts files generated
              // and bundled into a tarball served alongside the manifest
              extractThirdParty: true,
              extractRemoteTypes: true,
            },
          },
        })
      );
    },
  },
});

Notas sobre la configuracion del remote:

  • manifest.filePath: "static" genera el mf-manifest.json en el directorio de assets estaticos, haciendolo servible desde el CDN junto a los chunks. El Cloudflare Worker proxifica las peticiones a este manifest en la URL esperada.
  • exposes usa la convencion "./NombreModulo". El host los carga como loadRemote("mfe_dashboard/App"). El prefijo ./ en la clave es requerido por el plugin pero se omite en la llamada a la runtime API.
  • dts.generateTypes produce un @mf-types.d.ts.tgz (o archivos .d.ts individuales, segun la configuracion) que el dts.consumeTypes del host descarga en build time.
  • El campo name debe ser un identificador JavaScript valido (sin guiones). Usa guiones bajos: mfe_dashboard, no mfe-dashboard.

Estrategia de dependencias compartidas

La configuracion de dependencias compartidas es el aspecto mas propenso a errores de Module Federation. Una mala configuracion provoca instancias duplicadas de React (rompiendo hooks), incompatibilidades de version o crashes en runtime.

Negociacion de dependencias compartidas — El host declara shared, el remote verifica, singleton o separada Negociacion de dependencias compartidas Host (Shell)Declara shared depsreact: ^19.2.4 manifest Remote (MFE)Verifica rango de versionreact: ^19.2.4 Match? Usar singleton del HostUna instancia compartida Cargar separadaInstancia duplicada ⚠ singleton: trueSolo una version cargadaNecesario para React hooks eager: falseCarga asincrona para lanegociacion de bootstrap requiredVersionRango semver verificado enruntime por la negociacion de MF

Principios fundamentales

React y ReactDOM como singletons estrictos:

shared: {
  react: {
    singleton: true,          // Only one instance allowed at runtime
    requiredVersion: '^19.2.4', // Semver range the MFE was built against
    eager: false,             // Loaded async (required for proper bootstrap)
  },
  'react-dom': {
    singleton: true,
    requiredVersion: '^19.2.4',
    eager: false,
  },
}

Cuando singleton: true, el runtime de Module Federation garantiza que solo se carga una copia de la libreria. Si varios remotes proporcionan versiones distintas, el runtime selecciona la version mas alta que satisface todas las restricciones de requiredVersion. Si ninguna version unica satisface todas las restricciones, se emite un warning en consola y se usa la version mas alta disponible de todos modos (porque singleton fuerza una unica copia).

Atencion — desajuste de version en singleton: Cuando varios remotes declaran una dependencia compartida singleton con rangos de requiredVersion distintos, Module Federation usa silenciosamente la version mas alta disponible en runtime. No hay error en build time — solo un warning en consola. Esto puede provocar problemas sutiles en runtime si las APIs cambiaron entre versiones (por ejemplo, un remote compilado contra zustand@^4.5.0 ejecutandose con zustand@5.0.11 cargado por otro remote). Recomendacion: aplicar rangos de requiredVersion identicos en todos los remotes y el host. Agregar un paso de lint en CI que compare las configuraciones shared en todos los archivos rsbuild.config.ts del monorepo para detectar desviaciones a tiempo.

Design system como singleton compartido con fallback de MF remote:

El design system interno (@acme/design-system) se comparte como singleton para que todos los MFEs rendericen de forma consistente. Tambien se expone como MF remote para que el shell sirva la version canonica:

// In the shell's shared config:
shared: {
  '@acme/design-system': {
    singleton: true,
    requiredVersion: '^3.0.0',
    eager: false,
  },
}

Esto asegura que todos los MFEs usen la misma instancia del design system. Si un MFE empaqueta una version fuera del rango, la version del shell prevalece (comportamiento de singleton).

Tradeoffs entre carga eager y lazy

EstrategiaComportamientoCaso de uso
eager: false (por defecto)La dependencia compartida se carga de forma asincrona cuando se necesita por primera vezEstandar para todos los remotes y el host
eager: trueLa dependencia compartida se empaqueta en el chunk inicial y se carga de forma sincronaUsar solo cuando el host debe renderizar antes de cualquier async boundary (poco frecuente)

Recomendacion: Usar siempre eager: false. El patron de bootstrap asincrono (ver mas abajo) maneja la secuencia de carga correctamente. Configurar eager: true en un remote hara que empaquete su propia copia de la dependencia, anulando el proposito de compartir.

Manejo de desajustes de version

Cuando un MFE declara requiredVersion: "^19.2.4" pero el shell proporciona React 19.1.0:

  1. Modo singleton: El runtime usa el 19.1.0 del shell porque singleton fuerza una unica copia. Se imprime un warning en consola: Unsatisfied version 19.1.0 of shared singleton module react (required ^19.2.4).
  2. Modo no-singleton: El MFE carga su propia copia empaquetada de 19.2.x, resultando en dos instancias de React. Esto rompe hooks y context.

Estrategia de versionado recomendada:

  • Fijar la version major en todos los MFEs y el shell.
  • Permitir variacion de minor y patch — usar ^19.2.4, no 19.2.4.
  • Ejecutar un chequeo en CI que compare los rangos de requiredVersion en todos los archivos rsbuild.config.ts del monorepo.
  • Al actualizar versiones major de React, coordinar un flag-day en todos los MFEs o usar la configuracion de versiones para controlar el rollout.

Carga dinamica de remotes

La carga dinamica de remotes es la base del despliegue independiente de MFEs. El shell no tiene URLs de remotes hardcodeadas — obtiene una configuracion de versiones al arrancar y registra remotes sobre la marcha.

Flujo de registro dinamico de remotes — fetch config, registerRemotes, loadRemote Flujo de registro dinamico de remotes Fetch ConfigVersion Config KV registerRemotes()Mapear nombre a URL loadRemote()Al navegar a una ruta CDN FetchChunks + manifest Render Config: { "mfe_a": "https://cdn/…/mf-manifest.json" }Se obtiene una vez en el bootstrap del Shell Remotes registrados dinamicamenteSin URLs hardcodeadas en build config Carga lazy por rutaSuspense boundary muestra loading

Patron completo de bootstrap

// apps/shell/src/mf-runtime/init.ts

import { init, loadRemote, registerRemotes } from '@module-federation/enhanced/runtime';

/**
 * Version config fetched from Cloudflare KV at startup.
 * Maps MFE names to their manifest URLs.
 *
 * Example:
 * {
 *   "mfe_dashboard": "https://cdn.example.com/mfe-dashboard/v1.2.3/mf-manifest.json",
 *   "mfe_settings": "https://cdn.example.com/mfe-settings/v2.0.1/mf-manifest.json",
 *   "mfe_analytics": "https://cdn.example.com/mfe-analytics/v0.9.0/mf-manifest.json"
 * }
 */
export interface VersionConfig {
  [mfeName: string]: string; // MFE name → manifest URL
}

let initialized = false;

/**
 * Initialize the Module Federation runtime with remotes from the version config.
 * This must be called once before any `loadRemote()` call.
 */
export async function initializeFederation(versionConfig: VersionConfig): Promise<void> {
  if (initialized) {
    console.warn('[MF] Federation already initialized. Use updateRemotes() to change remotes.');
    return;
  }

  const remotes = Object.entries(versionConfig).map(([name, entry]) => ({
    name,
    entry,
  }));

  init({
    name: 'shell',
    remotes,
    shared: {
      react: {
        version: '19.2.4',
        scope: 'default',
        lib: () => require('react'),
        shareConfig: {
          singleton: true,
          requiredVersion: '^19.2.4',
        },
      },
      'react-dom': {
        version: '19.2.4',
        scope: 'default',
        lib: () => require('react-dom'),
        shareConfig: {
          singleton: true,
          requiredVersion: '^19.2.4',
        },
      },
    },
  });

  initialized = true;
}

/**
 * Update or add remotes at runtime without re-initializing.
 * Useful for feature flags that enable/disable MFEs dynamically.
 */
export function updateRemotes(versionConfig: Partial<VersionConfig>): void {
  const remotes = Object.entries(versionConfig).map(([name, entry]) => ({
    name,
    entry,
  }));

  registerRemotes(remotes, { force: true });
}

Cargar un componente remoto

// apps/shell/src/mf-runtime/loadMfe.ts

import { loadRemote } from '@module-federation/enhanced/runtime';
import type { ComponentType } from 'react';

interface RemoteModule {
  default: ComponentType<Record<string, unknown>>;
}

/**
 * Load a remote module by its federated name.
 *
 * @param remoteName - e.g., "mfe_dashboard/App"
 * @returns The default export of the remote module (typically a React component)
 */
export async function loadMfe<T = ComponentType<Record<string, unknown>>>(
  remoteName: string
): Promise<T> {
  const module = await loadRemote<RemoteModule>(remoteName);

  if (!module) {
    throw new Error(`[MF] Failed to load remote module: ${remoteName}`);
  }

  return module.default as T;
}

Wrapper de componente React con Suspense y ErrorBoundary

// apps/shell/src/components/RemoteComponent.tsx

import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react';
import type { ComponentType } from 'react';
import { loadMfe } from '../mf-runtime/loadMfe';

interface RemoteComponentProps {
  /** Federated module name, e.g., "mfe_dashboard/App" */
  remoteName: string;
  /** Props to pass through to the loaded remote component */
  componentProps?: Record<string, unknown>;
  /** Fallback UI shown while loading */
  loadingFallback?: React.ReactNode;
  /** Fallback UI shown on error */
  errorFallback?: React.ReactNode;
  /** Maximum number of retry attempts on load failure */
  maxRetries?: number;
  /** Delay in ms between retries (doubles on each attempt) */
  retryDelayMs?: number;
}

interface ErrorBoundaryState {
  hasError: boolean;
  error: Error | null;
}

class RemoteErrorBoundary extends React.Component<
  {
    children: React.ReactNode;
    fallback: React.ReactNode;
    onError?: (error: Error) => void;
    onRetry?: () => void;
  },
  ErrorBoundaryState
> {
  constructor(props: RemoteErrorBoundary['props']) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
    console.error('[MF] Remote component error:', error, errorInfo);
    this.props.onError?.(error);
  }

  reset = (): void => {
    this.setState({ hasError: false, error: null });
    this.props.onRetry?.();
  };

  render(): React.ReactNode {
    if (this.state.hasError) {
      return (
        <div role="alert">
          {this.props.fallback || (
            <div style={{ padding: '16px', border: '1px solid #e53e3e', borderRadius: '8px' }}>
              <h3>Failed to load module</h3>
              <p>{this.state.error?.message}</p>
              <button onClick={this.reset}>Retry</button>
            </div>
          )}
        </div>
      );
    }

    return this.props.children;
  }
}

/**
 * Wrapper component that dynamically loads a federated remote module,
 * handles loading states, errors, and retries.
 */
export function RemoteComponent({
  remoteName,
  componentProps = {},
  loadingFallback,
  errorFallback,
  maxRetries = 3,
  retryDelayMs = 1000,
}: RemoteComponentProps): React.ReactElement {
  const [LoadedComponent, setLoadedComponent] = useState<ComponentType<Record<string, unknown>> | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  const retryCountRef = useRef(0);

  const loadComponent = useCallback(async () => {
    setLoading(true);
    setError(null);

    try {
      const Component = await loadMfe(remoteName);
      setLoadedComponent(() => Component);
    } catch (err) {
      const loadError = err instanceof Error ? err : new Error(String(err));

      if (retryCountRef.current < maxRetries) {
        retryCountRef.current += 1;
        const delay = retryDelayMs * Math.pow(2, retryCountRef.current - 1);
        console.warn(
          `[MF] Retry ${retryCountRef.current}/${maxRetries} for ${remoteName} in ${delay}ms`
        );
        setTimeout(() => {
          loadComponent();
        }, delay);
        return;
      }

      setError(loadError);
    } finally {
      setLoading(false);
    }
  }, [remoteName, maxRetries, retryDelayMs]);

  useEffect(() => {
    retryCountRef.current = 0;
    loadComponent();
  }, [loadComponent]);

  const handleRetry = useCallback(() => {
    retryCountRef.current = 0;
    loadComponent();
  }, [loadComponent]);

  if (loading) {
    return <>{loadingFallback || <div>Loading module...</div>}</>;
  }

  if (error || !LoadedComponent) {
    return (
      <div role="alert">
        {errorFallback || (
          <div style={{ padding: '16px', border: '1px solid #e53e3e', borderRadius: '8px' }}>
            <h3>Failed to load {remoteName}</h3>
            <p>{error?.message || 'Unknown error'}</p>
            <button onClick={handleRetry}>Retry</button>
          </div>
        )}
      </div>
    );
  }

  return (
    <RemoteErrorBoundary
      fallback={errorFallback}
      onError={(err) => console.error(`[MF] Runtime error in ${remoteName}:`, err)}
      onRetry={handleRetry}
    >
      <Suspense fallback={loadingFallback || <div>Loading...</div>}>
        <LoadedComponent {...componentProps} />
      </Suspense>
    </RemoteErrorBoundary>
  );
}

Uso en el routing del Shell

// apps/shell/src/App.tsx

import { Routes, Route } from 'react-router-dom';
import { RemoteComponent } from './components/RemoteComponent';
import { ShellLayout } from './components/ShellLayout';
import { ModuleLoadingSkeleton } from './components/ModuleLoadingSkeleton';

export function App(): React.ReactElement {
  return (
    <ShellLayout>
      <Routes>
        <Route
          path="/dashboard/*"
          element={
            <RemoteComponent
              remoteName="mfe_dashboard/App"
              loadingFallback={<ModuleLoadingSkeleton />}
              maxRetries={3}
              retryDelayMs={500}
            />
          }
        />
        <Route
          path="/settings/*"
          element={
            <RemoteComponent
              remoteName="mfe_settings/App"
              loadingFallback={<ModuleLoadingSkeleton />}
            />
          }
        />
        <Route
          path="/analytics/*"
          element={
            <RemoteComponent
              remoteName="mfe_analytics/App"
              loadingFallback={<ModuleLoadingSkeleton />}
            />
          }
        />
      </Routes>
    </ShellLayout>
  );
}

Generacion de tipos TypeScript

Module Federation v2 resuelve uno de los aspectos mas dolorosos de los micro-frontends: la type safety entre aplicaciones desplegadas de forma independiente.

Generacion de tipos entre repos — build, @mf-types, consumo Generacion de tipos TypeScript entre repos Build del Remote MFE dts.generateTypes = true Extrae .d.ts de exposes Empaqueta en @mf-types.zip deploy CDN / R2 mf-manifest.json @mf-types.zip JS chunks fetch Build del Host (Shell) dts.consumeTypes = true Descarga @mf-types.zip → IntelliSense completo + type checking

Como funciona

  1. En build time, la configuracion dts.generateTypes del remote MFE extrae las declaraciones TypeScript de todas las entradas en exposes.
  2. Los archivos .d.ts generados se empaquetan en un tarball (por ejemplo, @mf-types.zip) y se colocan junto al mf-manifest.json en el output del build.
  3. En build time del host, la configuracion dts.consumeTypes descarga estos tarballs desde la URL del manifest de cada remote y los extrae en un directorio local @mf-types/.
  4. El compilador TypeScript del host detecta estos tipos via path mappings, proporcionando IntelliSense completo y type checking para los modulos remotos.

Configuracion DTS del remote

// In the remote's rsbuild.config.ts (within ModuleFederationPlugin)

dts: {
  generateTypes: {
    // Include types from third-party packages used in exposed APIs
    extractThirdParty: true,
    // Include types from other remotes if this MFE re-exports them
    extractRemoteTypes: true,
    // Compile only the exposed modules (faster than full project)
    compilerInstance: 'tsc',
    // Additional tsconfig options for type generation
    tsConfigPath: './tsconfig.json',
  },
},

Configuracion DTS del host

// In the host's rsbuild.config.ts (within ModuleFederationPlugin)

dts: {
  consumeTypes: {
    // Directory where fetched remote types are written
    remoteTypesFolder: '@mf-types',
    // Automatically fetch types when building
    consumeAPITypes: true,
  },
},

Path mappings en tsconfig.json

Para la resolucion de tipos en desarrollo (antes de que el build descargue los tipos), hay que configurar aliases de rutas:

// apps/shell/tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      // Module Federation remote type mappings
      "mfe_dashboard/*": ["./@mf-types/mfe_dashboard/*"],
      "mfe_settings/*": ["./@mf-types/mfe_settings/*"],
      "mfe_analytics/*": ["./@mf-types/mfe_analytics/*"],

      // Shared types package from the monorepo
      "@acme/shared-types": ["../../packages/shared-types/src/index.ts"]
    }
  },
  "include": ["src", "@mf-types"]
}

Comparticion de tipos: monorepo vs cross-repo

Monorepo (preferido): Todos los MFEs viven en el mismo repositorio. Los tipos de packages/shared-types se importan directamente. La generacion de @mf-types complementa esto con tipos para las fronteras de modulos federados.

Cross-repo: Los MFEs estan en repositorios separados. El output de dts.generateTypes debe ser accesible para el host en build time. Opciones:

  1. Tipos servidos desde CDN (recomendado): El tarball de tipos generado se despliega junto al manifest. El consumeTypes del host lo descarga via HTTPS.
  2. Paquete npm: Cada MFE publica un paquete @acme/mfe-dashboard-types. Esto acopla el despliegue a los ciclos de publicacion en npm y no se recomienda.
  3. Git submodules: Evitar. Agrega complejidad sin beneficios claros.

Protocolo mf-manifest.json

El archivo mf-manifest.json es el mecanismo central de descubrimiento en Module Federation v2. Reemplaza el opaco remoteEntry.js de v1 con un manifest estructurado e inspeccionable.

Estructura del manifest

// Example: https://cdn.example.com/mfe-dashboard/v1.2.3/mf-manifest.json
{
  "id": "mfe_dashboard",
  "name": "mfe_dashboard",
  "metaData": {
    "name": "mfe_dashboard",
    "type": "app",
    "buildInfo": {
      "buildVersion": "1.2.3",
      "buildHash": "a1b2c3d4"
    },
    "remoteEntry": {
      "name": "remoteEntry.js",
      "path": "",
      "type": "global"
    },
    "types": {
      "path": "",
      "name": "@mf-types.zip",
      "api": "@mf-types/mfe_dashboard/apis.d.ts"
    },
    "globalName": "mfe_dashboard",
    "pluginVersion": "2.0.1"
  },
  "shared": [
    {
      "id": "mfe_dashboard:react",
      "name": "react",
      "version": "19.2.4",
      "requiredVersion": "^19.2.4",
      "singleton": true,
      "eager": false,
      "assets": {
        "js": { "async": ["__federation_shared/react.js"], "sync": [] },
        "css": { "async": [], "sync": [] }
      }
    },
    {
      "id": "mfe_dashboard:react-dom",
      "name": "react-dom",
      "version": "19.2.4",
      "requiredVersion": "^19.2.4",
      "singleton": true,
      "eager": false,
      "assets": {
        "js": { "async": ["__federation_shared/react-dom.js"], "sync": [] },
        "css": { "async": [], "sync": [] }
      }
    }
  ],
  "exposes": [
    {
      "id": "mfe_dashboard:./App",
      "name": "./App",
      "path": "./src/App.tsx",
      "assets": {
        "js": { "async": ["static/js/expose_App.js"], "sync": [] },
        "css": { "async": ["static/css/expose_App.css"], "sync": [] }
      }
    },
    {
      "id": "mfe_dashboard:./DashboardWidget",
      "name": "./DashboardWidget",
      "path": "./src/components/DashboardWidget.tsx",
      "assets": {
        "js": { "async": ["static/js/expose_DashboardWidget.js"], "sync": [] },
        "css": { "async": [], "sync": [] }
      }
    }
  ]
}

Como usa el runtime el manifest

  1. Descubrimiento: Cuando se invoca init() o registerRemotes() con una URL de manifest, el runtime descarga el JSON.
  2. Negociacion de dependencias compartidas: El runtime compara las entradas shared entre todos los manifests cargados y el host. Determina que version de cada dependencia compartida usar, respetando singleton, requiredVersion y las reglas de precedencia de version.
  3. Carga de modulos: Cuando se llama a loadRemote("mfe_dashboard/App"), el runtime busca la entrada exposes para ./App, resuelve las URLs de assets relativas a la URL del manifest y carga dinamicamente el chunk JavaScript.
  4. Resolucion de tipos: El build del host lee metaData.types para saber de donde descargar el tarball .d.ts.

Consideraciones para servir desde CDN

El manifest y sus assets referenciados se sirven desde un CDN (Cloudflare R2 detras de Workers en esta arquitectura). Consideraciones clave:

  • Estabilidad de la URL del manifest: La URL del manifest incluye la version (/mfe-dashboard/v1.2.3/mf-manifest.json). La configuracion de versiones en KV apunta a la version actual. Esto permite rollback instantaneo actualizando la entrada en KV.
  • Inmutabilidad de assets: Todos los chunks JS/CSS tienen content-hash. Se pueden cachear agresivamente (Cache-Control: public, max-age=31536000, immutable).
  • Cache del manifest: El manifest en si debe tener un TTL corto o usar no-cache con validacion ETag, porque el host lo descarga en cada carga de pagina para descubrir las URLs de chunks actuales.

Atencion — race condition de frescura del manifest: El mf-manifest.json puede quedar obsoleto si un remote se redesplega mientras el shell aun tiene un manifest cacheado (por ejemplo, desde un cache de Service Worker, un cache agresivo de edge del CDN, o un cache en memoria de una pestana del navegador abierta durante mucho tiempo). Cuando esto ocurre, el shell puede intentar cargar URLs de chunks que ya no existen, provocando errores 404 y MFEs rotos. Mitigacion: agregar un parametro de cache-busting a las URLs del manifest (por ejemplo, mf-manifest.json?t=<timestamp> o mf-manifest.json?v=<deployId>), usar TTLs cortos (max-age=0, must-revalidate) en las respuestas del manifest, y asegurar que los Service Workers no cacheen agresivamente los manifests.

Headers de Cache-Control

# mf-manifest.json — revalidate on every request
Cache-Control: public, max-age=0, must-revalidate
ETag: "a1b2c3d4"

# remoteEntry.js — revalidate (it references chunk URLs)
Cache-Control: public, max-age=0, must-revalidate

# JS/CSS chunks (content-hashed filenames)
Cache-Control: public, max-age=31536000, immutable

# @mf-types.zip — immutable per version
Cache-Control: public, max-age=31536000, immutable

Requisito de bootstrap asincrono

Module Federation requiere un punto de entrada asincrono para negociar correctamente las dependencias compartidas antes de que se ejecute cualquier codigo de la aplicacion. Esta es la causa mas comun del error "Shared module is not available for eager consumption".

El patron

// apps/shell/src/index.ts
// This is the webpack/rspack entry point.
// It MUST be a dynamic import — nothing else.

import('./bootstrap');
// apps/shell/src/bootstrap.tsx
// This is where the actual application code lives.

import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { App } from './App';
import { initializeFederation } from './mf-runtime/init';
import { fetchVersionConfig } from './services/versionConfig';

async function main(): Promise<void> {
  // 1. Fetch version config from Cloudflare Worker (backed by KV)
  const versionConfig = await fetchVersionConfig();

  // 2. Initialize Module Federation runtime with remote manifests
  await initializeFederation(versionConfig);

  // 3. Render the application
  const container = document.getElementById('root');
  if (!container) {
    throw new Error('Root element not found');
  }

  const root = createRoot(container);
  root.render(
    <React.StrictMode>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </React.StrictMode>
  );
}

main().catch((err) => {
  console.error('[Shell] Failed to bootstrap application:', err);
  // Render a minimal error state without React (since React itself may have failed to load)
  const root = document.getElementById('root');
  if (root) {
    root.innerHTML = `
      <div style="padding: 32px; font-family: sans-serif;">
        <h1>Application failed to load</h1>
        <p>Please try refreshing the page. If the problem persists, contact support.</p>
        <pre style="color: red;">${err.message}</pre>
      </div>
    `;
  }
});

El mismo patron aplica a cada remote MFE cuando se ejecuta de forma independiente:

// apps/mfe-dashboard/src/index.ts
import('./bootstrap');
// apps/mfe-dashboard/src/bootstrap.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { App } from './App';

const container = document.getElementById('root');
if (container) {
  const root = createRoot(container);
  root.render(
    <React.StrictMode>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </React.StrictMode>
  );
}

Que se rompe sin bootstrap asincrono

Sin la indireccion de import('./bootstrap'), ocurre la siguiente secuencia:

  1. Rspack evalua el modulo de entrada de forma sincrona.
  2. El modulo de entrada importa React (una dependencia compartida).
  3. Module Federation aun no ha tenido oportunidad de verificar que version de React usar (la del host o la del remote).
  4. El contenedor de dependencias compartidas no esta inicializado, asi que el import cae en una copia local — o falla con: Uncaught Error: Shared module is not available for eager consumption.

El import() dinamico crea un boundary asincrono que da tiempo al runtime de Module Federation para:

  • Cargar los scripts de remote entry.
  • Negociar las versiones de dependencias compartidas.
  • Configurar el shared scope.

Solo despues de completar esta negociacion se ejecuta bootstrap.tsx, y todas las sentencias import dentro de el se resuelven contra el shared scope negociado.


Patrones de Error Boundary

Los componentes remotos se cargan por red desde servicios desplegados de forma independiente. Pueden fallar por muchas razones: errores de red, caidas del CDN, cambios de codigo incompatibles, o excepciones en runtime en el codigo remoto. Un manejo de errores robusto es innegociable.

Jerarquia de Error Boundary y Fallback — Suspense, ErrorBoundary, Fallback UI Jerarquia de Error Boundary y Fallback <React.Suspense> fallback = Spinner de carga <ErrorBoundary> fallback = Mensaje de error + boton de Retry Componente remoto OK Cargando...Mientras se descargan chunks Error + RetryError de red / runtime Error de carga404, error de parse, CDN caido

Manejo de errores por capas

El wrapper RemoteComponent mostrado en Carga dinamica de remotes implementa tres capas:

  1. Manejo de errores en carga: Si loadRemote() falla (error de red, 404, error de parse), el componente muestra un estado de error con un boton de retry. Los reintentos usan backoff exponencial.

  2. React ErrorBoundary: Si el componente remoto carga correctamente pero lanza una excepcion durante el renderizado, el ErrorBoundary la captura y muestra un fallback. Esto maneja errores de runtime en codigo remoto.

  3. Suspense boundary: Si el componente remoto usa React.lazy o se suspende para obtener datos, el Suspense boundary muestra un estado de carga.

Detalles del mecanismo de retry

// Retry configuration per remote, based on criticality
const REMOTE_RETRY_CONFIG: Record<string, { maxRetries: number; delayMs: number }> = {
  // Dashboard is the primary view — retry aggressively
  mfe_dashboard: { maxRetries: 5, delayMs: 500 },
  // Settings is less critical — fewer retries
  mfe_settings: { maxRetries: 2, delayMs: 1000 },
  // Analytics can be degraded gracefully
  mfe_analytics: { maxRetries: 1, delayMs: 2000 },
};

Reporte de errores en produccion

// apps/shell/src/mf-runtime-plugins/telemetry.ts

import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';

const telemetryPlugin: () => FederationRuntimePlugin = () => ({
  name: 'telemetry-plugin',

  errorLoadRemote({ id, error, from }) {
    // Report to your observability platform
    console.error(`[MF Telemetry] Failed to load remote: ${id}`, {
      error: error?.message,
      from,
      timestamp: Date.now(),
    });

    // Example: send to an error tracking service
    // errorTracker.captureException(error, { tags: { mfeId: id, loadFrom: from } });

    // Return undefined to let the default error handling proceed,
    // or return a fallback module to gracefully degrade
    return undefined;
  },

  beforeInit(args) {
    console.info('[MF Telemetry] Initializing federation runtime', {
      name: args.options.name,
      remotes: args.options.remotes?.map((r) => r.name),
    });
    return args;
  },

  afterResolve(args) {
    console.debug(`[MF Telemetry] Resolved remote: ${args.id}`, {
      pkgName: args.pkgName,
      entry: args.remote?.entry,
    });
    return args;
  },
});

export default telemetryPlugin;

Errores frecuentes y troubleshooting

Bug del sufijo de version

Sintoma: Las claves de dependencias compartidas en el scope interno de webpack/rspack tienen numeros de version agregados, por ejemplo, react@19.2.4 en lugar de react. Esto hace que host y remote no puedan hacer match de dependencias compartidas, resultando en copias duplicadas.

Causa: Esto ocurre cuando el campo requiredVersion se resuelve a una version especifica (desde package.json) en lugar de un rango semver, y el bundler lo agrega como sufijo de clave.

Solucion: Siempre configurar requiredVersion explicitamente en la configuracion de shared con un rango semver (^19.2.4), en lugar de depender de la inferencia automatica desde package.json:

// BAD: inferred from package.json, may resolve to exact version
shared: ['react', 'react-dom']

// GOOD: explicit range, consistent across host and remotes
shared: {
  react: {
    singleton: true,
    requiredVersion: '^19.2.4',
  },
}

Errores de consumo eager

Sintoma: Uncaught Error: Shared module is not available for eager consumption

Causa: Codigo de la aplicacion que importa una dependencia compartida se ejecuta de forma sincrona antes de que Module Federation haya inicializado el shared scope.

Solucion:

  1. Asegurar que el punto de entrada sea import('./bootstrap') — nada mas (ver Requisito de bootstrap asincrono).
  2. Verificar que ninguna dependencia compartida tenga eager: true en un remote. Solo el host puede usar eager: true, y solo con consideracion cuidadosa.
  3. Verificar que ningun import de nivel superior en el archivo de entrada importe React u otras dependencias compartidas.

Limitaciones de HMR con remotes

Sintoma: Los cambios en el codigo de un remote durante el desarrollo no hacen hot-reload en el shell. Hay que recargar el shell completamente.

Causa: El runtime de Module Federation cachea los modulos remotos cargados. El HMR dentro del propio dev server del remote funciona bien, pero el sistema HMR del shell no sabe como invalidar modulos federados.

Actualizacion de MF 2.0: Module Federation 2.0 ha mejorado significativamente el soporte de HMR. El HMR intra-remote (cambios dentro de un solo remote reflejados en su propio dev server) ahora funciona de forma fiable out of the box. Sin embargo, el HMR cross-remote — donde un cambio en un remote se recarga en caliente dentro del shell que aloja multiples remotes — aun requiere recarga completa de pagina en algunas configuraciones (particularmente cuando hay dependencias compartidas singleton involucradas).

Workaround:

  • Durante el desarrollo, ejecutar cada MFE de forma independiente en su propio puerto y usar remoteEntry.js (no el manifest) para recargas de desarrollo mas rapidas.
  • Usar el dev server del shell principalmente para pruebas de integracion.
  • Aceptar que una recarga completa de pagina es necesaria al alternar entre desarrollo en el shell y en el remote.

Compatibilidad con pnpm

Sintoma: Las dependencias compartidas fallan al resolverse. Errores como Module not found: Can't resolve 'react' en remotes, a pesar de que React esta instalado.

Causa: La estructura estricta de node_modules de pnpm (con symlinks, no plana) implica que las dependencias compartidas pueden no estar hoisted a una ubicacion donde Module Federation pueda encontrarlas.

Solucion: Agregar una de las siguientes opciones a .npmrc en la raiz del workspace:

# Option 1: Hoist everything (simplest, but loses pnpm's strictness benefits)
shamefully-hoist=true

# Option 2: Hoist only the packages that Module Federation needs to share (recommended)
public-hoist-pattern[]=react
public-hoist-pattern[]=react-dom
public-hoist-pattern[]=react-router-dom
public-hoist-pattern[]=zustand
public-hoist-pattern[]=@acme/design-system

La opcion 2 es la recomendada porque preserva la rigurosidad de pnpm para el resto de paquetes, asegurando al mismo tiempo que Module Federation pueda resolver las dependencias compartidas.

Headers CORS para remotes cross-origin

Sintoma: Access to script at 'https://cdn.example.com/mfe-dashboard/remoteEntry.js' from origin 'https://app.example.com' has been blocked by CORS policy

Causa: El shell y los remotes se sirven desde origenes distintos. Los navegadores aplican CORS para scripts cargados dinamicamente al usar import() o new Function().

Solucion: El CDN o Cloudflare Worker que sirve los assets de remotes debe incluir headers CORS:

// In the Cloudflare Worker that serves MFE assets
const corsHeaders = {
  'Access-Control-Allow-Origin': 'https://app.example.com',
  'Access-Control-Allow-Methods': 'GET, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type',
};

// For development, allow all origins
if (env.ENVIRONMENT === 'development') {
  corsHeaders['Access-Control-Allow-Origin'] = '*';
}

Importante: No usar Access-Control-Allow-Origin: * en produccion si la aplicacion usa credenciales (cookies, headers de autenticacion). Especificar el origen permitido exacto.

Fallos de carga de chunks en produccion

Sintoma: Los usuarios ven pantallas en blanco o MFEs rotos tras un despliegue. La consola del navegador muestra errores 404 para chunks JS/CSS.

Causa: Se desplego una nueva version de un MFE, reemplazando chunks en el CDN, pero usuarios que tenian la pagina abierta aun tienen el manifest antiguo cacheado. El manifest antiguo referencia URLs de chunks que ya no existen.

Solucion (multicapa):

  1. Content-hash en todos los chunks: Esto es el comportamiento por defecto en Rsbuild/Rspack. Cada nombre de archivo de chunk incluye un hash de su contenido. Los nuevos despliegues producen nombres de archivo nuevos; los archivos antiguos permanecen en el CDN.

  2. No eliminar chunks antiguos inmediatamente: Retener versiones antiguas en R2/CDN durante al menos 24 horas (o mas) tras el despliegue. La configuracion de versiones en KV apunta a la nueva version, pero usuarios en medio de una sesion pueden necesitar chunks antiguos.

  3. Cache control del manifest: Configurar Cache-Control: no-cache en mf-manifest.json para que los navegadores siempre revaliden. Usar ETag para peticiones condicionales.

  4. Recuperacion de errores en runtime: El wrapper RemoteComponent reintenta ante fallos de carga de chunks. Si los reintentos fallan, muestra un estado de error con un boton "Refrescar pagina".

  5. Paths con prefijo de version: Desplegar cada version en un path unico (/mfe-dashboard/v1.2.3/). Esto garantiza estabilidad en las URLs de chunks. La configuracion de versiones en KV mapea nombres de MFE a paths versionados.

# CDN directory structure
/mfe-dashboard/
  v1.2.0/
    mf-manifest.json
    remoteEntry.js
    static/js/expose_App.abc123.js
    static/js/expose_App.abc123.js.map
  v1.2.1/
    mf-manifest.json
    remoteEntry.js
    static/js/expose_App.def456.js
    static/js/expose_App.def456.js.map

Referencias