Design System

Tabla de Contenidos


Descripcion general

Modelo de distribucion dual — paquete npm para tipos/tokens + MF remote para componentes Design System — Modelo de distribucion dual packages/design-system/ Componentes · Tokens · Hooks · Utils Paquete npm (Build-time) Tipos TypeScript (.d.ts) Design tokens · Variables CSS Funciones utilitarias · Hooks compartidos tsup → dist/ tree-shakeable MF Remote (Runtime) Componentes React compilados Cargados por el Shell via CDN Actualizaciones sin rebuild Rsbuild → dist-mf/ mf-manifest.json

El design system proporciona una biblioteca de componentes compartida, design tokens, hooks y utilidades a cada micro frontend (MFE) de la plataforma. Sigue un modelo de distribucion dual para satisfacer dos necesidades de consumo fundamentalmente distintas:

Canal de distribucionQue entregaCuando se usa
Paquete npmTipos TypeScript, design tokens, definiciones de variables CSS, funciones utilitarias, hooks compartidosBuild-time -- consumido por bundlers y type checkers durante el desarrollo y CI
Module Federation remoteComponentes React compilados, listos para runtimeRuntime -- cargados por el shell o los MFEs en el navegador, sin necesidad de rebuild

Por que distribucion dual?

Un unico canal de distribucion no puede cubrir ambas necesidades de forma eficiente:

  • npm solo obligaria a cada MFE a empaquetar su propia copia de los componentes del design system. Cualquier actualizacion del design system requeriria rebuild y redeploy de cada MFE dependiente. Sin embargo, npm es el vehiculo natural para artefactos de build-time: declaraciones de tipos TypeScript, constantes de tokens consumidas por IntelliSense del IDE, y funciones utilitarias que se reducen via tree-shaking a solo lo que cada MFE importa.

  • Module Federation solo no puede exponer tipos TypeScript en build-time. Los editores y el type-checking en CI dependen de archivos .d.ts que deben estar presentes en disco antes de que la aplicacion se ejecute. Los MF remotes se resuelven en runtime en el navegador, lo cual es demasiado tarde para el analisis estatico.

El modelo dual da a los equipos lo mejor de ambos mundos: seguridad de tipos completa y acceso a tokens en build-time via el paquete npm, y actualizaciones de componentes en runtime sin rebuild via el Module Federation remote. Cuando el design system publica un patch (por ejemplo, ajustar el border-radius de un boton), cada MFE recoge el cambio en su siguiente carga de pagina sin ningun redeploy.

El codigo fuente del design system reside en el monorepo bajo packages/design-system/ y es mantenido por el equipo de plataforma junto con la aplicacion shell y los paquetes de infraestructura compartida.


Estructura del paquete

Estructura del paquete Design System Estructura del paquete — packages/design-system/ src/components/ Button/ Modal/ ThemeProvider/ Typography/ .tsx + .module.css + .test.tsx src/tokens/ colors.ts spacing.ts typography.ts breakpoints.ts shadows.ts src/hooks/ useMediaQuery.ts useTheme.ts Hooks React compartidos para todos los MFEs Salidas de build dist/ (npm, tsup) dist-mf/ (MF remote) mf-manifest.json @mf-types.zip
packages/design-system/
├── src/
│   ├── components/                # React UI components
│   │   ├── Button/
│   │   │   ├── Button.tsx         # Component implementation
│   │   │   ├── Button.module.css  # Scoped styles via CSS Modules
│   │   │   ├── Button.test.tsx    # Unit + interaction tests
│   │   │   └── index.ts          # Public barrel export
│   │   ├── Modal/
│   │   │   ├── Modal.tsx
│   │   │   ├── Modal.module.css
│   │   │   ├── Modal.test.tsx
│   │   │   └── index.ts
│   │   ├── ThemeProvider/
│   │   │   ├── ThemeProvider.tsx
│   │   │   ├── ThemeProvider.test.tsx
│   │   │   └── index.ts
│   │   ├── Typography/
│   │   │   ├── Typography.tsx
│   │   │   ├── Typography.module.css
│   │   │   ├── Typography.test.tsx
│   │   │   └── index.ts
│   │   └── index.ts              # Re-exports all components
│   ├── tokens/                    # Design tokens (colors, spacing, typography)
│   │   ├── colors.ts
│   │   ├── spacing.ts
│   │   ├── typography.ts
│   │   ├── breakpoints.ts
│   │   ├── shadows.ts
│   │   └── index.ts
│   ├── hooks/                     # Shared React hooks
│   │   ├── useMediaQuery.ts
│   │   ├── useTheme.ts
│   │   ├── useClickOutside.ts
│   │   └── index.ts
│   ├── utils/                     # Utility functions
│   │   ├── cn.ts                  # Class name merge utility
│   │   ├── colorContrast.ts       # WCAG contrast calculations
│   │   ├── responsive.ts          # Responsive helpers
│   │   └── index.ts
│   ├── types/                     # Shared TypeScript types
│   │   ├── theme.ts
│   │   ├── components.ts
│   │   └── index.ts
│   └── index.ts                   # Main entry point
├── tsup.config.ts                 # Build config for npm distribution
├── rsbuild.config.ts              # Build config for MF remote
├── package.json
├── tsconfig.json
├── vitest.config.ts
└── CHANGELOG.md

Cada componente sigue una estructura interna consistente:

  • Component.tsx -- la implementacion React, que acepta props tipados y usa CSS Modules para el estilado.
  • Component.module.css -- CSS con scope que compila a nombres de clase unicos, previniendo colisiones entre MFEs.
  • Component.test.tsx -- tests colocados junto al componente usando Vitest y Testing Library.
  • index.ts -- archivo barrel que reexporta el componente y sus tipos de props.

Configuracion de build

Distribucion npm (tsup)

tsup compila el paquete en salidas ESM y CommonJS tree-shakeable con declaraciones TypeScript completas. Los multiples entry points permiten a los consumidores importar solo lo que necesitan, manteniendo los tamanos de bundle al minimo.

