Estrategia de Repositorios

Tabla de Contenidos


Resumen

Estrategia hibrida de repositorios: monorepo platform-core en el centro con MFEs polyrepo alrededor platform-core Monorepo (pnpm workspaces + Turborepo) design-system shared-types shared-utils eslint-config tsconfig event-contracts mfe-dashboard Polyrepo mfe-settings Polyrepo mfe-analytics Polyrepo mfe-onboarding Polyrepo Flechas = paquetes npm via GitHub Packages

Esta plataforma adopta una estrategia de repositorios hibrida monorepo + polyrepo para equilibrar la co-localizacion de infraestructura compartida con la autonomia de cada equipo sobre sus micro frontends.

Enfoque

AspectoModelo de RepositorioJustificacion
Paquetes compartidos (design system, tipos, configs)Monorepo (platform-core)La co-localizacion permite cambios atomicos entre paquetes, un solo PR para breaking changes y versionado unificado via Changesets
Micro frontends (dashboard, settings, etc.)Polyrepo (un repo por MFE)Pipelines CI/CD independientes, permisos de equipo separados en GitHub, cadencia de deploy autonoma, radio de impacto aislado

Justificacion

Por que no un monorepo completo? A la escala de 3-5 equipos y 5-10 micro frontends, un monorepo completo introduce acoplamiento que socava el beneficio central de la arquitectura micro frontend: la autonomia de los equipos. Cada equipo debe poder hacer merge, versionar y desplegar su MFE sin coordinarse con los demas. Un monorepo completo tambien concentra la carga de CI---cada PR dispara checks en todo el codebase a menos que se mantenga un filtrado sofisticado.

Por que no polyrepo completo? El codigo compartido (design system, tipos TypeScript, configuraciones de lint) debe evolucionar de forma atomica. Si un breaking change en @org/shared-types requiere actualizaciones coordinadas en @org/design-system y @org/shared-utils, hacerlo en tres repos separados implica tres PRs, tres ciclos de revision y un problema fragil de ordenamiento de publicacion. Co-localizar los paquetes compartidos en un solo monorepo resuelve esto.

Escala

  • Equipos: 3-5 squads cross-funcionales, cada uno responsable de 1-3 micro frontends
  • Micro frontends: 5-10 aplicaciones desplegables de forma independiente
  • Paquetes compartidos: 7 paquetes en el monorepo, publicados en GitHub Packages bajo el scope @org
  • Toolchain: React 19 + TypeScript 5.x, Module Federation v2 (via Rsbuild), pnpm workspaces, Turborepo, Changesets, GitHub Actions

Estructura Monorepo (platform-core)

El repositorio platform-core contiene todos los paquetes de infraestructura compartida. Se gestiona con pnpm workspaces para resolucion y enlace de dependencias, Turborepo para orquestacion de tareas y caching, y Changesets para versionado y publicacion.

pnpm Workspaces

pnpm-workspace.yaml:

packages:
  - "packages/*"

Este unico glob le indica a pnpm que cada subdirectorio directo bajo packages/ es un paquete del workspace. pnpm enlaza automaticamente las referencias cruzadas entre ellos mediante symlinks en node_modules.

Estructura Completa de Directorios

platform-core/
├── packages/
│   ├── design-system/       # UI components, tokens, themes
│   ├── shared-types/        # TypeScript interfaces shared across platform
│   ├── shared-utils/        # Common utilities (date formatting, validation, etc.)
│   ├── eslint-config/       # Shared ESLint config (@org/eslint-config)
│   ├── tsconfig/            # Shared TypeScript base configs (@org/tsconfig)
│   ├── prettier-config/     # Shared Prettier config (@org/prettier-config)
│   └── event-contracts/     # CustomEvent type definitions for inter-MFE communication
├── turbo.json
├── pnpm-workspace.yaml
├── package.json             # Root scripts, devDependencies
├── .changeset/
│   └── config.json
├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── release.yml
├── .npmrc
└── .gitignore

package.json raiz:

{
  "name": "platform-core",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "test": "turbo run test",
    "lint": "turbo run lint",
    "typecheck": "turbo run typecheck",
    "format": "prettier --write \"packages/*/src/**/*.{ts,tsx}\"",
    "changeset": "changeset",
    "version-packages": "changeset version",
    "release": "turbo run build && changeset publish"
  },
  "devDependencies": {
    "@changesets/cli": "^2.27.0",
    "prettier": "^3.3.0",
    "turbo": "^2.8.10"
  },
  "packageManager": "pnpm@10.30.3",
  "engines": {
    "node": ">=20.0.0"
  }
}

.npmrc:

@org:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
auto-install-peers=true
strict-peer-dependencies=false

Nota (pnpm 10): A partir de pnpm 10, los lifecycle scripts (p. ej., postinstall) de las dependencias estan desactivados por defecto por seguridad. Si una dependencia necesita un script postinstall (p. ej., para compilacion nativa), hay que permitirlo explicitamente agregando onlyBuiltDependencies en package.json o configurando enable-pre-post-scripts=true en .npmrc.

Detalle de Paquetes

@org/design-system

