Monorepo de Componentes React: Storybook, Chromatic e Releases: Do Básico ao Avançado Já leu

O Que é um Monorepo de Componentes React 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. 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. 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

O Que é um Monorepo de Componentes React

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.

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.

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.

Estrutura Base e Configuração do Monorepo

Iniciando o Projeto

Começamos criando a estrutura de pastas e configurando os workspaces. Usaremos npm workspaces (disponível nativamente desde npm 7) para manter simplicidade.

mkdir meu-design-system
cd meu-design-system
npm init -y

Agora editamos o package.json raiz para declarar os workspaces:

{
  "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"
  }
}

Criamos a estrutura de pastas:

mkdir -p packages/button
mkdir -p packages/card
mkdir -p packages/badge
mkdir -p .storybook

Cada pacote terá sua própria estrutura:

cd packages/button
npm init -y

O package.json do componente Button fica assim:

{
  "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"
  }
}

Configuração TypeScript Centralizada

Na raiz do monorepo, criamos um tsconfig.json base que todos os pacotes herdam:

{
  "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
  }
}

E cada pacote tem seu próprio tsconfig.json que estende este:

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist"
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist", "**/*.stories.tsx"]
}

Storybook: Documentação Interativa de Componentes

Estrutura e Configuração Inicial

O Storybook é a ferramenta que permite documentar visualmente seus componentes. Vamos configurá-lo na raiz do monorepo. Primeiro, criamos a pasta .storybook:

npx storybook init --type react

A configuração principal fica em .storybook/main.ts:

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;

E o arquivo de preview em .storybook/preview.ts:

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;

Criando Histórias de Componentes

Agora criamos o arquivo packages/button/src/Button.tsx:

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';

Agora a história em packages/button/src/Button.stories.tsx:

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>
  ),
};

Para executar o Storybook localmente:

npm run storybook

Isso abre na porta 6006 uma interface interativa onde você pode ver todos os componentes e suas variações.

Chromatic: Testes Visuais Automatizados

O Que é Chromatic e Por Que Usar

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.

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.

Configuração e Integração

Primeiro, instale a CLI do Chromatic:

npm install -g chromatic

Crie uma conta em https://www.chromatic.com, crie um projeto e obtenha seu project-token. Agora adicione ao seu package.json raiz:

{
  "scripts": {
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "chromatic": "chromatic --project-token=sua_chave_aqui"
  }
}

Para testar localmente:

npm run build-storybook
npm run chromatic

Você pode também automatizar isso em GitHub Actions. Crie .github/workflows/chromatic.yml:

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 }}

Configuração do Storybook com Chromatic

O addon do Chromatic já está incluído no main.ts que configuramos anteriormente. Para refinar comportamentos, você pode adicionar ao .storybook/preview.ts:

import { Preview } from '@storybook/react';

const preview: Preview = {
  parameters: {
    chromatic: {
      delay: 300, // aguarda animações
      pauseAnimationAtEnd: true,
    },
  },
};

export default preview;

Para ignorar determinadas histórias do Chromatic (útil para componentes muito dinâmicos):

export const Dynamic: Story = {
  parameters: {
    chromatic: { disableSnapshot: true },
  },
  render: () => <div>{new Date().toISOString()}</div>,
};

Releases Automatizadas e Versionamento

Entendendo Semantic Versioning

Antes de automatizar, precisamos entender versionamento semântico (semver): MAJOR.MINOR.PATCH. Uma mudança é:
- PATCH: correção de bugs (0.1.1 → 0.1.2)
- MINOR: nova funcionalidade backwards-compatible (0.1.0 → 0.2.0)
- MAJOR: mudança que quebra compatibilidade (1.0.0 → 2.0.0)

O commit deve seguir o padrão Conventional Commits:
- feat: nova funcionalidade → MINOR
- fix: corrige bug → PATCH
- feat!: mudança breaking → MAJOR

Configurando Semantic Release

Instale a ferramenta que automatiza tudo:

npm install --save-dev semantic-release @semantic-release/changelog @semantic-release/git @semantic-release/npm

Crie .releaserc.json na raiz:

{
  "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}"
      }
    ]
  ]
}

Para múltiplos pacotes no monorepo, use lerna:

npm install --save-dev lerna

Configure lerna.json:

{
  "version": "independent",
  "npmClient": "npm",
  "useWorkspaces": true,
  "packages": ["packages/*"],
  "command": {
    "publish": {
      "conventionalCommits": true,
      "message": "chore(release): publish"
    }
  }
}

Automação em CI/CD

Adicione ao seu workflow em .github/workflows/release.yml:

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 }}

Exemplo Real de Fluxo de Release

  1. Você faz commits seguindo Conventional Commits:
git commit -m "feat(button): adiciona prop aria-label"
  1. Faz push para main:
git push origin main
  1. O workflow executa:
  2. Analisa commits desde a última tag
  3. Detecta que é uma MINOR (feat)
  4. Incrementa versão de 0.1.0 para 0.2.0
  5. Atualiza CHANGELOG.md
  6. Publica no npm
  7. Cria uma release no GitHub

  8. O pacote fica disponível:

npm install @meu-design-system/button@0.2.0

A beleza está em ser completamente automático. Não há intervenção manual, reduzindo erros e mantendo consistência.

Conclusão

Aprendemos que um monorepo de componentes React profissional repousa em três pilares complementares. Primeiro, o Storybook oferece documentação viva e interativa, tornando componentes autodescritivos e permitindo que designers e developers trabalhem juntos. Segundo, o Chromatic automatiza testes visuais, impedindo regressions e fornecendo confiança para mudanças ousadas. Terceiro, Semantic Release + Conventional Commits eliminam fricção no versionamento, transformando releases em um processo determinístico e reproduzível.

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.

Referências


Artigos relacionados