Nota: tsup esta actualmente en modo mantenimiento. El mantenedor ha redirigido su foco a tsdown, una alternativa emergente construida sobre Rolldown. Para proyectos nuevos, vale la pena evaluar tsdown como reemplazo. Para proyectos existentes como este, tsup sigue siendo estable y funcional, pero los equipos deben tener en cuenta la cadencia de mantenimiento reducida al planificar decisiones de tooling a largo plazo.

tsup.config.ts

import { defineConfig } from "tsup";

export default defineConfig({
  entry: {
    index: "src/index.ts",
    tokens: "src/tokens/index.ts",
    hooks: "src/hooks/index.ts",
    utils: "src/utils/index.ts",
    types: "src/types/index.ts",
  },
  format: ["esm", "cjs"],
  dts: true,
  sourcemap: true,
  clean: true,
  splitting: true,
  treeshake: true,
  external: ["react", "react-dom"],
  outDir: "dist",
  esbuildOptions(options) {
    options.banner = {
      js: '"use client";',
    };
  },
  onSuccess: "tsc --emitDeclarationOnly --declaration --declarationDir dist/types",
});

Decisiones clave:

  • Multiples entry points (index, tokens, hooks, utils, types) para que los consumidores puedan hacer deep-import de sub-paths sin cargar el paquete completo.
  • external: ["react", "react-dom"] asegura que React no se empaquete -- la aplicacion consumidora proporciona su propia copia.
  • splitting: true habilita code splitting dentro de la salida ESM para que los modulos internos compartidos no se dupliquen.
  • dts: true genera archivos .d.ts junto a cada salida, dando a los consumidores IntelliSense completo.

Mapa de exports en package.json

El campo exports usa conditional exports de Node.js para dirigir a bundlers y runtimes al formato correcto:

{
  "name": "@org/design-system",
  "version": "2.4.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "sideEffects": false,
  "exports": {
    ".": {
      "import": {
        "types": "./dist/index.d.ts",
        "default": "./dist/index.js"
      },
      "require": {
        "types": "./dist/index.d.cts",
        "default": "./dist/index.cjs"
      }
    },
    "./tokens": {
      "import": {
        "types": "./dist/tokens.d.ts",
        "default": "./dist/tokens.js"
      },
      "require": {
        "types": "./dist/tokens.d.cts",
        "default": "./dist/tokens.cjs"
      }
    },
    "./hooks": {
      "import": {
        "types": "./dist/hooks.d.ts",
        "default": "./dist/hooks.js"
      },
      "require": {
        "types": "./dist/hooks.d.cts",
        "default": "./dist/hooks.cjs"
      }
    },
    "./utils": {
      "import": {
        "types": "./dist/utils.d.ts",
        "default": "./dist/utils.js"
      },
      "require": {
        "types": "./dist/utils.d.cts",
        "default": "./dist/utils.cjs"
      }
    },
    "./types": {
      "import": {
        "types": "./dist/types.d.ts",
        "default": "./dist/types.js"
      },
      "require": {
        "types": "./dist/types.d.cts",
        "default": "./dist/types.cjs"
      }
    },
    "./styles.css": "./dist/styles.css"
  },
  "files": [
    "dist",
    "CHANGELOG.md"
  ],
  "scripts": {
    "build:npm": "tsup",
    "build:mf": "rsbuild build",
    "build": "pnpm build:npm && pnpm build:mf",
    "dev": "rsbuild dev",
    "test": "vitest run",
    "test:watch": "vitest",
    "lint": "eslint src/",
    "typecheck": "tsc --noEmit",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build"
  },
  "peerDependencies": {
    "react": "^19.2.4",
    "react-dom": "^19.2.4"
  },
  "devDependencies": {
    "@rsbuild/core": "^1.7.3",
    "@module-federation/rsbuild-plugin": "^2.0.1",
    "@module-federation/enhanced": "^2.0.1",
    "@storybook/react-vite": "^10.2.10",
    "tsup": "^8.3.0",
    "typescript": "^5.9.3",
    "vitest": "^4.0.18",
    "@testing-library/react": "^16.1.0"
  }
}

La declaracion "sideEffects": false indica a los bundlers que cualquier export no usado puede eliminarse de forma segura via tree-shaking. El export separado "./styles.css" da a los consumidores un path explicito de opt-in para la hoja de estilos base si la necesitan.

Importante: CSS Modules y sideEffects. Aunque "sideEffects": false es correcto para exports puros de JavaScript y TypeScript, los CSS Modules son side effects -- importar un archivo .module.css provoca la inyeccion de estilos en el documento, y los bundlers pueden eliminarlos incorrectamente via tree-shaking si no estan listados de forma explicita. Si el paquete incluye salidas CSS que los consumidores importan directamente, hay que actualizar el campo para declarar los archivos CSS como side effects:

"sideEffects": ["**/*.css"]

Esto asegura que los bundlers preserven los imports CSS durante el tree-shaking mientras siguen eliminando los exports JavaScript no usados.

Module Federation Remote

La configuracion de Rsbuild expone componentes React listos para runtime via Module Federation v2. Son cargados por la aplicacion shell y los MFEs en runtime -- sin necesidad de npm install ni rebuild del lado del consumidor.

rsbuild.config.ts

import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { pluginModuleFederation } from "@module-federation/rsbuild-plugin";

export default defineConfig({
  plugins: [
    pluginReact(),
    pluginModuleFederation({
      name: "design_system",
      filename: "remoteEntry.js",
      exposes: {
        "./Button": "./src/components/Button/index.ts",
        "./Modal": "./src/components/Modal/index.ts",
        "./Typography": "./src/components/Typography/index.ts",
        "./ThemeProvider": "./src/components/ThemeProvider/index.ts",
        "./hooks": "./src/hooks/index.ts",
        "./utils": "./src/utils/index.ts",
      },
      shared: {
        react: {
          singleton: true,
          requiredVersion: "^19.2.4",
          eager: false,
        },
        "react-dom": {
          singleton: true,
          requiredVersion: "^19.2.4",
          eager: false,
        },
      },
      manifest: {
        filePath: "mf-manifest.json",
      },
    }),
  ],

  output: {
    assetPrefix: "auto",
    cleanDistPath: true,
    distPath: {
      root: "dist-mf",
    },
    filename: {
      js: "[name].[contenthash:8].js",
      css: "[name].[contenthash:8].css",
    },
  },

  server: {
    port: 3010,
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
  },

  performance: {
    chunkSplit: {
      strategy: "split-by-experience",
    },
  },
});