AtributoValor
PropositoLibreria de componentes UI compartidos: botones, inputs, modals, primitivas de layout, design tokens y theme provider
ExportsComponentes React, tokens como CSS custom properties, ThemeProvider, helpers de clases utilitarias
Se consume comodependencies en los repos MFE (los componentes son codigo runtime)
BuildTypeScript + Rsbuild en modo libreria, genera ESM en dist/
Deps clavereact (peerDependency), @org/shared-types (dependencia workspace)
// packages/design-system/package.json
{
  "name": "@org/design-system",
  "version": "1.4.0",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    },
    "./tokens": {
      "types": "./dist/tokens/index.d.ts",
      "import": "./dist/tokens/index.js"
    }
  },
  "files": ["dist"],
  "scripts": {
    "build": "rsbuild build",
    "dev": "rsbuild dev",
    "test": "vitest run",
    "lint": "eslint src/",
    "typecheck": "tsc --noEmit"
  },
  "peerDependencies": {
    "react": "^19.2.4",
    "react-dom": "^19.2.4"
  },
  "dependencies": {
    "@org/shared-types": "workspace:*"
  },
  "devDependencies": {
    "@org/eslint-config": "workspace:*",
    "@org/tsconfig": "workspace:*",
    "@org/prettier-config": "workspace:*",
    "react": "^19.2.4",
    "react-dom": "^19.2.4",
    "typescript": "^5.9.3",
    "vitest": "^4.0.18",
    "@rsbuild/core": "^1.7.3"
  }
}

@org/shared-types

AtributoValor
PropositoDefiniciones de tipos TypeScript para toda la plataforma: modelos de usuario, shapes de respuesta API, definiciones de rutas, enums compartidos, interfaces de lifecycle MFE
ExportsInterfaces TypeScript, type aliases, enums (sin codigo runtime)
Se consume comodevDependencies en repos MFE (los tipos se eliminan en build time)
BuildSolo tsc---emite archivos .d.ts en dist/
Deps claveNinguna (cero dependencias runtime)
// packages/shared-types/package.json
{
  "name": "@org/shared-types",
  "version": "2.1.0",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    },
    "./events": {
      "types": "./dist/events/index.d.ts",
      "import": "./dist/events/index.js"
    }
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsc --project tsconfig.build.json",
    "lint": "eslint src/",
    "typecheck": "tsc --noEmit"
  },
  "devDependencies": {
    "@org/eslint-config": "workspace:*",
    "@org/tsconfig": "workspace:*",
    "typescript": "^5.9.3"
  }
}

@org/shared-utils

AtributoValor
PropositoFunciones utilitarias comunes: helpers de formato de fechas, schemas de validacion, constructores de URL, logica de reintentos, helpers de feature flags
ExportsFunciones puras, schemas Zod, constantes
Se consume comodependencies en repos MFE (las utilidades son codigo runtime)
BuildTypeScript compilado a ESM via tsc
Deps clave@org/shared-types (workspace), zod (peer)
// packages/shared-utils/package.json
{
  "name": "@org/shared-utils",
  "version": "1.2.0",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    },
    "./validation": {
      "types": "./dist/validation/index.d.ts",
      "import": "./dist/validation/index.js"
    },
    "./date": {
      "types": "./dist/date/index.d.ts",
      "import": "./dist/date/index.js"
    }
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsc --project tsconfig.build.json",
    "test": "vitest run",
    "lint": "eslint src/",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@org/shared-types": "workspace:*"
  },
  "peerDependencies": {
    "zod": "^3.23.0"
  },
  "devDependencies": {
    "@org/eslint-config": "workspace:*",
    "@org/tsconfig": "workspace:*",
    "typescript": "^5.9.3",
    "vitest": "^4.0.18",
    "zod": "^3.23.0"
  }
}

@org/eslint-config

AtributoValor
PropositoPresets de ESLint flat config compartidos que aseguran calidad de codigo consistente en toda la plataforma
ExportsArrays de flat config: base, react, worker
Se consume comodevDependencies en todas partes
BuildNinguno (los archivos de config se consumen directamente)

@org/tsconfig

AtributoValor
PropositoConfiguraciones base compartidas de TypeScript para opciones de compilador consistentes
ExportsArchivos de configuracion JSON: base.json, react.json, worker.json
Se consume comodevDependencies en todas partes, referenciado via extends en tsconfig.json
BuildNinguno (archivos JSON consumidos directamente)

@org/prettier-config

AtributoValor
PropositoFuente unica de verdad para las reglas de formateo de codigo
ExportsObjeto de configuracion Prettier
Se consume comodevDependencies en todas partes, referenciado desde .prettierrc.js
BuildNinguno

@org/event-contracts

AtributoValor
PropositoDefiniciones de tipos TypeScript para payloads de CustomEvent usados en la comunicacion inter-MFE via el event bus del navegador
ExportsConstantes de nombres de eventos, interfaces de tipos de payload, funciones helper tipadas (createEvent, listenForEvent)
Se consume comodevDependencies en repos MFE
Buildtsc emitiendo .d.ts y helpers JS runtime minimos
Deps clave@org/shared-types (workspace)
// packages/event-contracts/package.json
{
  "name": "@org/event-contracts",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    }
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsc --project tsconfig.build.json",
    "test": "vitest run",
    "lint": "eslint src/",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@org/shared-types": "workspace:*"
  },
  "devDependencies": {
    "@org/eslint-config": "workspace:*",
    "@org/tsconfig": "workspace:*",
    "typescript": "^5.9.3",
    "vitest": "^4.0.18"
  }
}

