DEV Community

Cover image for Criando um pacote React para seu Design System
Renan Henrique Zanoti
Renan Henrique Zanoti

Posted on

Criando um pacote React para seu Design System

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 - Gerenciador do monorepo.
  • 📦 Yarn 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.

npm i -g lerna
Enter fullscreen mode Exit fullscreen mode

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

mkdir design-system
cd design-system
npx lerna init
Enter fullscreen mode Exit fullscreen mode

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.

// ./lerna.json

{
   "packages": ["packages/*"],
   "npmClient": "yarn",
   "useWorkspaces": true,
   "version": "independent"
}
Enter fullscreen mode Exit fullscreen mode
// .package.json

{
   "name": "root",
   "private": true,
   "workspaces": ["packages/*"],
   "devDependencies": {
      "lerna": "^4.0.0"
   }
}
Enter fullscreen mode Exit fullscreen mode

Nesse ponto é recomendado adicionar um arquivo .gitignore.


⚙️ Preparando o ambiente de desenvolvimento

  • 🚀 React - Biblioteca JavaScript para desenvolvimento dos componentes.
  • 🧩 TypeScript - 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 - Compila JavaScript e Typescript.

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

yarn add -W --dev react react-dom typescript
Enter fullscreen mode Exit fullscreen mode

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

yarn add -W --dev @babel/cli @babel/core @babel/preset-react @babel/preset-typescript
Enter fullscreen mode Exit fullscreen mode

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

// ./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"
   ]
}
Enter fullscreen mode Exit fullscreen mode

🖼️ Ambiente de trabalho

  • 🎨Storybook - 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!

npx -p @storybook/cli sb init
Enter fullscreen mode Exit fullscreen mode

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.

// .storybook/main.js

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

📦 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.

lerna create @renanzan/core --yes
Enter fullscreen mode Exit fullscreen mode

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

// packages/core/tsconfig.json

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

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

lerna add styled-components --scope=@renanzan/core
Enter fullscreen mode Exit fullscreen mode

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?

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

// packages/core/package.json

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

❤️ 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, 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

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;
Enter fullscreen mode Exit fullscreen mode

packages/core/src/Button/styles.tsx

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);
   }
`;
Enter fullscreen mode Exit fullscreen mode

packages/core/src/index.ts

export { default as Button } from "./Button";
Enter fullscreen mode Exit fullscreen mode

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

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>
);
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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

babel.config.js

module.exports = {
   presets: ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"]
}
Enter fullscreen mode Exit fullscreen mode

jest-setup.ts

import "@testing-library/jest-dom";
Enter fullscreen mode Exit fullscreen mode

jest.config.js

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"
   }
};
Enter fullscreen mode Exit fullscreen mode

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

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);
   });
});
Enter fullscreen mode Exit fullscreen mode

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.

"test": "jest --maxWorkers=50%",
"test:watch": "jest --watch --maxWorkers=25%"
Enter fullscreen mode Exit fullscreen mode

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.


📚 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

// packages/core/package.json

"build": "tsc --build --clean && tsc"
Enter fullscreen mode Exit fullscreen mode

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.

// package.json

"build": "lerna run build"
Enter fullscreen mode Exit fullscreen mode

📫 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.

//npm.pkg.github.com/:_authToken=<token:write>
@renanzan:registry=https://npm.pkg.github.com
Enter fullscreen mode Exit fullscreen mode

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.

//npm.pkg.github.com/:_authToken=<token:read>
@renanzan:registry=https://npm.pkg.github.com
Enter fullscreen mode Exit fullscreen mode

⚠️ 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.

Top comments (1)

Collapse
 
nicolaslima321 profile image
Nicolas Lima

top man!