Decisiones clave:

  • singleton: true para React y ReactDOM asegura que cada MFE comparta exactamente una instancia de React. Sin esto, los hooks fallan por multiples copias de React.
  • manifest: { filePath: "mf-manifest.json" } genera un archivo manifest que el shell lee en runtime para descubrir los modulos disponibles y sus URLs de assets. Este manifest se despliega al CDN junto con los assets compilados.
  • Directorio de salida separado (dist-mf) mantiene los artefactos del build MF separados de la distribucion npm en dist/, evitando interferencias entre ambas pipelines.
  • Filenames con content-hash habilitan caching agresivo en CDN -- los assets son inmutables y se invalidan automaticamente en cada cambio.
  • assetPrefix: "auto" permite al runtime resolver URLs de assets relativas a la ubicacion de remoteEntry.js, lo cual es critico cuando los assets se sirven desde un CDN con un origen diferente al de la aplicacion host.

Versionado con Changesets

El monorepo usa Changesets para versionado semantico independiente de cada paquete. Cada paquete en el workspace puede versionarse y publicarse de forma independiente, de modo que un patch al design system no fuerza un bump de version en paquetes no relacionados.

Flujo de versionado con Changesets — changeset, version, publish, Renovate Flujo de versionado con Changesets changesetDev agrega .md Version PRCI crea PR PublishGitHub Packages RenovatePR automatico a MFEs MFE actualizadoEquipo merge PR de Renovate Versionado independiente — cada paquete se bumpa por separado · CHANGELOG generado automaticamente Los MFEs en polyrepos consumen actualizaciones via PRs de Renovate (sin coordinacion manual de versiones)

Configuracion

.changeset/config.json

{
  "$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json",
  "changelog": [
    "@changesets/changelog-github",
    { "repo": "org/micro-frontends-platform" }
  ],
  "commit": false,
  "fixed": [],
  "linked": [],
  "access": "restricted",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": [],
  "privatePackages": {
    "version": false,
    "tag": false
  }
}

Notas de configuracion:

  • "access": "restricted" porque los paquetes se publican en GitHub Packages bajo el scope de la organizacion, no en el registro publico de npm.
  • "updateInternalDependencies": "patch" asegura que cuando un paquete compartido como el design system obtiene una nueva version, cualquier paquete del monorepo que dependa de el tenga su rango de dependencia actualizado automaticamente.
  • "changelog": ["@changesets/changelog-github", ...] genera changelogs que enlazan al PR y commit en GitHub, facilitando rastrear por que se hizo un cambio.
  • "privatePackages": { "version": false, "tag": false } previene que los paquetes privados (como la app shell, que se despliega pero nunca se publica a npm) sean versionados o tagueados por Changesets.

Flujo de trabajo

El flujo de release tiene cuatro etapas:

  1. El desarrollador agrega un changeset. Cuando un PR modifica el design system, el desarrollador ejecuta pnpm changeset y selecciona los paquetes afectados, el tipo de bump semver (patch, minor, major), y escribe un resumen legible.

  2. El archivo changeset se commitea con el PR. El changeset es un pequeno archivo Markdown en .changeset/ que registra la intencion:

.changeset/blue-dogs-dance.md (ejemplo)

---
"@org/design-system": minor
---

Add new `Tooltip` component with configurable placement and delay. Supports both hover
and focus triggers for accessibility compliance.
  1. La GitHub Action de Changesets crea un PR de release. Despues de que el PR de feature se mergea a main, el bot de Changesets agrega todos los changesets pendientes en un unico PR "Version Packages". Este PR actualiza versiones en package.json, escribe entradas en CHANGELOG.md y elimina los archivos changeset consumidos.

  2. Mergear el PR de release publica a npm. Cuando el equipo mergea el PR "Version Packages", un workflow de CI ejecuta changeset publish, que publica la nueva version a GitHub Packages y crea un Git tag.

Como saben los equipos MFE consumidores cuando actualizar

Para MFEs en polyrepos externos que dependen de @org/design-system como paquete npm (para tipos y tokens), Renovate Bot esta configurado para monitorear GitHub Packages y crear PRs automaticamente cuando se publica una nueva version. Los equipos revisan el PR autogenerado, verifican que sus tipos sigan compilando y lo mergean.

Para el Module Federation remote, los MFEs no necesitan hacer nada. El shell carga el ultimo design system remote en runtime basandose en el MF manifest, de modo que los componentes runtime se actualizan automaticamente con cada deploy del design system.


Publicacion en GitHub Packages

Todos los paquetes del monorepo se publican en GitHub Packages bajo el scope de la organizacion @org.

Configuracion del registro

.npmrc en la raiz del monorepo

@org:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
always-auth=true

.npmrc del polyrepo consumidor

@org:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}

El GITHUB_TOKEN se inyecta como variable de entorno -- en CI proviene del secrets.GITHUB_TOKEN de GitHub Actions o de un PAT de grano fino, y en local los desarrolladores usan un PAT almacenado en su perfil de shell o un gestor de credenciales.

Workflow de publicacion CI/CD

.github/workflows/release.yml

name: Release Packages

on:
  push:
    branches:
      - main

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: false

permissions:
  contents: write
  packages: write
  pull-requests: write