Dependencias Internas

Grafo de dependencias internas del monorepo mostrando paquetes y sus dependencias eslint-config tsconfig prettier-config Paquetes hoja (sin deps internas) @org/shared-types @org/shared-utils @org/event-contracts @org/design-system dependency devDependency

Los paquetes dentro del monorepo se referencian entre si usando el protocolo workspace de pnpm. Esto le indica a pnpm que resuelva la dependencia desde el workspace local en lugar del registry.

// In packages/design-system/package.json
{
  "dependencies": {
    "@org/shared-types": "workspace:*"
  },
  "devDependencies": {
    "@org/eslint-config": "workspace:*",
    "@org/tsconfig": "workspace:*",
    "@org/prettier-config": "workspace:*"
  }
}

Al publicar, Changesets reemplaza automaticamente workspace:* con el numero de version real (p. ej., "@org/shared-types": "^2.1.0"), de modo que los consumidores fuera del monorepo reciben rangos semver correctos. Mas concretamente, workspace:* se resuelve a la version actual exacta con rango caret (^x.y.z), workspace:~ se resuelve a rango tilde (~x.y.z), y workspace:^ tambien se resuelve a rango caret. El reemplazo ocurre solo en tiempo de publicacion---pnpm-lock.yaml y el node_modules local siguen usando los symlinks del workspace durante el desarrollo.

Grafo de dependencias dentro del monorepo:

@org/eslint-config      (leaf - no internal deps)
@org/tsconfig           (leaf - no internal deps)
@org/prettier-config    (leaf - no internal deps)
@org/shared-types       (leaf - no internal deps, uses tsconfig/eslint-config as devDeps)
    ↑
@org/shared-utils       (depends on shared-types)
@org/event-contracts    (depends on shared-types)
    ↑
@org/design-system      (depends on shared-types)

TypeScript Project References

El tsconfig.json raiz del monorepo define project references para que TypeScript entienda el orden de build y pueda realizar compilacion incremental.

tsconfig.json raiz:

{
  "files": [],
  "references": [
    { "path": "packages/shared-types" },
    { "path": "packages/shared-utils" },
    { "path": "packages/event-contracts" },
    { "path": "packages/design-system" }
  ]
}

El tsconfig.build.json de cada paquete declara sus propias referencias a los paquetes upstream:

// packages/shared-utils/tsconfig.build.json
{
  "extends": "@org/tsconfig/base.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "composite": true
  },
  "include": ["src/**/*.ts"],
  "references": [
    { "path": "../shared-types" }
  ]
}
// packages/design-system/tsconfig.build.json
{
  "extends": "@org/tsconfig/react.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "composite": true
  },
  "include": ["src/**/*.ts", "src/**/*.tsx"],
  "references": [
    { "path": "../shared-types" }
  ]
}

Ejecutar tsc --build en la raiz compila los paquetes en el orden topologico correcto: primero shared-types (sin dependencias internas), luego shared-utils, event-contracts y design-system en paralelo (todos dependen solo de shared-types).


Configuracion de Turborepo

turbo.json

Turborepo orquesta la ejecucion de tareas en todos los paquetes del workspace, respetando el orden de dependencias y maximizando el paralelismo. Tambien proporciona caching local y remoto para evitar trabajo redundante.

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": [
    "**/.env.*local"
  ],
  "globalEnv": [
    "NODE_ENV"
  ],
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"],
      "env": ["NODE_ENV"]
    },
    "dev": {
      "dependsOn": ["^build"],
      "persistent": true,
      "cache": false
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"],
      "env": ["CI"]
    },
    "lint": {
      "outputs": [],
      "env": ["CI"]
    },
    "typecheck": {
      "dependsOn": ["^build"],
      "outputs": []
    }
  }
}

Decisiones de configuracion clave:

TareadependsOnoutputscacheJustificacion
build["^build"]["dist/**"]si (por defecto)Debe compilar los paquetes upstream primero. Cachea el output dist/.
dev["^build"]---falseNecesita upstream compilado, pero los servidores de desarrollo son long-running y no deben cachearse. persistent: true mantiene el proceso vivo.
test["^build"]["coverage/**"]siLos tests pueden importar artefactos compilados de paquetes upstream. Cachea los reportes de coverage.
lint(ninguno)[]siEl linting es autocontenido---no necesita esperar builds upstream. Sin output de archivos, pero el exit code se cachea.
typecheck["^build"][]siLa verificacion de tipos requiere que existan los archivos .d.ts upstream. Sin output de archivos, pero el resultado se cachea.

El prefijo ^ en dependsOn significa "ejecutar esta tarea primero en todos los paquetes de los que dependo" (dependencia topologica). Sin el caret, significaria "ejecutar esta tarea primero en el mismo paquete."

Remote Caching

