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
O primeiro passo é criar um novo projeto e inicializar o Lerna.
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
.
// ./lerna.json
{
"packages": ["packages/*"],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "independent"
}
// .package.json
{
"name": "root",
"private": true,
"workspaces": ["packages/*"],
"devDependencies": {
"lerna": "^4.0.0"
}
}
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
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
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"
]
}
🖼️ 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
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"
}
📦 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
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"
]
}
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
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"
(...)
❤️ 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;
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);
}
`;
packages/core/src/index.ts
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
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. 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
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"]
}
jest-setup.ts
import "@testing-library/jest-dom";
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"
}
};
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);
});
});
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%"
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"
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"
📫 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
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
⚠️ 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)
top man!