jobs:
  release:
    name: Version & Publish
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Setup pnpm
        uses: pnpm/action-setup@v4

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: "pnpm"
          registry-url: "https://npm.pkg.github.com"

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build all packages
        run: pnpm turbo run build --filter="./packages/*"

      - name: Run tests
        run: pnpm turbo run test --filter="./packages/*"

      - name: Create release PR or publish
        id: changesets
        uses: changesets/action@v1
        with:
          version: pnpm changeset version
          publish: pnpm changeset publish
          title: "chore: version packages"
          commit: "chore: version packages"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Deploy design system MF remote
        if: steps.changesets.outputs.published == 'true'
        run: |
          # Check if design-system was among the published packages
          PUBLISHED=$(echo '${{ steps.changesets.outputs.publishedPackages }}' | jq -r '.[] | select(.name == "@org/design-system") | .name')
          if [ -n "$PUBLISHED" ]; then
            echo "Design system was published, deploying MF remote..."
            pnpm --filter @org/design-system build:mf
            pnpm turbo run deploy:mf --filter=@org/design-system
          fi

El workflow maneja dos escenarios en cada push a main:

  • Si hay changesets pendientes: La action crea (o actualiza) un PR "Version Packages" que bumpa versiones y actualiza changelogs. No se publica ningun paquete todavia.
  • Si no hay changesets pendientes (es decir, el PR "Version Packages" se acaba de mergear): La action ejecuta changeset publish, que publica cada paquete con nueva version a GitHub Packages y crea Git tags.

El paso final despliega condicionalmente el Module Federation remote del design system solo cuando el propio design system esta entre los paquetes publicados.


Consumo del Design System

Como paquete npm (build-time)

Los equipos MFE instalan el design system como dev dependency (ya que los componentes runtime vienen via Module Federation, el paquete npm solo se necesita durante el desarrollo y CI):

pnpm add -D @org/design-system

Importando tipos y props de componentes:

import type { ButtonProps, ModalProps, TypographyProps } from "@org/design-system";
import type { Theme, ThemeMode } from "@org/design-system/types";

// Full IntelliSense and type-checking for component props
const buttonConfig: ButtonProps = {
  variant: "primary",
  size: "md",
  disabled: false,
  onClick: () => handleSubmit(),
};

Importando design tokens:

import { colors, spacing, typography, breakpoints } from "@org/design-system/tokens";

// Use tokens in styled-components, CSS-in-JS, or Tailwind config
const customStyles = {
  padding: spacing.md,         // "16px"
  color: colors.primary[600],  // "#2563EB"
  fontSize: typography.body.fontSize, // "1rem"
};

// Or use tokens to configure a Tailwind theme
// tailwind.config.ts
export default {
  theme: {
    extend: {
      colors: colors,
      spacing: spacing,
    },
  },
};

Importando hooks compartidos:

import { useMediaQuery, useClickOutside } from "@org/design-system/hooks";

function Sidebar() {
  const isMobile = useMediaQuery(`(max-width: ${breakpoints.md})`);
  const sidebarRef = useClickOutside<HTMLDivElement>(() => closeSidebar());

  return (
    <aside ref={sidebarRef} className={isMobile ? "overlay" : "docked"}>
      {/* ... */}
    </aside>
  );
}

Importando funciones utilitarias:

import { cn, meetsContrastRatio } from "@org/design-system/utils";

// cn() merges class names, handling conditional classes cleanly
<div className={cn("card", isActive && "card--active", className)} />

// Accessibility: check if custom theme colors meet WCAG AA
const isAccessible = meetsContrastRatio(foreground, background, "AA");

Como Module Federation Remote (runtime)

En runtime, los componentes React reales se cargan via Module Federation. La aplicacion shell registra el design system como remote en su configuracion:

rsbuild.config.ts del Shell (extracto relevante):

pluginModuleFederation({
  name: "shell",
  remotes: {
    design_system: "design_system@https://cdn.example.com/design-system/mf-manifest.json",
  },
  shared: {
    react: { singleton: true, requiredVersion: "^19.2.4" },
    "react-dom": { singleton: true, requiredVersion: "^19.2.4" },
  },
}),

Import dinamico directo en un MFE:

import { loadRemote } from "@module-federation/enhanced/runtime";
import type { ButtonProps } from "@org/design-system";

// loadRemote returns the module at runtime from the MF remote
const ButtonModule = await loadRemote<{ default: React.ComponentType<ButtonProps> }>(
  "design_system/Button"
);
const Button = ButtonModule.default;

Patron de paquete wrapper ligero

Para ofrecer una experiencia de desarrollo limpia que se sienta como importar de un paquete regular, un paquete wrapper ligero reexporta cada componente del design system como un componente React lazy-loaded. Los desarrolladores de MFEs nunca necesitan llamar a loadRemote directamente.

packages/design-system-remote/src/Button.tsx

import React, { lazy, Suspense } from "react";
import { loadRemote } from "@module-federation/enhanced/runtime";
import type { ButtonProps } from "@org/design-system";

const RemoteButton = lazy(async () => {
  const module = await loadRemote<{ default: React.ComponentType<ButtonProps> }>(
    "design_system/Button"
  );
  if (!module) {
    throw new Error("Failed to load design_system/Button remote module");
  }
  return { default: module.default };
});

/**
 * Button component loaded at runtime from the design system MF remote.
 * Accepts the same props as the design system Button.
 * Renders a skeleton placeholder while the remote chunk is loading.
 */
export function Button(props: ButtonProps) {
  return (
    <Suspense fallback={<ButtonSkeleton size={props.size} />}>
      <RemoteButton {...props} />
    </Suspense>
  );
}

function ButtonSkeleton({ size = "md" }: { size?: ButtonProps["size"] }) {
  const heightMap = { sm: "32px", md: "40px", lg: "48px" } as const;
  return (
    <div
      role="presentation"
      aria-hidden="true"
      style={{
        height: heightMap[size ?? "md"],
        width: "120px",
        borderRadius: "6px",
        backgroundColor: "var(--ds-color-neutral-100)",
        animation: "pulse 1.5s ease-in-out infinite",
      }}
    />
  );
}

export type { ButtonProps };

Uso en un MFE (se siente como un import regular):

import { Button } from "@org/design-system-remote";
import { Modal } from "@org/design-system-remote";

function CheckoutPage() {
  return (
    <div>
      <h1>Checkout</h1>
      <Button variant="primary" size="lg" onClick={handlePay}>
        Complete Purchase
      </Button>
      <Modal open={showTerms} onClose={() => setShowTerms(false)}>
        <p>Terms and conditions...</p>
      </Modal>
    </div>
  );
}