El remote cache de Turborepo permite que las ejecuciones de CI y las maquinas de los desarrolladores compartan artefactos de build, reduciendo drasticamente los tiempos de build para paquetes sin cambios.

Configuracion con Vercel:

# Authenticate (one-time per machine or CI secret)
npx turbo login

# Link the repo to a Vercel team for remote caching
npx turbo link

Variables de entorno en CI:

# .github/workflows/ci.yml (excerpt)
env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ vars.TURBO_TEAM }}

Con estas variables configuradas, cada comando turbo run lee y escribe automaticamente en el remote cache.

Impacto del cache hit:

EscenarioSin Remote CacheCon Remote Cache
PR que modifica solo design-system~90s (todos los paquetes compilan)~25s (solo design-system recompila; shared-types, shared-utils son cache hits)
PR que modifica solo eslint-config~90s~10s (cambio solo de config, todos los builds son cache hits, solo re-ejecuta lint)
Rebuild completo (cache frio)~90s~90s (igual, no hay cache disponible)

Alternativa self-hosted: Para equipos que no pueden usar Vercel, Turborepo soporta servidores de remote cache self-hosted que implementan la Turborepo Remote Cache API. Las opciones incluyen turbo-remote-cache-cloudflare (Cloudflare Workers + R2) o ducktape (basado en Docker).

Pipeline de Tareas

Pipeline de tareas de Turborepo: lint, typecheck, build y test con capa de remote caching lint sin deps necesarias typecheck requiere ^build build outputs: dist/** test requiere ^build Turborepo Remote Cache (Vercel / self-hosted) turbo run build cache hit = omitir

Turborepo construye un grafo aciclico dirigido (DAG) de tareas basado en las declaraciones dependsOn de turbo.json y las relaciones de dependencia declaradas en el package.json de cada paquete.

Ejemplo: turbo run build

Arbol de dependencias de build -- shared-types compila primero, luego los dependientes en paralelo @org/shared-types build (se ejecuta primero) en paralelo @org/shared-utils build @org/event-contracts build @org/design-system build

Orden de ejecucion:

  1. @org/shared-types#build se ejecuta primero (sin dependencias upstream).
  2. @org/shared-utils#build, @org/event-contracts#build y @org/design-system#build se ejecutan en paralelo (todos dependen solo de shared-types).
  3. @org/eslint-config, @org/tsconfig y @org/prettier-config no tienen script build, asi que Turborepo los omite.

Filtrado con --filter:

# Build only design-system and its dependencies
turbo run build --filter=@org/design-system...

# Build only packages that changed since main
turbo run build --filter=...[origin/main]

# Run tests in a specific package without building dependencies
turbo run test --filter=@org/shared-utils

# Build everything that depends on shared-types (downstream)
turbo run build --filter=...@org/shared-types

La sintaxis ... controla la direccion:

  • @org/design-system... = el paquete y todas sus dependencias (upstream)
  • ...@org/shared-types = el paquete y todo lo que depende de el (downstream)
  • ...[origin/main] = todos los paquetes con cambios desde el git ref indicado

Estructura Polyrepo (MFEs)

Cada micro frontend vive en su propio repositorio de GitHub, propiedad de un unico equipo. Todos los repos MFE siguen un layout estandarizado establecido por un repositorio template compartido (org/mfe-template).

Layout Estandar de un Repositorio MFE

mfe-dashboard/
├── src/
│   ├── App.tsx              # Root component (exposed via Module Federation)
│   ├── bootstrap.tsx        # Async bootstrap for Module Federation
│   ├── index.ts             # Entry point (dynamic import of bootstrap)
│   ├── pages/
│   │   ├── DashboardPage.tsx
│   │   └── AnalyticsPage.tsx
│   ├── components/
│   │   ├── DashboardCard.tsx
│   │   └── MetricsChart.tsx
│   ├── hooks/
│   │   ├── useDashboardData.ts
│   │   └── usePolling.ts
│   ├── services/            # API calls to BFF
│   │   └── dashboardApi.ts
│   └── types/
│       └── dashboard.ts     # MFE-specific types
├── rsbuild.config.ts        # Module Federation remote configuration
├── package.json
├── tsconfig.json
├── eslint.config.js         # Extends @org/eslint-config (flat config)
├── .prettierrc.js           # Extends @org/prettier-config
├── .npmrc                   # GitHub Packages registry config
├── vitest.config.ts
└── .github/
    └── workflows/
        ├── ci.yml           # Lint, typecheck, test on every PR
        └── deploy.yml       # Build + deploy to Cloudflare Pages on merge to main

Patron de entry point (critico para Module Federation):

// src/index.ts
// Dynamic import ensures shared dependencies are initialized before bootstrap
import("./bootstrap");
// src/bootstrap.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

const mount = (el: HTMLElement) => {
  const root = ReactDOM.createRoot(el);
  root.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
  return () => root.unmount();
};

// Standalone mode: mount directly if not consumed as a remote
const rootEl = document.getElementById("root");
if (rootEl) {
  mount(rootEl);
}

export { mount };

package.json (MFE tipico):

{
  "name": "@org/mfe-dashboard",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "rsbuild dev",
    "build": "rsbuild build",
    "preview": "rsbuild preview",
    "test": "vitest run",
    "test:watch": "vitest",
    "lint": "eslint src/",
    "typecheck": "tsc --noEmit",
    "format": "prettier --write \"src/**/*.{ts,tsx}\""
  },
  "dependencies": {
    "@org/design-system": "^1.4.0",
    "@org/shared-utils": "^1.2.0",
    "@org/event-contracts": "^1.0.0",
    "react": "^19.2.4",
    "react-dom": "^19.2.4",
    "react-router-dom": "^7.13.1"
  },
  "devDependencies": {
    "@org/eslint-config": "^1.0.0",
    "@org/prettier-config": "^1.0.0",
    "@org/shared-types": "^2.1.0",
    "@org/tsconfig": "^1.0.0",
    "@module-federation/enhanced": "^2.0.1",
    "@rsbuild/core": "^1.7.3",
    "@rsbuild/plugin-react": "^1.4.1",
    "@testing-library/react": "^16.0.0",
    "typescript": "^5.9.3",
    "vitest": "^4.0.18"
  }
}

