[PRIMEIRO ARTIGO] - Como Resolvi o Caos de Dependências na CLI da Pittaya UI
O Dia em que Tudo Quebrou
Semana 4 de desenvolvimento da CLI da Pittaya. Tínhamos 13 componentes no registry, um sonho, e estava tudo funcionando muito "bem" - até não estar mais.
O Lucas, um dos amigos que desenvolvem a Pittaya comigo, tenta instalar o componente orbit-images pra "testar o brinquedo novo" que o Marcos havia desenvolvido:
$ npx pittaya add orbit-images
✅ orbit-images installed!
Na minha máquina? Funcionava. No projeto dele?
Boom:
Error: Cannot find module './button'
Ele abre o código e encontra:
// orbit-images.tsx
import { Button } from "./button";
A dependência está bem ali no código. Mas a CLI não detectou. Por quê?
Porque eu tinha programado a CLI com um regex que só capturava imports absolutos. E esse componente, em específico, tinha um import relativo. E aí é que estava o problema: ele era invisível para o meu parser inocente.
Pior: descubro que 5 dos 13 componentes tinham o mesmo problema. Mas ninguém tinha notado porque a nós vínhamos copiando e colando exemplos que já funcionavam.
Era uma bomba relógio de débito técnico.
O Horror do Regex
Olho para o código da época:
// cli/scripts/build-registry.ts (2024)
function extractRegistryDependencies(content: string): string[] {
const deps = new Set<string>();
// Regex que só pega imports absolutos
const uiImportRegex = /from\s+["']@\/components\/ui\/([^"']+)["']/g;
let match;
while ((match = uiImportRegex.exec(content)) !== null) {
deps.add(match[1]);
}
return Array.from(deps).sort();
}
Parecia seguro? Muito. E, relativamente, ele era. Funciona em 60% dos casos. Mas é fundamentalmente frágil:
// ❌ Não detecta - import relativo
import { Button } from "./button";
// ❌ Não detecta - alias
import { Button } from "@ui/button";
// ❌ Não detecta - múltiplas linhas
import {
Button,
Badge
} from "@/components/ui/button";
// ❌ Não detecta - CommonJS
const { Button } = require("./button");
Pior que tudo? Ninguém reclama até quebrar em produção. Porque localmente, o dev testa tudo junto. Só quando alguém tenta usar o componente isolado no projeto dele é que descobre que faltam dependências.
O Custo Real Disso
- Meia hora que cada dev perde debugando "Cannot find module"
- Branches sendo revertidas
- Confiança na CLI caindo
- Meu tempo corrigindo issues que eram completamente evitáveis
E o pior: isso só piora conforme você adiciona mais componentes. A probabilidade de colisão é logarítmica.
O momento Eureka: Abstract Syntax Tree
Conversando com um amigo sobre o problema, ele me questiona se não haveria uma maneira "inteligente" de resolver isso. E aí me veio em mente um conceito quase esquecido nos meus anos de prática: por que não usar AST?
Meu primeiro pensamento: AST é coisa complicada de compiler, não é?
Pesquiso um pouco.
Descubro que a TypeScript Compiler API é exatamente o que preciso. Mesma tecnologia que o TS usa pra parsear código. Se é bom o suficiente para o TypeScript, é bom o suficiente pra mim.
Mas AST Nativo do TS, assim como em qualquer linguagem, é verboso demais:
import * as ts from "typescript";
const sourceFile = ts.createSourceFile(
"temp.tsx",
content,
ts.ScriptTarget.Latest,
true
);
// Agora preciso navegar essa árvore manualmente...
// Esse código ficaria com 200+ linhas em poucas horas
Então encontro o ts-morph: uma abstração elegante sobre o Compiler API. API simples, mantida, comunidade ativa.
Decido: vou mirar nisso.
Primeira Tentativa: A Implementação Ingênua
Instalo ts-morph e escrevo a primeira versão:
import { Project } from "ts-morph";
function extractRegistryDependenciesWithAST(
content: string,
componentName: string
): string[] {
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile(`${componentName}.tsx`, content);
const deps = new Set<string>();
const imports = sourceFile.getImportDeclarations();
for (const imp of imports) {
const spec = imp.getModuleSpecifierValue();
// Tira tudo que vem depois de /
if (spec.startsWith("@/components/ui/")) {
const name = spec.replace("@/components/ui/", "").split("/")[0];
deps.add(name);
}
}
return Array.from(deps).sort();
}
Testo com orbit-images:
// orbit-images.tsx
import { cn } from "@/lib/utils";
import { Button } from "./button"; // ← Relativo
export function OrbitImages() {
return <Button>Click</Button>;
}
Resultado:
{
"name": "orbit-images",
"registryDependencies": ["utils"]
}
Ainda não detecta button porque eu só trato imports absolutos.
Ah. Claro.
Refatoração 1: Detectar Relativos
else if (spec.startsWith("./") || spec.startsWith("../")) {
// Extrai o nome do arquivo
const resolved = spec
.split("/")
.pop()
?.replace(/\.(tsx?|jsx?)$/, "");
if (resolved) deps.add(resolved);
}
Agora funciona:
{
"name": "orbit-images",
"registryDependencies": ["button", "utils"] // ✅
}
Mas noto um problema potencial.
Se alguém fizer:
import { Button } from "../ui/button";
import { Badge } from "../../components/ui/badge";
Meu parser não sabe qual pasta está sendo importada. Pode virar "ui" ou "badge" dependendo de onde o arquivo tá. A solução inteligente que o meu amigo propôs me parece um eterno jogo de correr atrás de uma solução e outro problema surgir.
Refatoração 2: Resolução de Path Apropriada
function extractComponentNameFromRelativePath(
modulePath: string,
componentName: string,
isLibrary: boolean
): string | null {
// Se tá em library: src/lib/components/button.tsx
// Relativos resolvem da pasta do componente
const baseDir = isLibrary
? "src/lib/components"
: "src/components/ui";
let targetPath = path.normalize(
path.join(baseDir, componentName.replace(/\.(tsx?|jsx?)$/, ""), modulePath)
);
// Extrai apenas o filename
const fileName = path.basename(targetPath)
.replace(/\.(tsx?|jsx?)$/, "");
return fileName;
}
Agora preciso testar contra TODOS os componentes.
Faço isso.
Descubro que há 3 componentes que não detecta corretamente - justamente alguns edge cases com re-exports e imports dinâmicos.
O Novo Problema: Edge Cases
Enquanto estava testando, descubro coisas que não esperava:
Caso 1: Dialog com implicit dependencies
// dialog.tsx
export function Dialog({ children }) {
return <DialogContext>{children}</DialogContext>;
}
export function DialogContent() {
// Usa Dialog internamente (implícito)
return <div>Content</div>;
}
A análise AST detecta apenas imports TypeScript. Mas não vê que "DialogContent depende de Dialog" porque é export implícito.
Caso 2: Re-exports
// index.ts
export { Button } from "./button";
export { Badge } from "./badge";
// my-component.tsx
import { Button } from "@/components/ui";
Aqui, o import vem de @/components/ui, não de @/components/ui/button. Como vou saber que precisa de button e badge?
Caso 3: Imports Dinâmicos
const Badge = await import("./badge").then(m => m.Badge);
AST estático não pega isso.
💭 A Decisão: Validação Manual + Detecção Automática
Percebo que AST sozinho não é bala de prata. Preciso de um sistema híbrido real:
- Detecção automática para 95% dos casos (imports estáticos)
- Override manual para 5% dos casos (edge cases)
- Validação inteligente que avisa quando override é redundante
Crio o sistema final no build-registry.ts:
// Extract dependencies using AST
const registryDepsFromContent = isLibrary
? []
: extractRegistryDependenciesWithAST(content, componentName, isLibrary);
const registryDeps = new Set<string>(registryDepsFromContent);
// Process internalDependencies declared manually
if (internalDependencies && internalDependencies.length > 0) {
const autoDetected: string[] = [];
const manualOnly: string[] = [];
internalDependencies.forEach(dep => {
if (registryDepsFromContent.includes(dep)) {
autoDetected.push(dep); // Redundante (já detectado)
} else {
manualOnly.push(dep); // Necessário (edge case)
}
registryDeps.add(dep);
});
// Feedback ao desenvolvedor
if (autoDetected.length > 0) {
console.log(` ℹ️ Auto-detected: ${autoDetected.join(", ")} (internalDependencies not needed)`);
}
if (manualOnly.length > 0) {
console.log(` ✓ Manual override: ${manualOnly.join(", ")}`);
}
}
Benefício: Se alguém esquece de remover uma dependência que agora é auto-detectada, o sistema avisa no build.
📦 Processing components...
✓ button (ui)
registryDependencies: ["@radix-ui/react-slot"]
✓ copy-button (ui)
ℹ️ Auto-detected: button (internalDependencies not needed)
✓ dialog (ui)
✓ Manual override: content (implicit dependency)
Validação de Dependências NPM
Tem também um segundo layer de validação que roda após o registry ser gerado. Checa se todas as dependências NPM usadas no código estão declaradas. Se alguém esquecer de declarar sonner ou lucide-react, essa validação grita durante o build.
O output fica assim:
🔍 Validating dependencies...
⚠️ button (default): missing [class-variance-authority]
❌ Some components have missing dependencies!
💡 Run: npm run validate:deps for details
Os Números que Importam
Depois que deploy a solução, meço tudo:
Antes (Regex puro)
| Métrica | Valor |
|---|---|
| Componentes no registry | 13 |
Com internalDependencies manual | 5 (38%) |
| Tempo de build | ~1.5s |
| Bugs de deps faltantes/mês | 2-3 |
| Precisão de detecção | ~60% |
Depois (AST + Validação)
| Métrica | Valor |
|---|---|
| Componentes no registry | 19 |
Com internalDependencies manual | 1 (5%) |
| Tempo de build | ~2s |
| Bugs de deps faltantes/mês | 0 |
| Precisão de detecção | 100% |
Redução de configuração manual: 92%
Aumento de confiabilidade: ∞
O Que Muda na Performance e DX
Para Performance
- ✅ Build ligeiramente mais lento (+500ms) - aceitável porque é uma vez
- ✅ Runtime da CLI não é afetado - as dependências vêm do registry pré-compilado
- ✅ Menos erros em produção - não há overhead de debugging
Para Developer Experience
Antes:
$ npx pittaya add multi-select
✅ Installed!
# Dois dias depois em produção:
Error: Cannot find module './popover'
# Dev precisa:
# 1. Debugar qual component falta
# 2. Adicionar manualmente
# 3. Re-testar tudo
# 4. Re-deploy
Depois:
$ npx pittaya add multi-select
✅ multi-select installed (dependencies: button, badge, popover)
✅ All dependencies resolved correctly
# Produção: funciona (zero surpresas)
Diferença: entre "espero que funcione" e "sei que vai funcionar".
O Que Aprendi (E o que virou lição)
1. Regex Não É Parser
Sempre que você tá fazendo text matching em código, é hora de reconsiderar. Use a ferramenta que o compilador usa. Sério.
Isso economiza horas de debugging estúpido depois.
2. Premature Optimization vs. Premature Pessimism
Pensei que "AST seria overkill". Não era. Era exatamente a solução certa pra o trabalho.
O inverso também é verdade: premature pessimism é tão caro quanto premature optimization.
3. Validação Inteligente > Documentação
Ao invés de escrever "Por favor, lembre de adicionar internalDependencies...", deixa o sistema avisar quando não precisa.
Sistema inteligente > documentação que ninguém lê.
4. Edge Cases Não São Bugs
Quando descobre que AST não pega imports dinâmicos, não significa que AST falhou. Significa que 5% dos casos precisam de um override manual. Isso é OK. Não é falha de arquitetura.
Soluções híbridas (auto + manual) são superiores a automação pura quando edge cases existem.
Resultado Final
Hoje, a Pittaya CLI:
- ✅ Detecta 100% das dependências estáticas automaticamente
- ✅ Oferece escape hatch pra edge cases
- ✅ Valida e avisa quando override é redundante
- ✅ Escala horizontalmente, uma vez que adicionar componentes é trivial
- ✅ Zero manutenção por componente
Desde a migração (há 3 meses): 0 bugs relacionados a dependências faltantes.
Antes? 2-3 por mês durante o período de desenvolvimento.
É o tipo de solução que é invisível quando funciona bem - que é exatamente como deve ser.
TL;DR pra seu próprio projeto
Se você tá em situação similar:
- Regex é bom até não ser mais - acontece quando seus padrões crescem
- AST não é overkill pra análise de código - é exatamente a ferramenta certa
- Ferramentas como ts-morph tornam isso acessível - use-as
- Soluções híbridas (auto + manual) > automação pura - edge cases existem
- Validação inteligente vence documentação - sistema que avisa > docs que ninguém lê
E Você?
Já teve que lidar com aquele problema onde "funciona localmente mas quebra em produção"?
Deixa eu saber nos comentários:
- Qual era a raiz do problema? (Falta de validação, manutenção manual, another tool?)
- Você resolveu com análise estática (AST, linter) ou outra abordagem?
- Quanto tempo e quantas horas de debugging você economizaria com uma solução assim?
- Qual é o seu "regex frágil" secreto que tá queimando seu time agora?
A comunidade aprende com experiências reais.
Minha lib de UI: aqui
Escrito no final de uma noite de sexta, percebendo que teria economizado 20 horas de minha vida se tivesse feito isso antes.