Este patron da a los desarrolladores de MFEs:

  • Seguridad de tipos -- los props se verifican en build-time via los tipos de @org/design-system.
  • Code splitting automatico -- los componentes se cargan bajo demanda.
  • Estados de carga elegantes -- cada wrapper incluye un skeleton fallback construido a medida.
  • Deploys desacoplados -- el equipo del design system puede publicar actualizaciones visuales sin que los equipos MFE necesiten rebuild.

Theming y estrategia CSS

Estructura de design tokens

Los design tokens son la fuente unica de verdad para las decisiones visuales. Se definen como constantes TypeScript y se compilan a CSS custom properties en build-time.

src/tokens/colors.ts

export const colors = {
  primary: {
    50: "#EFF6FF",
    100: "#DBEAFE",
    200: "#BFDBFE",
    300: "#93C5FD",
    400: "#60A5FA",
    500: "#3B82F6",
    600: "#2563EB",
    700: "#1D4ED8",
    800: "#1E40AF",
    900: "#1E3A8A",
    950: "#172554",
  },
  neutral: {
    50: "#F9FAFB",
    100: "#F3F4F6",
    200: "#E5E7EB",
    300: "#D1D5DB",
    400: "#9CA3AF",
    500: "#6B7280",
    600: "#4B5563",
    700: "#374151",
    800: "#1F2937",
    900: "#111827",
    950: "#030712",
  },
  success: {
    500: "#22C55E",
    600: "#16A34A",
    700: "#15803D",
  },
  warning: {
    500: "#F59E0B",
    600: "#D97706",
    700: "#B45309",
  },
  error: {
    500: "#EF4444",
    600: "#DC2626",
    700: "#B91C1C",
  },
} as const;

export type Colors = typeof colors;

src/tokens/spacing.ts

export const spacing = {
  px: "1px",
  0: "0",
  0.5: "2px",
  1: "4px",
  1.5: "6px",
  2: "8px",
  2.5: "10px",
  3: "12px",
  4: "16px",
  5: "20px",
  6: "24px",
  8: "32px",
  10: "40px",
  12: "48px",
  16: "64px",
  20: "80px",
  24: "96px",
} as const;

// Semantic aliases for common use cases
export const space = {
  xs: spacing[1],   // 4px
  sm: spacing[2],   // 8px
  md: spacing[4],   // 16px
  lg: spacing[6],   // 24px
  xl: spacing[8],   // 32px
  "2xl": spacing[12], // 48px
} as const;

export type Spacing = typeof spacing;
export type Space = typeof space;

Generacion de CSS custom properties

Los tokens se compilan a CSS custom properties para que los componentes puedan referenciarlos en CSS Modules sin importar JavaScript:

src/tokens/css-variables.ts (utilidad de build-time)

import { colors } from "./colors";
import { spacing, space } from "./spacing";
import { typography } from "./typography";

type NestedRecord = { [key: string]: string | NestedRecord };

function flattenTokens(obj: NestedRecord, prefix: string): Record<string, string> {
  const result: Record<string, string> = {};

  for (const [key, value] of Object.entries(obj)) {
    const varName = `${prefix}-${key}`;
    if (typeof value === "string") {
      result[varName] = value;
    } else {
      Object.assign(result, flattenTokens(value, varName));
    }
  }

  return result;
}

export function generateCSSVariables(): string {
  const allTokens: Record<string, string> = {
    ...flattenTokens(colors as unknown as NestedRecord, "--ds-color"),
    ...flattenTokens(spacing as unknown as NestedRecord, "--ds-spacing"),
    ...flattenTokens(space as unknown as NestedRecord, "--ds-space"),
    ...flattenTokens(typography as unknown as NestedRecord, "--ds-typography"),
  };

  const declarations = Object.entries(allTokens)
    .map(([name, value]) => `  ${name}: ${value};`)
    .join("\n");

  return `:root {\n${declarations}\n}`;
}

Esto genera CSS como:

:root {
  --ds-color-primary-50: #EFF6FF;
  --ds-color-primary-100: #DBEAFE;
  /* ... */
  --ds-space-xs: 4px;
  --ds-space-sm: 8px;
  --ds-space-md: 16px;
  /* ... */
}

Componente ThemeProvider

Flujo de contexto del ThemeProvider — Shell a ThemeProvider a componentes MFE Flujo de contexto del ThemeProvider Shell App <ThemeProvider> Gestiona estado del tema CSS custom properties Modo claro / oscuro context ThemeContext hook useTheme() { theme, setTheme } MFE A: Button MFE B: Modal MFE C: Card CSS Custom Properties --color-primary: #2764F4 --color-background: #FBFCFD --spacing-md: 16px Aplicadas al elemento :root

El ThemeProvider gestiona el estado del tema y aplica CSS custom properties a un elemento raiz. Soporta tanto deteccion de preferencia del sistema como overrides manuales.

src/components/ThemeProvider/ThemeProvider.tsx

import React, {
  createContext,
  useCallback,
  useEffect,
  useMemo,
  useState,
} from "react";
import type { ReactNode } from "react";

export type ThemeMode = "light" | "dark" | "system";

interface ThemeContextValue {
  /** The user's preference: "light", "dark", or "system". */
  mode: ThemeMode;
  /** The resolved mode after evaluating system preference. Always "light" or "dark". */
  resolvedMode: "light" | "dark";
  /** Update the theme mode. */
  setMode: (mode: ThemeMode) => void;
}

export const ThemeContext = createContext<ThemeContextValue | null>(null);

const STORAGE_KEY = "ds-theme-mode";