Notar que @org/shared-types es devDependency porque sus exports son tipos TypeScript puros que se eliminan en compile time. @org/design-system y @org/shared-utils son dependencies porque entregan codigo runtime.

Consumo de Paquetes del Monorepo

Los repos MFE instalan los paquetes compartidos desde GitHub Packages, el registry npm privado alojado junto a los repositorios de GitHub.

.npmrc en cada repo MFE:

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

Instalacion de paquetes compartidos:

# Runtime dependencies
pnpm add @org/design-system @org/shared-utils @org/event-contracts

# Dev dependencies (types and configs)
pnpm add -D @org/shared-types @org/eslint-config @org/tsconfig @org/prettier-config

Actualizacion automatizada de dependencias con Renovate:

Renovate esta configurado en cada repo MFE para abrir PRs automaticamente cuando se publican nuevas versiones de los paquetes @org/*. Los patches se auto-mergean; las versiones minor y major requieren revision manual.

// renovate.json in each MFE repo
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "github>org/platform-core//renovate-presets"
  ]
}

Esto extiende el preset compartido de Renovate desde el monorepo (cubierto en Presets Compartidos de Renovate).


Estrategia TypeScript

Configuraciones Base Compartidas

El paquete @org/tsconfig provee configuraciones base de TypeScript que cada paquete y MFE extiende. Esto asegura un comportamiento de compilador consistente en toda la plataforma.

Estructura del paquete:

packages/tsconfig/
├── base.json
├── react.json
├── worker.json
└── package.json

base.json:

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022"],

    // Strict type checking
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "forceConsistentCasingInFileNames": true,
    "exactOptionalPropertyTypes": false,

    // Interop
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,

    // Output
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "skipLibCheck": true
  },
  "exclude": [
    "node_modules",
    "dist",
    "coverage"
  ]
}

react.json:

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "extends": "./base.json",
  "compilerOptions": {
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",
    "types": ["vite/client"]
  }
}

worker.json:

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "extends": "./base.json",
  "compilerOptions": {
    "lib": ["ES2022"],
    "types": ["@cloudflare/workers-types"]
  }
}

package.json:

{
  "name": "@org/tsconfig",
  "version": "1.0.0",
  "files": ["base.json", "react.json", "worker.json"],
  "exports": {
    "./base.json": "./base.json",
    "./react.json": "./react.json",
    "./worker.json": "./worker.json"
  }
}

Project References en el Monorepo

Dentro del monorepo, las TypeScript project references establecen el grafo de dependencias de compilacion. Esto permite:

  1. Orden de build correcto: tsc --build compila los paquetes en orden topologico.
  2. Builds incrementales: Solo recompila paquetes cuyas fuentes (o .d.ts upstream) cambiaron.
  3. Rendimiento del editor: El language server de TypeScript resuelve tipos cross-package via archivos .d.ts en lugar de cargar todos los archivos fuente.

Cadena de referencias:

tsconfig.json (root)
├── references → packages/shared-types/tsconfig.build.json
│                  extends: @org/tsconfig/base.json
│                  composite: true
│                  references: (none)
│
├── references → packages/shared-utils/tsconfig.build.json
│                  extends: @org/tsconfig/base.json
│                  composite: true
│                  references: [shared-types]
│
├── references → packages/event-contracts/tsconfig.build.json
│                  extends: @org/tsconfig/base.json
│                  composite: true
│                  references: [shared-types]
│
└── references → packages/design-system/tsconfig.build.json
                   extends: @org/tsconfig/react.json
                   composite: true
                   references: [shared-types]

El flag composite: true es obligatorio para cualquier paquete que sea target de un project reference. Fuerza a TypeScript a emitir archivos .d.ts y un manifiesto .tsbuildinfo usado para la compilacion incremental.

Comparticion de Tipos entre Repos

Los tipos fluyen desde el monorepo hacia los repos MFE a traves de dos mecanismos complementarios:

1. @org/shared-types como paquete npm:

Los contratos de plataforma---modelos de usuario, shapes de respuesta API, definiciones de rutas, feature flags---se definen en @org/shared-types y se publican en GitHub Packages. Los MFEs lo instalan como devDependency.

// In @org/shared-types/src/index.ts
export interface User {
  id: string;
  email: string;
  displayName: string;
  role: "admin" | "editor" | "viewer";
  preferences: UserPreferences;
}

export interface UserPreferences {
  theme: "light" | "dark" | "system";
  locale: string;
  timezone: string;
}

export interface ApiResponse<T> {
  data: T;
  meta: {
    timestamp: number;
    requestId: string;
  };
}
// In mfe-dashboard/src/services/dashboardApi.ts
import type { ApiResponse, User } from "@org/shared-types";

export async function fetchCurrentUser(): Promise<ApiResponse<User>> {
  const res = await fetch("/api/user");
  return res.json();
}

2. Generacion de tipos de Module Federation para tipos especificos del remote:

Los tipos para los modulos expuestos via Module Federation (los props del componente App, metadatos de rutas, etc.) se generan junto con el build del remote usando la funcionalidad de generacion de tipos de @module-federation/enhanced. El host application los consume en tiempo de desarrollo.

// Generated by Module Federation (mfe-dashboard)
declare module "mfe_dashboard/App" {
  import { ComponentType } from "react";

  interface AppProps {
    basePath: string;
    onNavigate?: (path: string) => void;
  }

  const App: ComponentType<AppProps>;
  export default App;
}

Resumen de patrones:

Categoria de TipoMecanismoEjemplo
Contratos de plataforma (modelos, shapes API)Paquete npm @org/shared-typesUser, ApiResponse<T>
Payloads de eventos inter-MFEPaquete npm @org/event-contractsNavigationEvent, NotificationEvent
Tipos de modulos expuestos del MFEGeneracion de tipos de Module FederationProps de mfe_dashboard/App
Tipos internos del MFEDirectorio local src/types/DashboardMetric, ChartConfig

Configuraciones Compartidas

ESLint Config (@org/eslint-config)

El paquete @org/eslint-config provee arrays de ESLint flat config pre-construidos para ESLint 10+ (eslint@^10.0.2). ESLint 10 elimina el soporte para el formato de configuracion legacy .eslintrc.*---solo el archivo flat config (eslint.config.js) esta soportado. Los equipos no escriben sus propias reglas---seleccionan un preset y opcionalmente agregan overrides especificos del proyecto.

Estructura del paquete:

packages/eslint-config/
├── src/
│   ├── index.ts       # Re-exports all presets
│   ├── base.ts        # Base config (TypeScript, import sorting, no-unused-vars)
│   ├── react.ts       # Extends base + React-specific rules
│   └── worker.ts      # Extends base + Cloudflare Workers rules
├── package.json
└── tsconfig.json

base.ts:

import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import importPlugin from "eslint-plugin-import-x";

export const base = tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.strictTypeChecked,
  ...tseslint.configs.stylisticTypeChecked,
  {
    plugins: {
      "import-x": importPlugin,
    },
    rules: {
      // Enforce consistent imports
      "import-x/order": [
        "error",
        {
          "groups": [
            "builtin",
            "external",
            "internal",
            "parent",
            "sibling",
            "index"
          ],
          "newlines-between": "always",
          "alphabetize": { "order": "asc" }
        }
      ],
      "import-x/no-duplicates": "error",

      // TypeScript-specific
      "@typescript-eslint/no-unused-vars": [
        "error",
        { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
      ],
      "@typescript-eslint/consistent-type-imports": [
        "error",
        { "prefer": "type-imports" }
      ],
      "@typescript-eslint/no-floating-promises": "error",
      "@typescript-eslint/no-misused-promises": "error",
    },
  }
);

react.ts:

import reactPlugin from "eslint-plugin-react";
import hooksPlugin from "eslint-plugin-react-hooks";
import a11yPlugin from "eslint-plugin-jsx-a11y";

import { base } from "./base.js";

export const react = [
  ...base,
  reactPlugin.configs.flat.recommended,
  reactPlugin.configs.flat["jsx-runtime"],
  {
    plugins: {
      "react-hooks": hooksPlugin,
      "jsx-a11y": a11yPlugin,
    },
    rules: {
      ...hooksPlugin.configs.recommended.rules,
      ...a11yPlugin.configs.recommended.rules,
      "react/prop-types": "off", // TypeScript handles prop validation
    },
    settings: {
      react: { version: "detect" },
    },
  },
];

Consumo en un repo MFE:

// eslint.config.js (in mfe-dashboard)
import { react } from "@org/eslint-config";

export default [
  ...react,
  {
    languageOptions: {
      parserOptions: {
        project: "./tsconfig.json",
      },
    },
  },
  // Project-specific overrides (rare)
  {
    files: ["src/legacy/**/*.ts"],
    rules: {
      "@typescript-eslint/no-explicit-any": "off",
    },
  },
];

