Versionamento: Semver, Changesets, deprecation, publicacao
9 min de leitura
Sua DS tem <Button>, <Card>, tokens,
Storybook, autodocs, MDX. Aí vem a pergunta
"como eu publico isso sem quebrar quem já
usa?". Sem versionamento, o consumidor
instala @sua-org/ui@1.2.3 e, do nada, na
próxima pnpm install, está com a versão
2.0.0 que mudou a API. App quebra. Confusão
geral.
DS grande em 2026 tem 3 práticas indissociáveis: Semver (a regra do que conta como breaking), Changesets (a ferramenta que o time do Storybook mantém pra gerenciar isso), e deprecation planejada (avisar antes de quebrar). Este nó cobre os 3.
O essencial 🟢
Semver: a regra. Semver (Semantic Versioning)
define que toda versão é MAJOR.MINOR.PATCH:
- MAJOR - quebra de compatibilidade. O consumidor precisa mudar código.
- MINOR - feature nova. O consumidor não precisa mudar código (mas pode ganhar feature nova).
- PATCH - bugfix. O consumidor não percebe diferença, só que o bug sumiu.
Exemplos:
1.0.0→1.0.1- bugfix em<Button>. App não muda.1.0.0→1.1.0- novo<Tabs>no DS. App não muda (a não ser que queira usar).1.0.0→2.0.0-<Button variant="primary">virou<Button intent="primary">. App precisa mudar.
A regra "MAJOR quando quebra" é o núcleo. Tudo o mais (Changesets, deprecation) é mecanismo pra garantir que essa regra é seguida.
O que conta como "quebra" em DS. Em DS React, quebra é:
- Mudou tipo de prop -
variant: "primary"virouvariant: 1(type incompatível). - Removeu prop -
<Button secondary>agora é erro (prop removida). - Mudou valor default -
<Button>semvariantagora édangerem vez deprimary(default mudou). - Mudou comportamento visível -
<Button loading>agora gira spinner diferente (UX mudou, código que esperava comportamento A quebrou). - Removeu componente -
<LegacyButton>removido. - Bump de peer dependency major - React 19 só, app em React 18 quebra.
- Mudou API de Context -
<CardContext>expõevariantagora, antes não expunha (consumidor que usavauseContextdireto quebrou).
O que NÃO conta como quebra (é MINOR ou PATCH):
- Adicionou prop -
<Button newProp={x}>. - Adicionou valor a union -
variantagora aceita"primary" | "secondary" | "danger" | "warning". App que só usava"primary"não muda. (Cuidado: em algumas linguagens isso conta como quebra, em TS é seguro.) - Adicionou componente - novo
<Tabs>no DS. - Bugfix visual - spinner agora gira consistentemente. Comportamento documentado mudou, mas o "comportamento esperado" era esse (era bug).
- Refatorou internamente - sem mudar API.
A regra prática: se o consumidor precisa mudar código, é MAJOR. Se não, é MINOR ou PATCH.
Changesets: a ferramenta padrão. Changesets (mantido pelo time do Storybook, depois independente) é a ferramenta que o ecossistema React usa pra:
- Coletar mudanças - cada PR com mudança
adiciona um changeset (arquivo
.mdcom a descrição). - Gerar CHANGELOG - na hora de release, o Changesets agrupa os changesets e escreve o CHANGELOG.
- Bump de versão - calcula a próxima versão
(MAJOR se algum changeset tem
major, MINOR se temminor, PATCH se só tempatch). - Publicar - roda
pnpm publish(ouchangesets publishcom NPM config).
pnpm add -D @changesets/cli
pnpm changeset init
Cria .changeset/config.json com a config base.
O fluxo de um PR com Changeset. Cada PR que muda comportamento do DS adiciona um changeset:
pnpm changeset
# ? Which packages would you like to include?
# - @sua-org/ui
# ? What kind of change is this?
# - patch (bugfix)
# - minor (feature)
# - major (breaking)
# ? Summary of the change:
# > Button loading agora usa aria-busy corretamente
Cria .changeset/cool-pumpkin-stand.md:
---
"@sua-org/ui": minor
---
Button loading agora usa aria-busy corretamente.
Antes mostrava o spinner sem anúncio ao leitor de
tela.
Esse arquivo vai no PR junto com o código. Quando o PR é mergeado na main, o changeset é consumido na próxima release.
Release PR (ou "Version Packages"). Changesets tem uma action oficial que abre um Version PR automaticamente quando detecta changesets pendentes:
# .github/workflows/changesets.yml
- uses: changesets/action@v1
with:
publish: pnpm changeset publish
version: pnpm changeset version
Quando você mergeia PRs com changesets, essa action:
- Roda
changeset version- agrupa changesets, bump versão empackage.json, atualizaCHANGELOG.md. - Faz commit "Version Packages" na main.
- Roda
changeset publish- publica no npm. - (Opcional) Abre um PR de "Version Packages" que você revisa antes do merge.
O resultado: o consumidor de @sua-org/ui sempre
sabe o que mudou entre versões (CHANGELOG)
e se precisa atualizar código (MAJOR vs
MINOR).
Fluxo visualizado:
Deprecation: avisar antes de quebrar. A regra de ouro: nunca quebre sem avisar com 1 minor de antecedência. O fluxo:
- Minor N - marca o componente como deprecated, adiciona warning no console quando usado. Continua funcionando.
- Major N+1 - remove o componente. App que não migrou quebra.
// v1.5.0: marca como deprecated
/**
* @deprecated Use `<Button variant="primary">` instead.
* Will be removed in v2.0.0.
*/
function PrimaryButton(props) {
if (process.env.NODE_ENV !== "production") {
console.warn(
"PrimaryButton is deprecated. Use <Button variant='primary'> instead. " +
"Will be removed in v2.0.0."
);
}
return <Button variant="primary" {...props} />;
}
// v2.0.0: remove
// PrimaryButton nao existe mais. App que usava
// precisa mudar pra <Button variant="primary">.
O aviso de deprecation no console é a maior
migração de DS que dá pra fazer. Sem ele,
o consumidor só descobre que quebrou quando
faz pnpm install e o app explode.
CodigoMod pra migração automatizada. Pra
mudanças mecânicas grandes (ex: renomear
variant="primary" em 50 lugares no app
consumidor), forneça um codemod (script
que reescreve o código automaticamente):
// transforms/primary-to-button-variant.js
module.exports = (file, api) => {
const j = api.jscodeshift;
const root = j(file.source);
// <PrimaryButton> -> <Button variant="primary">
root
.find(j.JSXElement, { openingElement: { name: { name: "PrimaryButton" } } })
.forEach((path) => {
path.value.openingElement.name = j.jsxIdentifier("Button");
path.value.openingElement.attributes = [
...path.value.openingElement.attributes,
j.jsxAttribute(j.jsxIdentifier("variant"), j.stringLiteral("primary")),
];
// ... (mesma coisa pro closingElement)
});
return root.toSource();
};
# Consumidor roda
npx jscodeshift --transform @sua-org/codemods/primary-to-button-variant.js src/
Em DS grande, codemods são essenciais. Storybook
mantém @storybook/codemods como exemplo (renomeia
storiesOf → CSF3 automaticamente). Vale a
pena se você tem dezenas de componentes
migrando.
peerDependencies corretas. A DS não
embute React (é peer dep). Isso evita ter
2 cópias de React no app do consumidor:
{
"name": "@sua-org/ui",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"devDependencies": {
"react": "^18.3.0",
"react-dom": "^18.3.0"
}
}
O consumidor instala React. A DS usa a
React do consumidor. Bump de peerDependencies
major (ex: React 17 → React 18 só) = MAJOR
da DS.
Publicação no npm: pnpm publish ou
changesets publish. Em DS grande, use
Changesets Action (vista acima) que automatiza
publicar. Em DS pequeno, pnpm publish manual
resolve:
pnpm build
pnpm publish --access public
--access public é obrigatório pra pacotes
com scope (@sua-org/ui é scoped). Sem isso,
o npm tenta publicar como privado e falha.
O que entra no .npmignore (ou files no
package.json). Por default, o npm publica
tudo da pasta. Pra evitar publicar
node_modules, .storybook, tests, etc:
{
"files": [
"dist",
"README.md",
"LICENSE"
]
}
files é a forma positiva (lista o que vai).
Alternativa é .npmignore (lista o que não
vai, tipo .gitignore). Use files - é mais
explícito.
Tagueamento de versão no git. Cada release é uma tag no git:
git tag v1.5.0
git push --tags
O GitHub Actions (ou o Changesets Action) gera
a tag automaticamente. Consumers podem referenciar
versões específicas no npm ("@sua-org/ui": "1.5.0"
em vez de "^1.5.0").
Aprofundamento 🟡
peerDependencies vs dependencies vs
devDependencies. Resumo:
dependencies- o que a DS usa em runtime. React, ReactDOM vão aqui se você publica comoreact, mas como DS você usa peer.peerDependencies- o que o consumidor tem que ter. React, ReactDOM, e qualquer outra lib que o consumidor já tem (TanStack Query, Radix, etc).devDependencies- o que só você usa pra desenvolver. Storybook, Vitest, etc. Não vai pro npm.
A pegadinha: peerDependencies não são
instaladas automaticamente. Se o consumidor
não tem React, o npm avisa mas não instala. Em
DS grande, use peerDependenciesMeta pra
marcar como opcional:
{
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"react": { "optional": false }
}
}
optional: false (default) significa "obrigatório
- tem que ter".
optional: truesignifica "se tiver, ok; se não, sem erro".
exports field no package.json (Node 12+).
Em vez de main e types separados, use o
exports field que define o que cada path
exporta:
{
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./styles.css": "./dist/styles.css"
}
}
Com exports, o consumidor faz:
import { Button } from "@sua-org/ui"; // ESM
const { Button } = require("@sua-org/ui"); // CJS
import "@sua-org/ui/styles.css"; // sub-export
Sem exports, o consumidor tem que adivinhar o
path (@sua-org/ui/dist/styles.css).
Trusted publishing com OIDC (2024+). Em vez
de NPM_TOKEN secret, use OIDC trust entre
GitHub e npm:
# .github/workflows/publish.yml
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
- run: pnpm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Migre pra OIDC:
- No npm, em "Publishing access" → "Trusted Publishers" → adicione o repo.
- Use
npm logincom OIDC em CI. - Sem
NPM_TOKENsecret, sem risco de leak.
Em 2026, OIDC é o caminho padrão. O suporte está em todos os providers principais (npm, GitHub Packages, Cloudflare, etc).
Pre-release versioning (alpha, beta, rc). Pra features em desenvolvimento, use pre-release:
2.0.0-alpha.1- alpha 1 da 2.0.2.0.0-beta.1- beta 1.2.0.0-rc.0- release candidate.2.0.0- release final.
Changesets suporta pre-release via config:
{
"prettier": false,
"snapshot": {
"useCalculatedVersion": true
}
}
Consumidor que quiser testar a 2.0 antes:
pnpm add @sua-org/ui@beta
@beta, @alpha, @rc, @next são tags
do npm que o pnpm add entende. Útil pra "release
candidate" antes do MAJOR final.
Cumulative vs single changesets. Changesets suporta dois modos:
- Cumulative (default) - cada changeset
descreve uma mudança (1 arquivo
.mdpor PR). Ochangeset versionagrupa tudo na release. Bom pra DS ativo. - Snapshot (use
changeset version --snapshot)- uma versão "snapshot" do repo, útil pra preview em PR. Menos comum.
Versionado em monorepo (Nx, Turborepo). Em
DS com várias packages (@sua-org/ui-core,
@sua-org/ui-icons, @sua-org/ui-tokens), use
Changesets com fixed ou independent mode:
// .changeset/config.json
{
"fixed": [["@sua-org/ui-core", "@sua-org/ui-icons", "@sua-org/ui-tokens"]]
}
fixed = todos bump juntos (1 versão shared).
independent = cada package bump separado
(mais flexível, mas mais confuso pro consumidor).
Pra quem quer ir além 🔴
História do Semver. Criado por Tom Preston-Werner (co-fundador do GitHub) em 2011, publicado em semver.org. Resolveu um problema real: npm (na época) e outras comunidades não tinham um padrão de "o que é breaking". O documento tem 12 regras, mas as 3 que importam são MAJOR.MINOR.PATCH.
A força do Semver é ser uma especificação
simples que virou convenção universal na
era do npm. Em 2026, é esperado que toda lib
publicada siga Semver - consumidor que vê
MAJOR.0.0 sabe que vai ter que mudar código,
e isso é informação valiosa.
Changesets vs Lerna vs Release Please. Tem 3 ferramentas de versionamento na era JS:
- Changesets (Time Storybook, depois
independente) - baseado em PR com arquivo
.changeset/*.md. O mais popular em DS React. - Lerna (Nrwl) - focado em monorepo, gerencia várias packages. Mais antigo. Changesets se integra com Lerna.
- Release Please (Google) - baseado em conventional commits. Automático, mas opinativo sobre mensagem de commit.
Em 2026, Changesets é o padrão em DS React. Lerna é mais usado em monorepo genérico (Nrwl Nx). Release Please é mais usado em projetos Google.
Por que peerDependencies é detalhe crítico.
O problema de duas cópias de React no
mesmo bundle é real e quebra o app. Quando
você tem react@18 em @sua-org/ui/Button.tsx
e react@18.2 no app/, são instâncias
diferentes, Context não compartilha estado,
hooks não compartilham scheduling, e o app
quebra de formas estranhas (Provider não
atualiza, useEffect roda 2x, etc).
peerDependencies força o consumidor a ter
React, e a DS usa a do consumidor. Em
projeto grande, isso é o que evita o inferno
de "2 Reacts".
Lockfile e DS versionada em monorepo. Em
monorepo com Nx/Turborepo, a DS é uma package
dentro. O consumidor (outro package no monorepo)
pode referenciar via workspace:* (pnpm) ou
"version": "file:../ds" (npm). Em CI, isso
cria acoplamento forte - mudar a DS quebra
todos os apps no monorepo. Em time grande,
o ideal é a DS ser publicada no registry interno
e os apps instalarem por versão - o lockfile
de cada app trava a versão, e quebrar a DS
fica opt-in (muda a versão, atualiza o app).
Leitura recomendada:
- Semver.org - a especificação oficial, 12 regras, leitura de 5 min.
- Changesets (oficial) - a ferramenta padrão.
- Storybook - Publish Storybook - workflow integrado com Storybook.
Dica: o erro mais comum no começo é não ter CHANGELOG. "Mudei o
<Button>na 1.2.0 mas não escrevi o que mudou". Consumidor instala a 1.2.0, algo quebra, vai no GitHub pra ver "o que mudou" - não tem. Frustração. Com Changesets, o CHANGELOG é gerado a partir dos changesets. Você não esquece.
No próximo nó, vamos falar de contract testing: Storybook test runner, addon a11y, Chromatic visual review - como garantir que o DS não quebra sem o consumidor perceber.
// Quiz
Qual a regra do Semver que importa na prática pra quem mantém um DS?