// Semantic token overrides per theme
const lightTokens: Record<string, string> = {
  "--ds-bg-primary": "var(--ds-color-neutral-50)",
  "--ds-bg-secondary": "var(--ds-color-neutral-100)",
  "--ds-bg-surface": "#FFFFFF",
  "--ds-text-primary": "var(--ds-color-neutral-900)",
  "--ds-text-secondary": "var(--ds-color-neutral-600)",
  "--ds-text-muted": "var(--ds-color-neutral-400)",
  "--ds-border-default": "var(--ds-color-neutral-200)",
  "--ds-border-strong": "var(--ds-color-neutral-300)",
  "--ds-shadow-sm": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
  "--ds-shadow-md": "0 4px 6px -1px rgba(0, 0, 0, 0.1)",
};

const darkTokens: Record<string, string> = {
  "--ds-bg-primary": "var(--ds-color-neutral-900)",
  "--ds-bg-secondary": "var(--ds-color-neutral-800)",
  "--ds-bg-surface": "var(--ds-color-neutral-950)",
  "--ds-text-primary": "var(--ds-color-neutral-50)",
  "--ds-text-secondary": "var(--ds-color-neutral-300)",
  "--ds-text-muted": "var(--ds-color-neutral-500)",
  "--ds-border-default": "var(--ds-color-neutral-700)",
  "--ds-border-strong": "var(--ds-color-neutral-600)",
  "--ds-shadow-sm": "0 1px 2px 0 rgba(0, 0, 0, 0.3)",
  "--ds-shadow-md": "0 4px 6px -1px rgba(0, 0, 0, 0.4)",
};

function getSystemPreference(): "light" | "dark" {
  if (typeof window === "undefined") return "light";
  return window.matchMedia("(prefers-color-scheme: dark)").matches
    ? "dark"
    : "light";
}

function resolveMode(mode: ThemeMode): "light" | "dark" {
  return mode === "system" ? getSystemPreference() : mode;
}

interface ThemeProviderProps {
  children: ReactNode;
  /** Initial theme mode. Defaults to "system". */
  defaultMode?: ThemeMode;
  /** CSS selector for the element that receives theme CSS variables. Defaults to ":root". */
  targetSelector?: string;
}

export function ThemeProvider({
  children,
  defaultMode = "system",
  targetSelector = ":root",
}: ThemeProviderProps) {
  const [mode, setModeState] = useState<ThemeMode>(() => {
    if (typeof window === "undefined") return defaultMode;
    return (localStorage.getItem(STORAGE_KEY) as ThemeMode) ?? defaultMode;
  });

  const [resolvedMode, setResolvedMode] = useState<"light" | "dark">(() =>
    resolveMode(mode)
  );

  // Apply CSS variables to the target element
  const applyTheme = useCallback(
    (resolved: "light" | "dark") => {
      const target =
        targetSelector === ":root"
          ? document.documentElement
          : document.querySelector(targetSelector);

      if (!target || !(target instanceof HTMLElement)) return;

      const tokens = resolved === "dark" ? darkTokens : lightTokens;
      for (const [prop, value] of Object.entries(tokens)) {
        target.style.setProperty(prop, value);
      }

      target.setAttribute("data-theme", resolved);
    },
    [targetSelector]
  );

  // Listen for system preference changes when mode is "system"
  useEffect(() => {
    const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");

    function handleChange() {
      if (mode === "system") {
        const newResolved = getSystemPreference();
        setResolvedMode(newResolved);
        applyTheme(newResolved);
      }
    }

    mediaQuery.addEventListener("change", handleChange);
    return () => mediaQuery.removeEventListener("change", handleChange);
  }, [mode, applyTheme]);

  // Apply theme whenever the resolved mode changes
  useEffect(() => {
    const resolved = resolveMode(mode);
    setResolvedMode(resolved);
    applyTheme(resolved);
  }, [mode, applyTheme]);

  const setMode = useCallback((newMode: ThemeMode) => {
    setModeState(newMode);
    localStorage.setItem(STORAGE_KEY, newMode);
  }, []);

  const contextValue = useMemo<ThemeContextValue>(
    () => ({ mode, resolvedMode, setMode }),
    [mode, resolvedMode, setMode]
  );

  return (
    <ThemeContext.Provider value={contextValue}>
      {children}
    </ThemeContext.Provider>
  );
}

src/hooks/useTheme.ts

import { useContext } from "react";
import { ThemeContext } from "../components/ThemeProvider/ThemeProvider";
import type { ThemeMode } from "../components/ThemeProvider/ThemeProvider";

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error("useTheme must be used within a <ThemeProvider>");
  }
  return context;
}

export type { ThemeMode };

CSS Modules para scope de componentes

Cada componente usa CSS Modules para evitar colisiones de estilos entre MFEs. Los nombres de clase se hashean en build-time, asegurando que una clase .button en el design system nunca pueda entrar en conflicto con una clase .button en un MFE.

src/components/Button/Button.module.css

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--ds-space-xs);
  border: 1px solid transparent;
  border-radius: 6px;
  font-family: var(--ds-typography-fontFamily);
  font-weight: 500;
  cursor: pointer;
  transition: background-color 150ms ease, border-color 150ms ease,
    box-shadow 150ms ease;
}

.button:focus-visible {
  outline: 2px solid var(--ds-color-primary-500);
  outline-offset: 2px;
}

.button:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

/* Size variants */
.sm {
  height: 32px;
  padding: 0 var(--ds-space-sm);
  font-size: 0.875rem;
}

.md {
  height: 40px;
  padding: 0 var(--ds-space-md);
  font-size: 0.875rem;
}

.lg {
  height: 48px;
  padding: 0 var(--ds-space-lg);
  font-size: 1rem;
}

/* Visual variants */
.primary {
  background-color: var(--ds-color-primary-600);
  color: white;
}

.primary:hover:not(:disabled) {
  background-color: var(--ds-color-primary-700);
}

.secondary {
  background-color: var(--ds-bg-surface);
  color: var(--ds-text-primary);
  border-color: var(--ds-border-default);
}

.secondary:hover:not(:disabled) {
  background-color: var(--ds-bg-secondary);
  border-color: var(--ds-border-strong);
}

.ghost {
  background-color: transparent;
  color: var(--ds-text-primary);
}

.ghost:hover:not(:disabled) {
  background-color: var(--ds-bg-secondary);
}

src/components/Button/Button.tsx

