Monorepo com TypeScript e Turborepo

1. Fundamentos do Monorepo com TypeScript

Um monorepo é uma estratégia de gerenciamento de código onde múltiplos projetos (pacotes, aplicações, bibliotecas) residem em um único repositório. Quando combinado com TypeScript, o monorepo oferece vantagens significativas: compartilhamento direto de tipos, interfaces e configurações entre pacotes sem a necessidade de publicação intermediária.

Vantagens principais:
- Tipos e interfaces compartilhados em tempo real durante o desenvolvimento
- Configurações centralizadas (tsconfig, ESLint, Prettier)
- Refatoração cross-package com segurança de tipos
- Versionamento atômico de mudanças interdependentes

Desafios comuns:
- Compilação incremental e cache entre pacotes
- Dependências circulares entre módulos TypeScript
- Isolamento de pacotes para evitar vazamento de tipos internos

O Turborepo resolve esses desafios com pipeline inteligente, cache distribuído e execução paralela de tarefas.

2. Configuração Inicial do Turborepo

Para iniciar, crie um novo monorepo com Turborepo:

// Terminal
npx create-turbo@latest meu-monorepo
cd meu-monorepo

A estrutura gerada segue o padrão:

meu-monorepo/
├── apps/
│   ├── web/          # Aplicação Next.js
│   └── api/          # Servidor Node.js
├── packages/
│   ├── ui/           # Componentes compartilhados
│   ├── utils/        # Utilitários TypeScript
│   └── tsconfig/     # Configurações base
├── tooling/
│   └── eslint/       # Configuração ESLint
├── turbo.json        # Pipeline do Turborepo
└── package.json      # Raiz com workspaces

Configuração inicial do turbo.json:

{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"]
    },
    "typecheck": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

3. Gerenciamento de Pacotes TypeScript com npm Workspaces

Configure o package.json raiz para usar workspaces:

{
  "name": "meu-monorepo",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*",
    "tooling/*"
  ],
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "typecheck": "turbo run typecheck"
  },
  "devDependencies": {
    "turbo": "^1.10.0"
  }
}

Para consumir pacotes internos, use o padrão de escopo:

// packages/ui/package.json
{
  "name": "@meu-projeto/ui",
  "version": "0.0.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts"
}

// apps/web/package.json
{
  "name": "web",
  "dependencies": {
    "@meu-projeto/ui": "*",
    "@meu-projeto/utils": "*"
  }
}

A resolução de dependências entre pacotes TypeScript funciona porque o npm workspaces cria symlinks na node_modules raiz.

4. Compilação e Tipagem com TypeScript Project References

Configure um tsconfig.json base:

// packages/tsconfig/base.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "composite": true,
    "declarationMap": true,
    "declaration": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

Cada pacote estende a configuração base e define suas referências:

// packages/utils/tsconfig.json
{
  "extends": "@meu-projeto/tsconfig/base.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"],
  "references": [
    { "path": "../tsconfig" }
  ]
}

// apps/web/tsconfig.json
{
  "extends": "@meu-projeto/tsconfig/base.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"],
  "references": [
    { "path": "../../packages/utils" },
    { "path": "../../packages/ui" }
  ]
}

Integre com Turborepo no pipeline:

// turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["src/**", "tsconfig.json"],
      "outputs": ["dist/**"]
    },
    "typecheck": {
      "dependsOn": ["^build"],
      "inputs": ["src/**", "tsconfig.json"]
    }
  }
}

5. Compartilhando Configurações e Utilitários TypeScript

Crie um pacote de configuração TypeScript:

// packages/tsconfig/package.json
{
  "name": "@meu-projeto/tsconfig",
  "version": "0.0.0",
  "files": ["base.json", "nextjs.json", "node.json"]
}

Pacote de regras ESLint com TypeScript:

// tooling/eslint/package.json
{
  "name": "@meu-projeto/eslint-config",
  "main": "index.js",
  "dependencies": {
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0"
  }
}

Utilitários compartilhados:

// packages/utils/src/helpers.ts
export type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

export function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// packages/utils/src/index.ts
export * from './helpers';

6. Workflow de Desenvolvimento e Build

Scripts no turbo.json para desenvolvimento:

{
  "pipeline": {
    "dev": {
      "cache": false,
      "persistent": true,
      "dependsOn": ["^build"]
    },
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"],
      "cache": {
        "inputs": ["src/**", "tsconfig.json", "package.json"]
      }
    },
    "lint": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "typecheck": {
      "dependsOn": ["^build"],
      "outputs": []
    }
  }
}

O cache inteligente do Turborepo evita recompilações desnecessárias:

// Terminal - primeira execução (compila tudo)
turbo run build

// Terminal - segunda execução (usa cache se nada mudou)
turbo run build  # Output: "cache hit, replaying output..."

Execução paralela e ordenada:

// Terminal
turbo run build --parallel  # Executa builds em paralelo onde possível
turbo run typecheck --continue  # Continua mesmo com erros em alguns pacotes

7. Publicação e Versionamento

Configure Changesets para versionamento semântico:

// .changeset/config.json
{
  "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [],
  "linked": [],
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": []
}

Script de publicação com Turborepo:

// package.json
{
  "scripts": {
    "release": "turbo run build --filter=./packages/* && changeset publish",
    "version": "changeset version && turbo run build --filter=./packages/*"
  }
}

Para publicar apenas pacotes alterados:

// Terminal
turbo run build --filter=./packages/* --filter=[HEAD^1]
changeset publish

8. Boas Práticas e Armadilhas Comuns

Evitando dependências circulares:

// ❌ Ruim: pacote A depende de B, B depende de A
// packages/a/src/index.ts
import { something } from '@meu-projeto/b';

// packages/b/src/index.ts
import { something } from '@meu-projeto/a';

// ✅ Bom: extrair interface comum para pacote compartilhado
// packages/types/src/interfaces.ts
export interface SharedInterface {
  // ...
}

Consistência de versões do TypeScript:

// package.json raiz
{
  "devDependencies": {
    "typescript": "^5.3.0"
  },
  "resolutions": {
    "typescript": "^5.3.0"
  }
}

Debugging de tipos entre pacotes:

// Terminal - rastrear resolução de módulos
npx tsc --traceResolution

// Terminal - explicar arquivos incluídos na compilação
npx tsc --explainFiles

// Terminal - verificar tipos em pacote específico
turbo run typecheck --filter=@meu-projeto/utils

Isolamento de pacotes:

// packages/utils/package.json
{
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "files": ["dist"],
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  }
}

Com essas práticas, seu monorepo TypeScript com Turborepo será escalável, rápido e seguro para times de qualquer tamanho.

Referências