Executando verificação de segurança...
4

[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:

  1. Detecção automática para 95% dos casos (imports estáticos)
  2. Override manual para 5% dos casos (edge cases)
  3. 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étricaValor
Componentes no registry13
Com internalDependencies manual5 (38%)
Tempo de build~1.5s
Bugs de deps faltantes/mês2-3
Precisão de detecção~60%

Depois (AST + Validação)

MétricaValor
Componentes no registry19
Com internalDependencies manual1 (5%)
Tempo de build~2s
Bugs de deps faltantes/mês0
Precisão de detecção100%

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:

  1. Regex é bom até não ser mais - acontece quando seus padrões crescem
  2. AST não é overkill pra análise de código - é exatamente a ferramenta certa
  3. Ferramentas como ts-morph tornam isso acessível - use-as
  4. Soluções híbridas (auto + manual) > automação pura - edge cases existem
  5. 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.

Carregando publicação patrocinada...