Conflicto entre Prettier y el ordenamiento de imports de ESLint: La configuracion compartida de Prettier usa prettier-plugin-organize-imports, que puede reordenar imports de forma que entre en conflicto con la regla import-x/order de @org/eslint-config. Para evitar un bucle fix/format, instalar eslint-config-prettier y agregarlo como ultima entrada en el array de flat config. Esto desactiva todas las reglas de ESLint que se solapan con el formateo de Prettier. Alternativamente, eliminar prettier-plugin-organize-imports de la configuracion de Prettier y confiar unicamente en la regla import-x/order de ESLint para el ordenamiento de imports.

Prettier Config (@org/prettier-config)

Un unico paquete asegura que cada archivo en toda la plataforma se formatea de manera identica.

packages/prettier-config/index.js:

/** @type {import("prettier").Config} */
const config = {
  semi: true,
  singleQuote: false,
  tabWidth: 2,
  trailingComma: "all",
  printWidth: 100,
  bracketSpacing: true,
  arrowParens: "always",
  endOfLine: "lf",
  plugins: ["prettier-plugin-organize-imports"],
};

export default config;

package.json:

{
  "name": "@org/prettier-config",
  "version": "1.0.0",
  "type": "module",
  "main": "./index.js",
  "exports": {
    ".": "./index.js"
  },
  "files": ["index.js"],
  "peerDependencies": {
    "prettier": "^3.3.0"
  },
  "dependencies": {
    "prettier-plugin-organize-imports": "^4.0.0"
  }
}

