24/07/2026

Major, minor e patch: por que o acento circunflexo do package.json decide se seu upgrade quebra

Rodei um upgrade de dependências num projeto e nada quebrou. Não foi sorte: foi o caret do package.json travando as majors. Como o SemVer funciona, o que cada símbolo faz e por que ainda assim eu verifico o build.

Major, minor e patch: por que o acento circunflexo do package.json decide se seu upgrade quebra

Rodei um pnpm update num projeto meu, um portfólio de domínios em React e Vite, e o diff veio gordo: 1557 linhas inseridas e 1610 removidas só no lockfile. Trinta e poucos pacotes com versão nova.

A primeira reação diante de um diff desse tamanho é sempre a mesma: quebrou alguma coisa? Rodei typecheck, testes, build e abri a aplicação no navegador. Passou tudo. Zero erro de console.

Não foi sorte. Olhando de novo os números das versões, dava pra prever o resultado antes de rodar qualquer comando: nenhum pacote tinha subido de major. E é justamente isso que o ^ no package.json garante, silenciosamente, desde o dia em que você instalou cada dependência.

O que é SemVer, rapidamente

SemVer (Semantic Versioning) é a convenção que dá significado aos três números de uma versão. O formato é MAJOR.MINOR.PATCH, e cada posição comunica uma coisa diferente pra quem vai atualizar.

Pegando o TypeScript do meu upgrade, que foi de 5.8.3 pra 5.9.3:

  • PATCH (5.9.25.9.3): correção de bug. Nada de novo, nada removido. Só conserta o que estava errado.
  • MINOR (5.8.x5.9.0): funcionalidade nova, mas retrocompatível. Ganhou recurso, e o código que você já tinha continua funcionando igual.
  • MAJOR (5.x6.0.0): breaking change. Removeram uma função, mudaram o comportamento de um parâmetro, inverteram um default. Aqui seu código pode quebrar.

A regra prática cabe numa linha: só o primeiro número quebra seu código.

Onde o caret entra na história

Esse é o pedaço que faltava pra eu entender por que o upgrade foi tranquilo. Abre qualquer package.json e olha o símbolo antes do número:

"typescript": "^5.9.3"

Aquele acento circunflexo é o caret. Ele significa: aceite qualquer versão nova, desde que o primeiro número continue sendo 5.

NotaçãoAceitaNão aceitaQuando usar
^5.9.3 (caret)5.9.4, 5.10.0, 5.99.06.0.0Default do npm/pnpm. Pega correção e recurso novo, trava breaking change.
~5.9.3 (tilde)5.9.4, 5.9.95.10.0Mais conservador: só correção de bug, sem recurso novo.
5.9.3 (exato)5.9.3todo o restoQuando você precisa de reprodutibilidade absoluta.

Todas as dependências do meu projeto usam caret. Por isso o pnpm update pegou tudo que havia de mais novo dentro dos ranges e parou nas fronteiras de major. Não precisei configurar nada: a proteção já estava escrita no arquivo.

Foi o que aconteceu na prática:

PacoteAntesDepoisTipo
typescript5.8.35.9.3minor
eslint9.32.09.39.5minor
wrangler4.93.04.114.0minor
react-hook-form7.61.17.82.0minor
vite5.4.195.4.21patch
react18.3.118.3.1inalterado

Só minor e patch. A promessa do SemVer é exatamente que isso não quebre, e o build confirmou que a promessa se cumpriu.

A pegadinha das versões 0.x

Aqui tem uma exceção que pega muita gente, e eu tenho três casos dela no mesmo projeto: lucide-react em ^0.462.0, next-themes em ^0.3.0, vaul em ^0.9.9.

Antes do 1.0, o projeto é considerado instável pela própria convenção. E o caret muda de comportamento: em 0.x, ele trava no segundo número. ^0.3.0 aceita 0.3.7, mas não sobe pra 0.4.0.

Faz sentido quando você pensa: num projeto pré-1.0, o segundo número é que faz o papel de major. A biblioteca ainda está se achando, e quebrar API entre 0.3 e 0.4 é esperado. O caret é mais restrito justamente porque a promessa de estabilidade ainda não foi feita.

