<h2>O Que é um Monorepo de Componentes React</h2>
<p>Um monorepo (repositório monolítico) é uma estrutura de projeto onde múltiplos pacotes ou módulos coexistem em um único repositório versionado. No contexto de componentes React, isso significa manter uma biblioteca completa de componentes reutilizáveis, seus testes, documentação e ferramentas de publicação em um só lugar, facilitando a manutenção, o versionamento e a distribuição.</p>
<p>A vantagem principal é a facilidade em manter consistência entre componentes. Quando você precisa fazer uma mudança que afeta vários componentes, tudo está centralizado. Além disso, simplifica o gerenciamento de dependências compartilhadas, já que todas as versões do React, TypeScript e outras libs estão declaradas uma única vez. Ferramentas como Yarn Workspaces e npm Workspaces tornaram isso viável e prático em projetos de qualquer tamanho.</p>
<p>Neste artigo, vamos construir um monorepo real com três pilares: documentação interativa via Storybook, testes visuais automatizados com Chromatic e releases automatizadas com versioning semântico. Essa é a stack moderna e profissional para libraries de componentes.</p>
<h2>Estrutura Base e Configuração do Monorepo</h2>
<h3>Iniciando o Projeto</h3>
<p>Começamos criando a estrutura de pastas e configurando os workspaces. Usaremos npm workspaces (disponível nativamente desde npm 7) para manter simplicidade.</p>
<pre><code class="language-bash">mkdir meu-design-system
cd meu-design-system
npm init -y</code></pre>
<p>Agora editamos o <code>package.json</code> raiz para declarar os workspaces:</p>
<pre><code class="language-json">{
"name": "meu-design-system",
"version": "1.0.0",
"private": true,
"workspaces": [
"packages/*"
],
"devDependencies": {
"@storybook/react": "^7.6.0",
"@storybook/addon-essentials": "^7.6.0",
"@chromatic-com/storybook": "^2.0.0",
"typescript": "^5.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}</code></pre>
<p>Criamos a estrutura de pastas:</p>
<pre><code class="language-bash">mkdir -p packages/button
mkdir -p packages/card
mkdir -p packages/badge
mkdir -p .storybook</code></pre>
<p>Cada pacote terá sua própria estrutura:</p>
<pre><code class="language-bash">cd packages/button
npm init -y</code></pre>
<p>O <code>package.json</code> do componente Button fica assim:</p>
<pre><code class="language-json">{
"name": "@meu-design-system/button",
"version": "0.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsc",
"test": "jest"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}</code></pre>
<h3>Configuração TypeScript Centralizada</h3>
<p>Na raiz do monorepo, criamos um <code>tsconfig.json</code> base que todos os pacotes herdam:</p>
<pre><code class="language-json">{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"jsx": "react-jsx",
"declaration": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true
}
}</code></pre>
<p>E cada pacote tem seu próprio <code>tsconfig.json</code> que estende este:</p>
<pre><code class="language-json">{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist", "*/.stories.tsx"]
}</code></pre>
<h2>Storybook: Documentação Interativa de Componentes</h2>
<h3>Estrutura e Configuração Inicial</h3>
<p>O Storybook é a ferramenta que permite documentar visualmente seus componentes. Vamos configurá-lo na raiz do monorepo. Primeiro, criamos a pasta <code>.storybook</code>:</p>
<pre><code class="language-bash">npx storybook init --type react</code></pre>
<p>A configuração principal fica em <code>.storybook/main.ts</code>:</p>
<pre><code class="language-typescript">import type { StorybookConfig } from '@storybook/react-webpack5';
const config: StorybookConfig = {
stories: ['../packages/*/.stories.tsx'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@chromatic-com/storybook'
],
framework: {
name: '@storybook/react-webpack5',
options: {},
},
webpackFinal: async (config) => {
config.module?.rules?.push({
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
});
return config;
},
};
export default config;</code></pre>
<p>E o arquivo de preview em <code>.storybook/preview.ts</code>:</p>
<pre><code class="language-typescript">import type { Preview } from '@storybook/react';
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;</code></pre>
<h3>Criando Histórias de Componentes</h3>
<p>Agora criamos o arquivo <code>packages/button/src/Button.tsx</code>:</p>
<pre><code class="language-typescript">import React, { ButtonHTMLAttributes } from 'react';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger'; size?: 'small' | 'medium' | 'large';
loading?: boolean;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'primary', size = 'medium', loading = false, children, ...props }, ref) => {
const baseStyles = 'font-semibold rounded cursor-pointer transition-colors';
const variantStyles = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
danger: 'bg-red-600 text-white hover:bg-red-700',
};
const sizeStyles = {
small: 'px-2 py-1 text-sm',
medium: 'px-4 py-2 text-base',
large: 'px-6 py-3 text-lg',
};
const className = ${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]};
return (
<button
ref={ref}
className={className}
disabled={loading || props.disabled}
{...props}
>
{loading ? 'Carregando...' : children}
</button>
);
}
);
Button.displayName = 'Button';</code></pre>
<p>Agora a história em <code>packages/button/src/Button.stories.tsx</code>:</p>
<pre><code class="language-typescript">import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta = {
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger'],
},
size: {
control: 'select',
options: ['small', 'medium', 'large'],
},
loading: {
control: 'boolean',
},
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Clique aqui',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Botão secundário',
},
};
export const Danger: Story = {
args: {
variant: 'danger',
children: 'Deletar',
},
};
export const Loading: Story = {
args: {
loading: true,
children: 'Salvando...',
},
};
export const AllSizes: Story = {
render: () => (
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button size="small">Pequeno</Button>
<Button size="medium">Médio</Button>
<Button size="large">Grande</Button>
</div>
),
};</code></pre>
<p>Para executar o Storybook localmente:</p>
<pre><code class="language-bash">npm run storybook</code></pre>
<p>Isso abre na porta 6006 uma interface interativa onde você pode ver todos os componentes e suas variações.</p>
<h2>Chromatic: Testes Visuais Automatizados</h2>
<h3>O Que é Chromatic e Por Que Usar</h3>
<p>Chromatic é um serviço que captura screenshots do Storybook e detecta automaticamente mudanças visuais indesejadas. Integra-se perfeitamente com CI/CD pipelines e pull requests, impedindo regressions visuais antes do merge. Isso é essencial em um design system onde aparência é crítica.</p>
<p>O fluxo é simples: cada vez que você faz push, o Chromatic compara as histórias atuais com a baseline anterior. Se encontrar diferenças, você revisa manualmente antes de aceitar ou rejeitar.</p>
<h3>Configuração e Integração</h3>
<p>Primeiro, instale a CLI do Chromatic:</p>
<pre><code class="language-bash">npm install -g chromatic</code></pre>
<p>Crie uma conta em https://www.chromatic.com, crie um projeto e obtenha seu <code>project-token</code>. Agora adicione ao seu <code>package.json</code> raiz:</p>
<pre><code class="language-json">{
"scripts": {
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"chromatic": "chromatic --project-token=sua_chave_aqui"
}
}</code></pre>
<p>Para testar localmente:</p>
<pre><code class="language-bash">npm run build-storybook
npm run chromatic</code></pre>
<p>Você pode também automatizar isso em GitHub Actions. Crie <code>.github/workflows/chromatic.yml</code>:</p>
<pre><code class="language-yaml">name: Chromatic
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
chromatic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '18'
- run: npm ci
- run: npm run build-storybook
- uses: chromaui/action@v1
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}</code></pre>
<h3>Configuração do Storybook com Chromatic</h3>
<p>O addon do Chromatic já está incluído no <code>main.ts</code> que configuramos anteriormente. Para refinar comportamentos, você pode adicionar ao <code>.storybook/preview.ts</code>:</p>
<pre><code class="language-typescript">import { Preview } from '@storybook/react';
const preview: Preview = {
parameters: {
chromatic: {
delay: 300, // aguarda animações
pauseAnimationAtEnd: true,
},
},
};
export default preview;</code></pre>
<p>Para ignorar determinadas histórias do Chromatic (útil para componentes muito dinâmicos):</p>
<pre><code class="language-typescript">export const Dynamic: Story = {
parameters: {
chromatic: { disableSnapshot: true },
},
render: () => <div>{new Date().toISOString()}</div>,
};</code></pre>
<h2>Releases Automatizadas e Versionamento</h2>
<h3>Entendendo Semantic Versioning</h3>
<p>Antes de automatizar, precisamos entender versionamento semântico (semver): MAJOR.MINOR.PATCH. Uma mudança é:</p>
<ul>
<li><strong>PATCH</strong>: correção de bugs (0.1.1 → 0.1.2)</li>
<li><strong>MINOR</strong>: nova funcionalidade backwards-compatible (0.1.0 → 0.2.0)</li>
<li><strong>MAJOR</strong>: mudança que quebra compatibilidade (1.0.0 → 2.0.0)</li>
</ul>
<p>O commit deve seguir o padrão Conventional Commits:</p>
<ul>
<li><code>feat: nova funcionalidade</code> → MINOR</li>
<li><code>fix: corrige bug</code> → PATCH</li>
<li><code>feat!: mudança breaking</code> → MAJOR</li>
</ul>
<h3>Configurando Semantic Release</h3>
<p>Instale a ferramenta que automatiza tudo:</p>
<pre><code class="language-bash">npm install --save-dev semantic-release @semantic-release/changelog @semantic-release/git @semantic-release/npm</code></pre>
<p>Crie <code>.releaserc.json</code> na raiz:</p>
<pre><code class="language-json">{
"branches": ["main", { "name": "develop", "prerelease": true }],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md"
}
],
[
"@semantic-release/npm",
{
"pkgRoot": "packages/button"
}
],
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md", "packages/*/package.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
]
}</code></pre>
<p>Para múltiplos pacotes no monorepo, use <code>lerna</code>:</p>
<pre><code class="language-bash">npm install --save-dev lerna</code></pre>
<p>Configure <code>lerna.json</code>:</p>
<pre><code class="language-json">{
"version": "independent",
"npmClient": "npm",
"useWorkspaces": true,
"packages": ["packages/*"],
"command": {
"publish": {
"conventionalCommits": true,
"message": "chore(release): publish"
}
}
}</code></pre>
<h3>Automação em CI/CD</h3>
<p>Adicione ao seu workflow em <code>.github/workflows/release.yml</code>:</p>
<pre><code class="language-yaml">name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build
- run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}</code></pre>
<h3>Exemplo Real de Fluxo de Release</h3>
<ol>
<li>Você faz commits seguindo Conventional Commits:</li>
</ol>
<pre><code class="language-bash">git commit -m "feat(button): adiciona prop aria-label"</code></pre>
<ol>
<li>Faz push para main:</li>
</ol>
<pre><code class="language-bash">git push origin main</code></pre>
<ol>
<li>O workflow executa:</li>
</ol>
<ul>
<li>Analisa commits desde a última tag</li>
<li>Detecta que é uma MINOR (feat)</li>
<li>Incrementa versão de 0.1.0 para 0.2.0</li>
<li>Atualiza CHANGELOG.md</li>
<li>Publica no npm</li>
<li>Cria uma release no GitHub</li>
</ul>
<ol>
<li>O pacote fica disponível:</li>
</ol>
<pre><code class="language-bash">npm install @meu-design-system/button@0.2.0</code></pre>
<p>A beleza está em ser completamente automático. Não há intervenção manual, reduzindo erros e mantendo consistência.</p>
<h2>Conclusão</h2>
<p>Aprendemos que um monorepo de componentes React profissional repousa em três pilares complementares. Primeiro, o <strong>Storybook oferece documentação viva e interativa</strong>, tornando componentes autodescritivos e permitindo que designers e developers trabalhem juntos. Segundo, o <strong>Chromatic automatiza testes visuais</strong>, impedindo regressions e fornecendo confiança para mudanças ousadas. Terceiro, <strong>Semantic Release + Conventional Commits eliminam fricção no versionamento</strong>, transformando releases em um processo determinístico e reproduzível.</p>
<p>A combinação dessas três tecnologias cria um workflow profissional que escala com o time. Componentes ficam documentados, testados visualmente e distribuídos automaticamente sem overhead manual. Novos developers ramp up mais rápido consultando o Storybook, designers entendem exatamente o que é suportado, e releases deixam de ser operações arriscadas para virar eventos confiáveis.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer">Storybook Documentation</a></li>
<li><a href="https://www.chromatic.com" target="_blank" rel="noopener noreferrer">Chromatic Official Website</a></li>
<li><a href="https://semantic-release.gitbook.io" target="_blank" rel="noopener noreferrer">Semantic Release Documentation</a></li>
<li><a href="https://docs.npmjs.com/cli/v9/using-npm/workspaces" target="_blank" rel="noopener noreferrer">npm Workspaces Guide</a></li>
<li><a href="https://www.conventionalcommits.org" target="_blank" rel="noopener noreferrer">Conventional Commits Specification</a></li>
</ul>
<p><!-- FIM --></p>