Criando um pacote React para seu Design System
Tags: react, webdev, tutorial, ptbr
Published: 2022-06-25 21:37:47

Design Systems são uma ótima maneira de manter o estilo e a consistência de um projeto, seu planejamento e desenvolvimento deve ser feito com desenvolvedores front-end e equipe de design trabalhando em sinergia para definir padrões de interface.

O objetivo é criar um conjunto de regras e padrões que podem ser facilmente reutilizados em diferentes projetos e produtos, mantendo consistência e aumentando a agilidade da equipe de desenvolvimento.

---

## 🎉 Primeiro commit

- 🐉 [Lerna](https://lerna.js.org/) - Gerenciador do monorepo.
- 📦 [Yarn Workspaces](https://classic.yarnpkg.com/en/docs/workspaces/) - Gerenciamento lógico de vários pacotes.

### Criar o projeto inicial
É recomendado instalar o Lerna como dependência global, pois frequentemente iremos utilizar alguns comandos dele.

```shell
npm i -g lerna
```

O primeiro passo é criar um novo projeto e inicializar o Lerna.

```shell
mkdir design-system
cd design-system
npx lerna init
```

Isso é o suficiente para criar a estrutura inicial. Agora precisamos configurar o Yarn Workspaces, para isso basta modificar os arquivos `lerna.json` e `package.json`.

```json
// ./lerna.json

{
   "packages": ["packages/*"],
   "npmClient": "yarn",
   "useWorkspaces": true,
   "version": "independent"
}
```

```json
// .package.json

{
   "name": "root",
   "private": true,
   "workspaces": ["packages/*"],
   "devDependencies": {
      "lerna": "^4.0.0"
   }
}
```

Nesse ponto é recomendado adicionar um arquivo [.gitignore](https://github.com/renanzan/design-system-boilerplate/blob/master/.gitignore).

---

## ⚙️ Preparando o ambiente de desenvolvimento

- 🚀 [React](https://reactjs.org/) - Biblioteca JavaScript para desenvolvimento dos componentes.
- 🧩 [TypeScript](https://www.typescriptlang.org/) - Uma linguagem de programação fortemente tipada para garantir que o código seja consistente e confiável. Isto será útil para gerar os arquivos necessários para o autocomplete da lib.
- 🛠 [Babel](https://babeljs.io/) - Compila JavaScript e Typescript.

Iremos adicionar o React e o Typescript como dependência de desenvolvimento no Workspace com a flag `-W`.

```shell
yarn add -W --dev react react-dom typescript
```

Será necessário adicionar algumas dependências do Babel para compilar um arquivo React escrito em TypeScript.

```shell
yarn add -W --dev @babel/cli @babel/core @babel/preset-react @babel/preset-typescript
```

Crie um arquivo `tsconfig.json` na pasta raiz do projeto.

```json
// ./tsconfig.json

{
   "compilerOptions": {
      "module": "CommonJS",
      "declaration": true,
      "removeComments": true,
      "noLib": false,
      "emitDecoratorMetadata": true,
      "experimentalDecorators": true,
      "target": "ES6",
      "lib": ["ES6", "DOM"],
      "resolveJsonModule": true,
      "isolatedModules": true,
      "esModuleInterop": true,
      "jsx": "react",
   },
   "exclude": [
      "node_modules",
      "**/*.spec.ts"
   ]
}
```

---

## 🖼️ Ambiente de trabalho
- 🎨[Storybook](https://storybook.js.org/) - Documentação e visualização dos componentes.

O Storybook permite criação de um ambiente isolado perfeito para desenvolver e testar componentes. Ele será muito útil durante a etapa de desenvolvimento. Também é possível gerar uma página que funcionará como documentação e vitrine para os componentes desenvolvidos, algo como um playground UI interativo e descritivo.

Para configurar o storybook basta executar o seguinte código, o cli se encarrega do resto!

```shell
npx -p @storybook/cli sb init
```

Ta-da! Você já pode executar o comando `yarn storybook` e ver a mágica acontecer. Ao instalar o storybook alguns componentes exemplo estarão disponíveis e podem ser vistos na página.

Como estaremos usando a estrutura de monorepo não usaremos a pasta `stories` que foi criada na raiz do projeto, podemos nos livrar dela.

Agora, para que o Storybook saiba onde procurar por nossas histórias, precisaremos editar o arquivo `.storybook/main.js`.

```javascript
// .storybook/main.js

module.exports = {
   "stories": [
      "../packages/**/*.story.@(tsx|mdx)"
   ],
   "addons": [...],
   "framework": "@storybook/react"
}
```

---

## 📦 O primeiro pacote

Nossos pacotes serão gerenciados pelo Lerna e estarão localizados na pasta `/packages`.

### Escopos npm

Os `npm scopes` são usados para explicitar que este é um pacote pertencente a uma organização que possivelmente possui outros pacotes. Ao instalar mais de um pacote com o mesmo escopo ele irá compartilhar o mesmo diretório (node_modules) com seus irmãos de organização. Outra vantagem dessa abordagem é a possibilidade de limitar as permissões entre membros da organização.

É importante que o escopo do pacote criado seja o idêntico ao nome de uma organização que você possua permissão de leitura e escrita. Neste tutorial usarei minha conta do GitHub como escopo, o que é interpretado pelo GitHub como minha organização pessoal. Exemplo: `@renanzan/core`.

Leia mais sobre `npm scopes` aqui: https://docs.npmjs.com/about-scopes

### Criando pacotes

Para criar nosso primeiro pacote iremos executar um comando lerna.

```shell
lerna create @renanzan/core --yes
```

Para compilar a lib corretamente é necessário adicionar o seguinte arquivo `tsconfig.json` na raiz do pacote que acabamos de criar.
```json
// packages/core/tsconfig.json

{
   "extends": "../../tsconfig.json",
   "compilerOptions": {
      "baseUrl": ".",
      "outDir": "lib",
      "skipLibCheck": true
   },
   "exclude": [
      "lib",
      "__stories__"
   ],
   "include": [
      "**/*.ts",
      "**/*.tsx"
   ]
}
```

Agora, usando comandos do lerna, podemos instalar os pacotes que serão usados pela nossa lib da seguinte maneira:

```shell
lerna add styled-components --scope=@renanzan/core
```

Nota: Caso sua lib possua restrições de versionamento de dependências vale a pena considerar usar `peerDependencies` para fazer esse gerenciamento, você pode saber mais sobre elas em [Por que peerDependencies?](C:\Users\Renan\Desktop\Nova pasta\design-system)

Renomeie o arquivo `packages/core/lib` para `packages/core/src` e atualize o arquivo `package.json`.

```json
// packages/core/package.json

{
   (...)
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   (...)
   "bootstrap": "lerna bootstrap --use-workspaces"
   (...)
```

---

## ❤️ Core Package

A ideia deste pacote "core" é que ele exporte os componentes mais importantes e utilizados do nosso Design System, algo como uma lib indispensável para aqueles que forem usar nosso design system, é a mesma abordagem utilizada pelo [material-ui/core](https://www.npmjs.com/package/@material-ui/core), por exemplo.

Apague todos os arquivos que estiverem dentro da pasta `packages/core/src` e `packages/core/__tests__`. Crie uma nova pasta `packages/core/__stories__`.

Agora vamos escrever nosso primeiro componente, um botão simples.

`packages/core/src/Button/index.tsx`

```javascript
import React from "react";

import * as S from "./styles";

interface Props extends React.ParamHTMLAttributes<HTMLButtonElement> {
   children: React.ReactNode;
}

const Button: React.FC<Props> = ({ children, ...rest }) => {
   return (
      <S.Button {...rest}>
         {children}
      </S.Button>
   );
}

export default Button;
```

`packages/core/src/Button/styles.tsx`

```javascript
import styled from "styled-components";

export const Button = styled.button`
   cursor: pointer;
   border: none;
   padding: 8px 16px;
   border-radius: 4px;
   background: red;
   color: white;
   transition: 250ms;

   :hover {
      filter: brightness(0.95);
   }
`;
```

`packages/core/src/index.ts`

```javascript
export { default as Button } from "./Button";
```

Com isso temos um componente chamado "Button" exportado pela lib "core" que poderá ser importado facilmente em qualquer projeto que tenha a nossa lib core instalada.

Qual a vantagem disso? O código está completamente isolado e centralizado, todos os lugares que usarem este botão terão a mesma estilização e comportamento. Quando for necessário dar manutenção, basta alterar um arquivo e subir uma nova versão da lib.

Para desenvolver bons componentes reutilizáveis eles devem funcionar com o mínimo de dependências externas possível e possuir escopos de uso bem definidos. Algo como uma chave de fenda em uma caixa de ferramentas que sempre poderá ser usada para apertar um parafuso.

Para possibilitar a visualização dos componentes que estão sendo desenvolvidos usaremos o storybook, para isso basta criar o seguinte arquivo:

`packages/core/__stories__/Button.story.tsx`

```javascript
import React from "react";
import { Meta, Story } from "@storybook/react";

import { Button } from "../src";

export default {
   title: "Button",
   component: Button
} as Meta;


export const Default: Story = () => (
   <Button>Hello World</Button>
);
```

Ele funcionará como uma documentação/pré visualização do componente. Basta rodar o comando `yarn storybook` para visualizar o componente que acabamos de criar.

**Observe**
O Storybook é capaz de identificar alterações nos arquivos e fazer um "auto refresh", então podemos usa-lo como referência imediata enquanto desenvolvemos nossos componentes. Experimente alterar a cor de fundo do botão com o storybook rodando.

O storybook possui suporte para instalação de plugins. Para documentar seus componentes de maneira mais eficiente recomendo o uso do plugin [Docs Addon](https://storybook.js.org/addons/@storybook/addon-docs). Com ele é possível escrever uma documentação em markdown (MDX) e relacionar com o componente.

---

## 🩺 Testes Unitários com JEST

Uma feature que não foi testada na verdade será testada pelo usuário final.

De maneira simplificada os testes unitários podem nos ajudar a garantir que os componentes façam o que foram desenvolvidos para fazer. Eles podem ser rodados automaticamente sempre que alguma alteração no código for feita para garantir que seu funcionamento essencial não foi comprometido. Usaremos o Jest para isso.

Para começarmos precisaremos instalar as seguintes dependências no nosso Workspace.

```shell
yarn add -W --dev @testing-library/jest-dom "@testing-library/user-event@^13.5.0" @testing-library/react @testing-library/dom jest-environment-jsdom babel-jest jest
```

Adicione os seguintes arquivos de configuração Jest na raiz do projeto

`babel.config.js`

```javscript
module.exports = {
   presets: ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"]
}
```

`jest-setup.ts`

```javascript
import "@testing-library/jest-dom";
```

`jest.config.js`

```javascript
module.exports = {
   cacheDirectory: '.jest-cache',
   coverageDirectory: '.jest-coverage',
   coveragePathIgnorePatterns: ['<rootDir>/packages/(?:.+?)/lib/'],
   coverageReporters: ['html', 'text'],
   coverageThreshold: {
      global: {
         branches: 100,
         functions: 100,
         lines: 100,
         statements: 100
      }
   },
   testEnvironment: "jsdom",
   testPathIgnorePatterns: ['<rootDir>/packages/(?:.+?)/lib/'],
   setupFilesAfterEnv: ['<rootDir>/jest-setup.ts'],
   moduleNameMapper: {
      "\\.(css|less|scss|sass)$": "identity-obj-proxy"
   }
};
```

Com isso terminamos de configurar o Jest no nosso projeto e já podemos começar escrever o primeiro teste unitário.

`packages/core/__tests__/Button.spec.js`

```javascript
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";

import { Button } from "../src";

const ChildNode = () => <span data-testid="button-text">Clique aqui</span>;

describe('Button', () => {
   it("Deve ser capaz de renderizar elementos filhos.", () => {
      render(<Button><ChildNode /></Button>);

   	expect(screen.getByRole("button")).toHaveTextContent("Clique aqui");
      expect(screen.getByTestId("button-text")).toBeInTheDocument();
   });

   it("Deve acionar a função onClick apenas uma vez quando clicar no botão.", () => {
      const handleClick = jest.fn();

      render(<Button onClick={handleClick} />);

   
      userEvent.click(screen.getByRole('button'));

		expect(handleClick).toHaveBeenCalledTimes(1);
   });
});
```

Para executar nossos testes podemos adicionar o seguinte script no `package.json` da raiz do projeto, trata-se de um pequeno hack para rodar os testes mais rápido.

```json
"test": "jest --maxWorkers=50%",
"test:watch": "jest --watch --maxWorkers=25%"
```

Saiba mais sobre este hack para rodar os testes com jest mais rápido em [Make Your Jest Tests up to 20% Faster by Changing a Single Setting](https://dev.to/vantanev/make-your-jest-tests-up-to-20-faster-by-changing-a-single-setting-i36).

---

## 📚 Buildando os pacotes

Fazendo uma relação com pacotes reais nesta etapa estamos lacrando o pacote antes de enviar para os correios. Ao buildar teremos uma versão do código compilado.

O código compilado é mais leve e possui correções de compatibilidade, por isso quando um pacote é instalado em outro projeto na verdade apenas os arquivos compilados são levados para ele. Por isso o build é uma etapa importante antes de publicar uma nova versão do pacote!

O `package.json` e o `tsconfig.json` são os arquivos responsáveis por detalhar como esse build é feito. Se você seguiu o tutorial até aqui eles já devem estar configurados corretamente para o primeiro build.

Basta adicionar o seguinte script no nosso pacote

```json
// packages/core/package.json

"build": "tsc --build --clean && tsc"
```

Para fazer o build apenas deste pacote precisamos acessar seu diretório raiz a partir do terminal e rodar o comando "yarn build".

Como estamos trabalhando com monorepo podemos ter o interesse de fazer o build de todos os nossos pacotes com um único comando. Para isto basta executar o comando `lerna run build`.

Para simplificar podemos adicionar o script na raiz do projeto.

```json
// package.json

"build": "lerna run build"
```

---

## 📫 Publicando sua lib no GitHub Packages

O GitHub Packages é uma boa opção para uma lib privada. Com ele podemos definir quem pode instalar e quem pode subir novas versões da lib.

Para fazer essa gestão precisaremos gerar tokens privados em https://github.com/settings/tokens clicando no botão `Generate new token` com uma conta que possua privilégios de administrador da organização da lib.

Para um token que permita, àqueles que o possuam, apenas a instalação dos pacotes, é necessário que conceda apenas privilégio de leitura. Para isso basta que o item `read:packages` esteja assinalado.

Em um token que permita subir novas versões da lib será necessário ceder privilégio de escrita `write:packages`.

Para subir novas versões da lib será necessário criar um arquivo `.npmrc` na raiz do projeto `design-system` com o token de escrita.

```json
//npm.pkg.github.com/:_authToken=<token:write>
@renanzan:registry=https://npm.pkg.github.com
```

Este token pode ser compartilhado entre os membros responsáveis pelo desenvolvimento da lib e de projetos que usarão a lib como dependência.

Para instalar essa dependência em outros projetos também será necessário criar um arquivo `.npmrc` na raiz, porém com o token de leitura.

```json
//npm.pkg.github.com/:_authToken=<token:read>
@renanzan:registry=https://npm.pkg.github.com
```

⚠️ Atenção, possuir estes tokens significa possuir permissões de leitura/escrita incondicional para as libs da organização, mantenha esta chave segura e não suba o arquivo `.nmprc` para o github, o arquivo `.gitignore` pode te ajudar nessa gestão.

---

O código fonte do projeto está disponível em https://github.com/renanzan/design-system-boilerplate.