Consumo en cualquier repo:

// .prettierrc.js
export { default } from "@org/prettier-config";

Alternativamente, en package.json:

{
  "prettier": "@org/prettier-config"
}

Presets Compartidos de Renovate

El monorepo aloja un preset compartido de Renovate que todos los repos MFE extienden. Esto asegura un comportamiento de actualizacion de dependencias consistente en toda la plataforma.

platform-core/renovate-presets.json:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "description": "Shared Renovate presets for the micro frontends platform",
  "extends": [
    "config:recommended",
    ":semanticCommits",
    ":automergePatch",
    "schedule:earlyMondays"
  ],
  "labels": ["dependencies"],
  "packageRules": [
    {
      "description": "Group all @org packages into a single PR",
      "matchPackagePatterns": ["^@org/"],
      "groupName": "platform packages",
      "groupSlug": "platform-packages",
      "automerge": false,
      "schedule": ["at any time"]
    },
    {
      "description": "Auto-merge patch updates for @org packages",
      "matchPackagePatterns": ["^@org/"],
      "matchUpdateTypes": ["patch"],
      "automerge": true,
      "automergeType": "pr",
      "schedule": ["at any time"]
    },
    {
      "description": "Group React-related packages",
      "matchPackageNames": ["react", "react-dom", "@types/react", "@types/react-dom"],
      "groupName": "react",
      "groupSlug": "react"
    },
    {
      "description": "Group test infrastructure",
      "matchPackageNames": ["vitest", "@testing-library/react", "@testing-library/jest-dom"],
      "groupName": "testing",
      "groupSlug": "testing",
      "automerge": true,
      "matchUpdateTypes": ["minor", "patch"]
    },
    {
      "description": "Group TypeScript and build tooling",
      "matchPackageNames": ["typescript", "@rsbuild/core", "@rsbuild/plugin-react"],
      "groupName": "build tooling",
      "groupSlug": "build-tooling"
    },
    {
      "description": "Pin major versions of critical shared deps to prevent Module Federation mismatches",
      "matchPackageNames": ["react", "react-dom"],
      "matchUpdateTypes": ["major"],
      "enabled": false
    }
  ],
  "vulnerabilityAlerts": {
    "labels": ["security"],
    "automerge": true,
    "schedule": ["at any time"]
  }
}

Consumo en cada repo MFE:

// renovate.json
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "github>org/platform-core//renovate-presets"
  ]
}

La sintaxis github>org/platform-core//renovate-presets le indica a Renovate que obtenga el archivo de preset renovate-presets.json desde la raiz del repo platform-core en la organizacion org de GitHub.


Flujo de Dependencias entre Repos

Flujo de dependencias entre repos: Changesets versiona, publica en npm, Renovate abre PR, MFE se actualiza Changesets version bump Publicar changeset publish GitHub Packages npm registry Renovate detecta update PR CI + revision MFE Actualizado deploy Monorepo (platform-core) Polyrepos (repos MFE) Updates patch se auto-mergean tras pasar CI | Minor/major requieren revision del equipo

Pipeline de Propagacion de Cambios

Cuando un desarrollador realiza un cambio en un paquete compartido del monorepo, la siguiente secuencia propaga ese cambio a todos los repos MFE consumidores:

Pipeline de Propagacion de Cambios -- Release del monorepo al consumo en polyrepos MFE MONOREPO (platform-core) 1 Desarrollador abre PR modificando @org/design-system 2 CI ejecuta: build, test, lint, typecheck (Turborepo) 3 Desarrollador agrega changeset: paquete + tipo de bump + descripcion 4 PR mergeado a main 5 Changesets abre PR "Version Packages" 6 PR "Version Packages" mergeado 7 Workflow de release: a. changeset version (bump + CHANGELOG) b. changeset publish (GitHub Packages) c. repository-dispatch a repos MFE npm publish + repository_dispatch POLYREPOS (repos MFE) 8 Renovate detecta nueva version (o repository-dispatch dispara ejecucion) 9 Renovate abre PR: "Update platform packages" Agrupa todos los updates @org/* + extractos del changelog 10 CI ejecuta en el PR de Renovate: build, test, lint, typecheck 11 Patch: auto-merge | Minor/Major: revision del equipo 12 Merge dispara deploy: MFE recompilado + desplegado

Configuracion de Changesets

.changeset/config.json:

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

Configuraciones clave:

  • access: "restricted": Los paquetes se publican como privados para la organizacion de GitHub.
  • updateInternalDependencies: "patch": Cuando se hace bump de @org/shared-types, los paquetes que dependen de el (como @org/design-system) reciben automaticamente un patch bump.
  • changelog: "@changesets/changelog-github": Genera changelogs con enlaces a PRs de GitHub y contribuidores.

Workflow de Release

.github/workflows/release.yml:

name: Release

on:
  push:
    branches:
      - main

concurrency: ${{ github.workflow }}-${{ github.ref }}

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

jobs:
  release:
    name: Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - 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
        run: pnpm run build
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

      - name: Create Release Pull Request or Publish
        id: changesets
        uses: changesets/action@v1
        with:
          publish: pnpm run release
          version: pnpm run version-packages
          title: "chore: version packages"
          commit: "chore: version packages"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Notify MFE repos
        if: steps.changesets.outputs.published == 'true'
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.MFE_DISPATCH_TOKEN }}
          script: |
            const repos = [
              'mfe-dashboard',
              'mfe-settings',
              'mfe-admin',
              'mfe-analytics',
              'mfe-onboarding',
            ];

            const published = JSON.parse('${{ steps.changesets.outputs.publishedPackages }}');

            for (const repo of repos) {
              await github.rest.repos.createDispatchEvent({
                owner: context.repo.owner,
                repo,
                event_type: 'platform-packages-released',
                client_payload: {
                  packages: published.map(p => ({
                    name: p.name,
                    version: p.version,
                  })),
                },
              });
            }

Workflow de CI

.github/workflows/ci.yml:

name: CI

on:
  pull_request:
    branches: [main]

permissions:
  contents: read

jobs:
  ci:
    name: Build, Test & Lint
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        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: Build
        run: pnpm run build
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

      - name: Lint
        run: pnpm run lint

      - name: Type Check
        run: pnpm run typecheck

      - name: Test
        run: pnpm run test

Mecanismo de Notificacion

El evento repository-dispatch enviado en el workflow de release cumple dos propositos:

  1. Trigger inmediato de Renovate: Los repos MFE pueden tener un workflow que escucha eventos platform-packages-released y dispara una ejecucion de Renovate, saltandose el schedule normal de Renovate.
  2. Visibilidad: El payload del evento incluye la lista de paquetes publicados y sus versiones, que se puede loggear o publicar en Slack.

Workflow del lado MFE que escucha dispatches:

# .github/workflows/platform-update.yml (in each MFE repo)
name: Platform Update

on:
  repository_dispatch:
    types: [platform-packages-released]

jobs:
  trigger-renovate:
    name: Trigger Renovate
    runs-on: ubuntu-latest
    steps:
      - name: Log published packages
        run: |
          echo "Platform packages released:"
          echo '${{ toJSON(github.event.client_payload.packages) }}'

      - name: Trigger Renovate
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.RENOVATE_TOKEN }}
          script: |
            await github.rest.actions.createWorkflowDispatch({
              owner: context.repo.owner,
              repo: context.repo.repo,
              workflow_id: 'renovate.yml',
              ref: 'main',
            });

Troubleshooting: Resolucion de pnpm Workspace

Problemas comunes al trabajar con pnpm workspaces en este repositorio:

Conflictos de peer dependency durante pnpm install:

Cuando un paquete del workspace declara una peerDependency (p. ej., react) y otro paquete del workspace depende de una version diferente, pnpm reportara un conflicto de peer dependency. Para diagnosticar, ejecutar pnpm why <package> para inspeccionar el arbol de resolucion. Configurar auto-install-peers=true y strict-peer-dependencies=false en .npmrc (ya configurado mas arriba) relaja la enforcement, pero los conflictos deben resolverse alineando las versiones en todo el workspace.

El protocolo workspace:* no resuelve:

Si pnpm install falla con un error como No matching version found for @org/shared-types@workspace:*, verificar que (a) el nombre del paquete en la dependencia coincide exactamente con el campo name en el package.json del paquete destino, y (b) el directorio del paquete destino esta incluido en el glob packages de pnpm-workspace.yaml. Ejecutar pnpm ls --depth 0 en la raiz puede ayudar a verificar que paquetes reconoce pnpm como miembros del workspace.

Phantom dependencies o problemas de hoisting:

Por defecto, pnpm usa un layout de node_modules estricto que impide el acceso a phantom dependencies. Si un paquete importa un modulo que no declara en su propio package.json, la importacion fallara. Esto es intencional. Agregar la dependencia faltante de forma explicita en lugar de depender del hoisting. Si alguna herramienta requiere hoisting (raro), se puede configurar public-hoist-pattern en .npmrc, pero es preferible corregir la declaracion primero.


Referencias