import React, { forwardRef } from "react";
import { cn } from "../../utils/cn";
import styles from "./Button.module.css";

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  /** Visual variant of the button. */
  variant?: "primary" | "secondary" | "ghost" | "danger";
  /** Size of the button. */
  size?: "sm" | "md" | "lg";
  /** Show a loading spinner and disable interaction. */
  loading?: boolean;
  /** Icon to render before the label. */
  leftIcon?: React.ReactNode;
  /** Icon to render after the label. */
  rightIcon?: React.ReactNode;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      variant = "primary",
      size = "md",
      loading = false,
      leftIcon,
      rightIcon,
      disabled,
      className,
      children,
      ...rest
    },
    ref
  ) => {
    return (
      <button
        ref={ref}
        className={cn(
          styles.button,
          styles[variant],
          styles[size],
          loading && styles.loading,
          className
        )}
        disabled={disabled || loading}
        aria-busy={loading || undefined}
        {...rest}
      >
        {loading ? (
          <Spinner size={size} />
        ) : (
          <>
            {leftIcon && <span className={styles.icon}>{leftIcon}</span>}
            {children}
            {rightIcon && <span className={styles.icon}>{rightIcon}</span>}
          </>
        )}
      </button>
    );
  }
);

Button.displayName = "Button";

function Spinner({ size }: { size: "sm" | "md" | "lg" }) {
  const sizeMap = { sm: 14, md: 16, lg: 20 };
  const pixels = sizeMap[size];
  return (
    <svg
      className={styles.spinner}
      width={pixels}
      height={pixels}
      viewBox="0 0 24 24"
      fill="none"
      aria-hidden="true"
    >
      <circle
        cx="12"
        cy="12"
        r="10"
        stroke="currentColor"
        strokeWidth="3"
        strokeLinecap="round"
        strokeDasharray="60"
        strokeDashoffset="20"
      />
    </svg>
  );
}

Estrategia de modo oscuro

El modo oscuro se gestiona mediante dos mecanismos complementarios:

  1. Deteccion de preferencia del sistema -- El ThemeProvider escucha prefers-color-scheme via matchMedia y resuelve automaticamente a la preferencia del SO del usuario cuando el modo esta configurado como "system".

  2. Toggle manual -- Los usuarios pueden sobreescribir la preferencia del sistema con una eleccion explicita de "light" o "dark", que se persiste en localStorage y sobrevive a recargas de pagina.

Todos los tokens semanticos (fondos, colores de texto, bordes, sombras) se definen como CSS custom properties que cambian segun el tema activo. Los componentes referencian estos tokens semanticos en lugar de valores de color crudos, de modo que el soporte de modo oscuro es automatico para cada componente.

/* Components reference semantic tokens, not raw colors */
.card {
  background-color: var(--ds-bg-surface);   /* white in light, neutral-950 in dark */
  color: var(--ds-text-primary);            /* neutral-900 in light, neutral-50 in dark */
  border: 1px solid var(--ds-border-default);
  box-shadow: var(--ds-shadow-sm);
}

Storybook

Storybook 10 sirve como documentacion interactiva, entorno de testing visual y playground de componentes para el design system.

Configuracion

.storybook/main.ts

import type { StorybookConfig } from "@storybook/react-vite";

const config: StorybookConfig = {
  stories: ["../src/**/*.stories.@(ts|tsx)"],
  addons: [
    "@storybook/addon-essentials",
    "@storybook/addon-a11y",
    "@storybook/addon-interactions",
    "@storybook/addon-themes",
  ],
  framework: {
    name: "@storybook/react-vite",
    options: {},
  },
  typescript: {
    reactDocgen: "react-docgen-typescript",
  },
  viteFinal(config) {
    return config;
  },
};

export default config;

.storybook/preview.ts

import type { Preview } from "@storybook/react";
import { withThemeByDataAttribute } from "@storybook/addon-themes";
import "../src/styles/globals.css";

const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
    layout: "centered",
  },
  decorators: [
    withThemeByDataAttribute({
      themes: {
        light: "light",
        dark: "dark",
      },
      defaultTheme: "light",
      attributeName: "data-theme",
    }),
  ],
};

export default preview;

Ejemplo de story

Las stories se organizan por componente, con cada archivo de story demostrando todas las variantes, estados y patrones de interaccion.

src/components/Button/Button.stories.tsx

import type { Meta, StoryObj } from "@storybook/react";
import { expect, userEvent, within } from "@storybook/test";
import { Button } from "./Button";

const meta: Meta<typeof Button> = {
  title: "Components/Button",
  component: Button,
  tags: ["autodocs"],
  argTypes: {
    variant: {
      control: "select",
      options: ["primary", "secondary", "ghost", "danger"],
      description: "The visual style of the button",
    },
    size: {
      control: "select",
      options: ["sm", "md", "lg"],
      description: "The size of the button",
    },
    loading: {
      control: "boolean",
      description: "Shows a loading spinner and disables interaction",
    },
    disabled: {
      control: "boolean",
      description: "Disables the button",
    },
  },
  args: {
    children: "Button",
    variant: "primary",
    size: "md",
  },
};

export default meta;
type Story = StoryObj<typeof Button>;

// ---- Variant stories ----

export const Primary: Story = {
  args: {
    variant: "primary",
    children: "Primary Action",
  },
};

export const Secondary: Story = {
  args: {
    variant: "secondary",
    children: "Secondary Action",
  },
};

export const Ghost: Story = {
  args: {
    variant: "ghost",
    children: "Ghost Action",
  },
};

export const Danger: Story = {
  args: {
    variant: "danger",
    children: "Delete Item",
  },
};

// ---- Size stories ----

export const Small: Story = {
  args: { size: "sm", children: "Small" },
};

export const Medium: Story = {
  args: { size: "md", children: "Medium" },
};

export const Large: Story = {
  args: { size: "lg", children: "Large" },
};

// ---- State stories ----

export const Loading: Story = {
  args: {
    loading: true,
    children: "Saving...",
  },
};

export const Disabled: Story = {
  args: {
    disabled: true,
    children: "Disabled",
  },
};

