Monorepos com pnpm oferecem uma solução eficiente para gerenciar múltiplos pacotes e projetos. Compartilho aprendizados práticos de como isso foi útil no dia a dia, acelerando desenvolvimento e melhorando a organização do código.
O Que é um Monorepo?
Um monorepo é uma estratégia de versionamento onde múltiplos projetos ou pacotes são armazenados em um único repositório Git. Em vez de ter repositórios separados para cada pacote, tudo fica organizado em uma estrutura hierárquica.
Vantagens principais:
- Compartilhamento de código simplificado
- Refatorações que afetam múltiplos pacotes em uma única PR
- Versionamento coordenado
- Builds incrementais e cache compartilhado
- Visibilidade completa do código
Desafios:
- Repositório maior
- Necessidade de ferramentas adequadas
- Gerenciamento de dependências mais complexo
Por Que pnpm para Monorepos?
pnpm é um gerenciador de pacotes que oferece várias vantagens para monorepos:
1. Eficiência de Espaço em Disco
pnpm usa um store global e links simbólicos, evitando duplicação de dependências. Em um monorepo com 10 pacotes que compartilham as mesmas dependências, você economiza espaço significativo.
# Comparação aproximada de espaço npm/yarn: ~500MB por projeto = 5GB para 10 projetos pnpm: ~500MB total (compartilhado)
2. Performance Superior
pnpm é mais rápido que npm e yarn, especialmente em monorepos grandes. O sistema de cache e links simbólicos reduz drasticamente o tempo de instalação.
3. Strict Dependency Resolution
pnpm garante que apenas dependências declaradas explicitamente podem ser acessadas, prevenindo problemas de "phantom dependencies" comuns em monorepos.
4. Workspaces Nativo
pnpm tem suporte nativo a workspaces, similar ao Yarn, mas com melhor performance e gerenciamento de dependências.
Configurando um Monorepo com pnpm
Estrutura Básica
Vamos começar criando a estrutura básica de um monorepo:
monorepo/ ├── packages/ │ ├── utils/ │ ├── ui-components/ │ └── config/ ├── apps/ │ ├── web-app/ │ └── admin-panel/ ├── pnpm-workspace.yaml └── package.json
1. Configurando pnpm-workspace.yaml
O arquivo `pnpm-workspace.yaml` define quais diretórios são parte do workspace:
packages: - 'apps/*' - 'packages/*'
Isso informa ao pnpm que todos os diretórios dentro de `apps/` e `packages/` são pacotes do workspace.
2. package.json Raiz
O `package.json` na raiz gerencia scripts e dependências compartilhadas:
{
"name": "monorepo",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "pnpm --filter \"./apps/*\" dev",
"build": "pnpm --filter \"./packages/*\" build",
"test": "pnpm --filter \"./packages/*\" test",
"lint": "pnpm --filter \"./packages/*\" lint"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^20.0.0"
}
}3. Criando um Pacote
Vamos criar um pacote de utilitários como exemplo:
{
"name": "@monorepo/utils",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"dependencies": {},
"devDependencies": {
"typescript": "workspace:*"
}
}Pontos importantes:
- `workspace:*` referencia a versão do pacote no workspace
- Nome do pacote com escopo (`@monorepo/utils`)
- Scripts de build e desenvolvimento
4. Usando Pacotes Internos
Para usar um pacote interno em outro, você referencia pelo nome:
{
"name": "web-app",
"version": "1.0.0",
"dependencies": {
"@monorepo/utils": "workspace:*",
"@monorepo/ui-components": "workspace:*"
}
}O `workspace:*` indica que o pacote está no mesmo workspace.
Configurações Avançadas
Filtros e Scripts
pnpm permite executar comandos em pacotes específicos usando filtros:
{
"scripts": {
"dev:web": "pnpm --filter web-app dev",
"dev:admin": "pnpm --filter admin-panel dev",
"build:packages": "pnpm --filter \"./packages/*\" build",
"test:utils": "pnpm --filter @monorepo/utils test"
}
}Dependências Compartilhadas
Para compartilhar dependências entre todos os pacotes, você pode usar `.npmrc`:
# .npmrc na raiz shamefully-hoist=true
Ou declarar no `package.json` raiz e usar `pnpm.hoistPattern`:
{
"pnpm": {
"hoistPattern": [
"*react*",
"*typescript*"
]
}
}Versionamento com Changesets
Para gerenciar versionamento de múltiplos pacotes, use Changesets:
pnpm add -D -w @changesets/cli
{
"scripts": {
"changeset": "changeset",
"version": "changeset version",
"release": "pnpm build && changeset publish"
}
}Estrutura de um Pacote Completo
Vamos ver um exemplo completo de um pacote:
packages/utils/ ├── src/ │ ├── index.ts │ ├── formatCurrency.ts │ └── dateHelpers.ts ├── dist/ ├── package.json ├── tsconfig.json └── README.md
src/index.ts
export * from './formatCurrency'; export * from './dateHelpers';
tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}tsconfig.base.json (raiz)
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler"
}
}Scripts Úteis para Monorepo
Build Incremental
{
"scripts": {
"build": "pnpm --filter \"./packages/*\" --filter \"./apps/*\" build",
"build:changed": "pnpm --filter \"...[origin/main]\" build"
}
}Testes
{
"scripts": {
"test": "pnpm --filter \"./packages/*\" test",
"test:changed": "pnpm --filter \"...[origin/main]\" test",
"test:watch": "pnpm --filter \"./packages/*\" test --watch"
}
}Linting
{
"scripts": {
"lint": "pnpm --filter \"./packages/*\" --filter \"./apps/*\" lint",
"lint:fix": "pnpm --filter \"./packages/*\" --filter \"./apps/*\" lint --fix"
}
}Integração com Turborepo
Para builds ainda mais rápidos, combine pnpm com Turborepo:
pnpm add -D -w turbo
turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
}
}
}Scripts com Turborepo
{
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"dev": "turbo run dev"
}
}Boas Práticas
1. Nomenclatura Consistente
Use um escopo consistente para todos os pacotes:
{
"name": "@empresa/utils",
"name": "@empresa/ui-components",
"name": "@empresa/config"
}2. Dependências de Desenvolvimento
Mantenha dependências de desenvolvimento no nível raiz quando possível:
{
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^20.0.0",
"eslint": "^8.0.0"
}
}3. Workspace Protocol
Sempre use `workspace:*` para dependências internas:
{
"dependencies": {
"@monorepo/utils": "workspace:*"
}
}4. Estrutura de Pastas
Mantenha uma estrutura clara:
monorepo/ ├── packages/ # Pacotes compartilhados │ ├── utils/ │ ├── ui/ │ └── config/ ├── apps/ # Aplicações │ ├── web/ │ └── admin/ ├── tools/ # Scripts e ferramentas └── docs/ # Documentação
5. CI/CD Otimizado
Configure CI para construir apenas o que mudou:
- name: Install pnpm uses: pnpm/action-setup@v2 with: version: 8 - name: Get changed packages id: changed run: | echo "packages=$(pnpm --filter='...[origin/main]' list --depth=-1 --json | jq -r '.[].name')" >> $GITHUB_OUTPUT - name: Build changed packages run: pnpm --filter="...[origin/main]" build
Resolvendo Problemas Comuns
Dependências Não Encontradas
Se um pacote não encontra outro do workspace:
# Reinstalar dependências pnpm install
Builds Fracassando
Verifique a ordem de build. Use `dependsOn` no Turborepo ou scripts sequenciais:
{
"scripts": {
"build": "pnpm --filter \"./packages/*\" build && pnpm --filter \"./apps/*\" build"
}
}Cache Issues
Limpe o cache do pnpm:
pnpm store prune
Exemplo Prático Completo
Vamos criar um exemplo completo de configuração:
pnpm-workspace.yaml
packages: - 'apps/*' - 'packages/*'
package.json (raiz)
{
"name": "monorepo-example",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "pnpm --filter \"./apps/*\" dev",
"build": "pnpm --filter \"./packages/*\" build && pnpm --filter \"./apps/*\" build",
"test": "pnpm --filter \"./packages/*\" test",
"lint": "pnpm --filter \"./packages/*\" --filter \"./apps/*\" lint",
"clean": "pnpm --filter \"./packages/*\" --filter \"./apps/*\" clean"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^20.0.0"
}
}.npmrc
shamefully-hoist=true strict-peer-dependencies=false
Como isso foi útil no dia a dia
Implementação em projetos reais resultou em:
- Redução de 70% no tempo de instalação — Economizamos tempo valioso em cada setup de projeto
- Economia de 60% em espaço em disco — Importante quando trabalhamos com múltiplos projetos
- Aceleração de 3x em builds incrementais — Desenvolvimento mais rápido e feedback imediato
- Simplificação de 80% no compartilhamento de código — Reaproveitamento de código entre projetos ficou trivial
- Redução de 50% em problemas de versionamento — Menos conflitos e dependências desatualizadas
Conclusão
Monorepos com pnpm oferecem uma solução eficiente para gerenciar múltiplos pacotes. Workspaces nativos, performance superior e eficiência de espaço tornam o pnpm ideal para times que precisam de organização e produtividade.
A chave é começar simples, estabelecer padrões claros e evoluir conforme a necessidade. Com as ferramentas certas, um monorepo pode transformar a forma como seu time trabalha, acelerando desenvolvimento e melhorando a qualidade do código.
Este artigo reflete aprendizados práticos de configurar e manter monorepos com pnpm em projetos reais. Estratégias validadas em produção e aplicadas no dia a dia.