Por que eu ainda verifico

Poderia terminar o post aqui, com a conclusão confortável de que o caret resolve. Mas seria desonesto.

SemVer é uma convenção, não uma garantia técnica. Nada no npm impede um mantenedor de quebrar algo num minor por descuido. É raro em pacote maduro, mas acontece. O número é uma promessa humana, não uma verificação automática.

Por isso, mesmo com o diff dizendo "só minor e patch", rodei:

  1. pnpm install --frozen-lockfile, pra confirmar que o lockfile bate com o package.json. Se não bater, o CI vai falhar depois e você descobre no pior momento.
  2. Typecheck (tsc --noEmit), que é o teste que pega mudança de assinatura de tipo sem custo nenhum.
  3. Build de produção, porque compilar é diferente de checar tipo.
  4. A aplicação aberta no navegador, porque build passar não significa que a tela renderiza.

Esse último ponto foi o que mais me rendeu no dia. O projeto tem exatamente um teste unitário, e ele é trivial: não valida nada da aplicação. Se eu tivesse olhado só pro pnpm test verde, teria uma falsa sensação de cobertura. Quem me deu confiança de verdade foi abrir as duas telas e ver 0 erro no console.

O truque pra separar o que é seu do que já estava lá

O lint falhou depois do upgrade: 3 erros. A pergunta óbvia é se o upgrade causou aquilo.

Em vez de adivinhar, criei um worktree do Git no commit anterior, instalei as dependências antigas e rodei o lint lá:

git worktree add --detach /tmp/pre-upgrade HEAD
cd /tmp/pre-upgrade && pnpm install --frozen-lockfile
pnpm lint

Resultado idêntico: mesmos 3 erros, mesmos arquivos. Os problemas eram pré-existentes, em código gerado que o upgrade nem tocou.

Vale como técnica geral: worktree é a forma barata de comparar comportamento entre dois commits sem mexer no seu working tree. Você não precisa dar stash, nem trocar de branch, nem arriscar perder o que estava fazendo. Cria, testa, remove.

E quando você quiser subir uma major?

Aí o processo é outro, e o caret não te ajuda mais: ele foi feito exatamente pra impedir isso.

Subir de React 18 pra 19, por exemplo, exige editar o package.json na mão ou usar um comando explícito (pnpm update --latest). E o método muda:

  1. Uma major por vez. Se subir cinco pacotes juntos e a tela ficar branca, você não sabe qual foi.
  2. Ler o changelog antes, não depois. Projeto sério publica guia de migração, e ele costuma listar exatamente o que quebrou.
  3. Commit separado. Major não entra no mesmo commit que patch, porque o git revert precisa ser cirúrgico.

Se você usa Claude Code ou outro agente pra ajudar nesse tipo de tarefa, vale ter o processo escrito no CLAUDE.md do projeto: "nunca suba major sem perguntar" é o tipo de regra que evita um diff surpresa.

Resumo

Faça isso:

  • Mantenha o caret (^) nas dependências. É o default e é bom default.
  • Rode pnpm update com frequência: upgrades pequenos são fáceis de verificar.
  • Verifique com typecheck e build, não só com a suíte de testes (principalmente se ela for magra).
  • Use worktree pra provar se um erro é novo ou pré-existente.

Evite isso:

  • Confiar que "só minor" significa "impossível quebrar". SemVer é promessa, não garantia.
  • Subir várias majors no mesmo commit.
  • Esquecer que 0.x segue outra regra: ali o segundo número é que manda.

O resumo do resumo: o caret já te protege da categoria mais perigosa de atualização. O que sobra pra você é verificar, e verificar é barato quando o upgrade é pequeno.

Referências

#frontend #produtividade

Tags

Se eu te ajudei de alguma forma e você quiser retribuir, pode me pagar um café ☕ ou ainda usar um dos meus links de indicação para abrir conta em serviços como Asaas, Bunny, Clara, assinar conteúdos gratuitos ou usar serviços recomendados.

Você pode me encontrar no: Github, Behance, LinkedIn, YouTube, Instagram, X ou por email hello@marciotoledo.com!

© 2002 - 2026 | Marcio Toledo. Todos os direitos reservados. For LLMs