// ---- All variants side-by-side ----

export const AllVariants: Story = {
  render: () => (
    <div style={{ display: "flex", gap: "12px", alignItems: "center" }}>
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="danger">Danger</Button>
    </div>
  ),
};

export const AllSizes: Story = {
  render: () => (
    <div style={{ display: "flex", gap: "12px", alignItems: "center" }}>
      <Button size="sm">Small</Button>
      <Button size="md">Medium</Button>
      <Button size="lg">Large</Button>
    </div>
  ),
};

// ---- Interaction test ----

export const ClickInteraction: Story = {
  args: {
    children: "Click Me",
  },
  play: async ({ canvasElement, args }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole("button", { name: /click me/i });

    await userEvent.click(button);

    // Verify the button is still in the document after click
    await expect(button).toBeInTheDocument();
    await expect(button).not.toBeDisabled();
  },
};

export const DisabledInteraction: Story = {
  args: {
    children: "Cannot Click",
    disabled: true,
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole("button", { name: /cannot click/i });

    await expect(button).toBeDisabled();
  },
};

Testing de regresion visual con Chromatic

Pipeline de Storybook + testing de regresion visual Pipeline de Storybook + testing de regresion visual Escribir Story*.stories.tsx PR abiertoGitHub Actions ChromaticCaptura de stories Diff de pixelesCompara con baseline Revision + AprobacionCambios visuales aceptados Storybook desplegado como sitio estatico → Documentacion interactiva para todos los equipos → Playground de componentes

Para equipos que quieran testing de regresion visual automatizado, Chromatic se integra con Storybook para capturar screenshots de cada story en cada PR:

.github/workflows/chromatic.yml

name: Chromatic Visual Tests

on:
  pull_request:
    paths:
      - "packages/design-system/src/**"

jobs:
  chromatic:
    name: Visual Regression
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Setup pnpm
        uses: pnpm/action-setup@v4

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: "pnpm"

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run Chromatic
        uses: chromaui/action@latest
        with:
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
          workingDir: packages/design-system
          buildScriptName: build-storybook
          onlyChanged: true
          exitZeroOnChanges: true

Deploy de Storybook

El Storybook compilado se despliega como sitio estatico (por ejemplo, en Cloudflare Pages o GitHub Pages) en cada merge a main, proporcionando una URL de referencia persistente para todos los equipos:

https://design-system.example.com

Esta URL se enlaza desde el README del monorepo, la documentacion de onboarding y el campo homepage del package.json del design system.


Testing de sincronizacion entre distribuciones

Dado que el design system se distribuye a traves de dos canales independientes (paquete npm y Module Federation remote), es importante verificar que ambas salidas se mantengan consistentes y produzcan los mismos resultados visuales y de comportamiento. Un drift entre las dos distribuciones puede causar bugs sutiles donde los componentes se ven o se comportan de forma diferente segun como los carga el MFE consumidor.

Estrategia de testing recomendada

  1. Tests de regresion visual en ambas distribuciones. Extender la pipeline de Chromatic (o equivalente de regresion visual) para capturar screenshots de componentes renderizados tanto desde el build del paquete npm como desde el build del MF remote. Comparar los snapshots para asegurar consistencia a nivel de pixel. Esto se puede lograr manteniendo una pequena aplicacion de test harness que importe componentes de ambas formas y los ejecute a traves de las mismas stories de Storybook.

  2. Comparaciones de snapshots en CI. Agregar un paso de CI que construya ambas distribuciones (build:npm y build:mf), renderice un conjunto representativo de componentes de cada una y compare la salida HTML o los arboles de componentes serializados. Cualquier diff inesperado debe romper el build.

  3. Suite de tests de integracion. Mantener un test de integracion ligero que monte componentes clave desde el paquete npm y desde el MF remote en paralelo, verificando que su salida renderizada, estilos aplicados y comportamiento interactivo (click handlers, navegacion por teclado) sean equivalentes.

  4. Deteccion automatizada de drift. Como parte del workflow de release, ejecutar los tests de sincronizacion antes de publicar. Si se detecta una discrepancia visual o de comportamiento entre las distribuciones npm y MF, bloquear el release hasta que se resuelva el problema.


Comunicacion de breaking changes

Cuando el design system introduce breaking changes, una comunicacion clara y estructurada es esencial para minimizar la disrupcion a los equipos MFE consumidores.

Directrices

  • Warnings de deprecacion primero. Antes de eliminar o alterar una API publica, agregar warnings de deprecacion en runtime (por ejemplo, console.warn) en la version actual. Estos warnings deben referenciar la version especifica que eliminara la funcionalidad deprecated y sugerir la ruta de migracion. Esto da a los equipos consumidores al menos un ciclo de release minor para prepararse.

  • Guias de migracion. Para cada bump de version major o breaking change significativo, publicar una guia de migracion junto con el release. La guia debe incluir:

    • Un resumen de que cambio y por que.
    • Ejemplos de codigo antes/despues mostrando la API antigua y la nueva.
    • Pasos para actualizar, incluyendo scripts de codemod si corresponde.
    • Casos borde conocidos o gotchas.
  • Notas de release versionadas. Usar el CHANGELOG.md generado por Changesets como registro principal de cambios. Para releases major, complementar el changelog con un documento de migracion dedicado en el repositorio (por ejemplo, docs/migrations/v3-to-v4.md).

  • Canales de comunicacion. Anunciar breaking changes a traves de los canales establecidos del equipo (por ejemplo, Slack, email o un newsletter interno de desarrolladores) antes de que se publique el release. Incluir un enlace a la guia de migracion y un timeline estimado para el periodo de deprecacion.

  • Rollout coordinado para MF remotes. Dado que los Module Federation remotes se actualizan en runtime sin rebuilds de MFEs, los breaking changes en componentes runtime requieren cuidado especial. Usar feature flags o entry points de remote versionados (por ejemplo, design-system/v4/mf-manifest.json) para permitir a los equipos MFE optar por la nueva version a su propio ritmo en lugar de forzar una actualizacion inmediata.


Referencias