DEV Community

Cover image for Criando Monorepo com Lerna e Yarn Workspaces
Eduardo Rabelo
Eduardo Rabelo

Posted on

Criando Monorepo com Lerna e Yarn Workspaces

Conforme os aplicativos crescem, você inevitavelmente chegará a um ponto em que deseja escrever componentes reutilizáveis ​​compartilhados que podem ser usados ​​em qualquer lugar. Historicamente, temos repositórios separados para cada pacote. No entanto, isso se torna um problema por alguns motivos:

  • Não se ajusta bem. Antes que você perceba, você tem dezenas de repositórios de pacotes diferentes repetindo o mesmo processo de construção, teste e lançamento.
  • Promove o agrupamento de componentes desnecessários. Precisamos criar um novo repo para este botão? Vamos colocá-lo junto com este outro pacote. Agora aumentamos o tamanho do pacote para algo que 95% dos consumidores não usarão.
  • Isso torna a atualização difícil. Se você atualizar um componente básico, agora terá que atualizar seus consumidores, os consumidores de seu consumidor, etc. Esse problema fica pior conforme você escala.

Para tornar nossos aplicativos o mais eficientes, precisamos ter pacotes pequenos. Isso significa que devemos incluir apenas o código que estamos usando em nosso pacote.

Junto com isso, ao desenvolver bibliotecas de componentes compartilhados, queremos ter semver sobre pedaços individuais ao invés do pacote inteiro. Isso evita cenários onde:

  1. O consumidor A só precisa do pacote para um componente e está ligado a v1.
  2. O consumidor B usa o pacote para todos os componentes. Eles ajudam a criar e modificar outros componentes no pacote e ele cresceu muito. Agora está ligado a v8.
  3. O consumidor A agora precisa de uma correção de bug para o componente que usa. Eles precisam atualizar para v8.

Lerna

Lerna e Yarn Workspaces nos dão a capacidade de construir bibliotecas e aplicativos em um único repo (também conhecido como Monorepo ) sem nos forçar a publicar no NPM até que estejamos prontos. Isso torna mais rápido a iteração local ao construir componentes que dependem uns dos outros.

Lerna também fornece comandos de alto nível para otimizar o gerenciamento de vários pacotes. Por exemplo, com um comando Lerna, você pode iterar por todos os pacotes, executando uma série de operações (como linting, teste e build) em cada pacote.

Vários grandes projetos de JavaScript usam monorepos, incluindo: Babel , React , Jest , Vue , Angular e mais.

Monorepo

Neste guia, iremos utilizar:

  • 🐉 Lerna - gerente do Monorepo
  • 📦 Yarn Workspaces - gerenciamento lógico de vários pacotes
  • 🚀 React - biblioteca JavaScript para interfaces de usuário
  • 💅 styled-components - CSS em JS com elegância
  • 🛠 Babel - Compila JavaScript de última geração
  • 📖 Storybook - Ambiente de componentes de UI
  • 🃏 Jest - Teste de Unidade / Snapshot

Você pode acompanhar ou visualizar o repositório final aqui .

Ok, vamos começar! Primeiro, vamos criar um novo projeto e configurar o Lerna.

$ mkdir monorepo
$ cd monorepo
$ npx lerna init

Isso cria um arquivo package.json para seu projeto.

// Em ./package.json
{
  "name": "root",
  "private": true,
  "devDependencies": {
    "lerna": "^3.22.1"
  }
}

Você notará que um arquivo lerna.json também foi criado, bem como uma pasta /packages que conterá nossas bibliotecas. Vamos agora modificar nosso arquivo lerna.json para usar o Yarn Workspaces. Usaremos controle de versão independente para que possamos aplicar semver corretamente para cada pacote.

// Em ./lerna.json
{
  "packages": ["packages/*"],
  "npmClient": "yarn",
  "useWorkspaces": true,
  "version": "independent"
}

Também precisaremos modificar nosso package.json para definir onde Yarn Workspaces estão localizados.

// Em ./package.json
{
  "name": "root",
  "private": true,
  "workspaces": ["packages/*"],
  "devDependencies": {
    "lerna": "^3.22.1"
  }
}

Babel

A seguir, vamos adicionar todas as dependências de que precisaremos para o Babel 7.

$ yarn add --dev -W @babel/cli @babel/core @babel/preset-react @babel/preset-env babel-core@7.0.0-bridge.0 babel-loader babel-plugin-styled-components webpack

Usando -W instrui Yarn a instalar as dependências fornecidas para todo o espaço de trabalho. Essas dependências geralmente são compartilhadas entre todos os pacotes.

Após yarn ser executado, você terá uma pasta /node_modules. Não queremos adicionar ao Git nenhum desses pacotes, então vamos adicionar um .gitignore.

# Em ./.gitignore
.log
.DS_Store
.jest-*
lib
node_modules

Ok, de volta ao Babel. Para definir a configuração global do Babel, precisaremos de um arquivo babel.config.js na raiz do repositório.

// Em ./babel.config.js
module.exports = {
  plugins: ['babel-plugin-styled-components'],
  presets: ['@babel/preset-env', '@babel/preset-react']
};

Este arquivo diz ao Babel como compilar nossos pacotes. Agora, vamos criar um script para executar o Babel. Vamos adicionar isso ao nosso package.json.

package.json

// Em ./package.json
"scripts": {
    "build": "lerna exec --parallel -- babel --root-mode upward src -d lib --ignore **/*.stories.js,**/*.spec.js"
}

Vamos dissecar este comando. lerna exec irá pegar qualquer comando e executá-lo em todos os pacotes diferentes. Este comando instrui o Babel a executar em paralelo sobre cada pacote, puxando da pasta /src e compilando na pasta /lib. Não queremos incluir quaisquer testes ou histórias (que veremos mais tarde) no resultado compilado.

Usar --root-mode upward é o molho especial para usar com Yarn Workspaces. Isso diz ao Babel que node_modulesestão localizados na raiz ao invés de aninhados dentro de cada um dos pacotes individuais. Isso evita que cada pacote tenha o mesmo node_modules e os extrai até a raiz. Mais a frente, estaremos utilizando uma abordagem semelhante para testes.

React

Concluímos a infraestrutura para um Monorepo. Vamos criar alguns pacotes para ele consumir. Usaremos React e styled-components para desenvolver nossos componentes de UI, então vamos instalá-los primeiro.

$ yarn add --dev -W react react-dom styled-components

Então, dentro de /packages vamos criar uma pasta chamada /button e configurar nosso primeiro pacote.

// Em ./packages/button/package.json
{
  "name": "button",
  "version": "1.0.0",
  "main": "lib/index.js",
  "module": "src/index.js",
  "dependencies": {
    "react": "latest",
    "react-dom": "latest",
    "styled-components": "latest"
  },
  "peerDependencies": {
    "react": "^16.0.0",
    "react-dom": "^16.0.0",
    "styled-components": "^5.0.0"
  }
}

Este arquivo informa aos consumidores que module ficará dentro da pasta /src e que o resultado executado pelo Babel ( main) ficará dentro de /lib. Este será o principal ponto de entrada do pacote. Listar o peerDependencies ajuda a garantir que os consumidores estejam incluindo os pacotes corretos.

Também queremos vincular nossas dependências raiz ao nosso pacote recém-criado. Vamos criar um script para fazer isso dentro do nosso package.json.

// Em ./package.json
"scripts": {
  "bootstrap": "lerna bootstrap --use-workspaces"
}

Agora podemos simplesmente executar yarn bootstrap para instalar e vincular todas as dependências.

Ok, agora vamos criar o nosso primeiro componente: <Button />.

// Em ./packages/button/src/Button.js

import styled from 'styled-components';

const Button = styled.button`
  background: red;
  color: #fff;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
  font-weight: 300;
  padding: 9px 36px;
`;

export default Button;

Vamos testar se o Babel está configurado corretamente. Devemos ser capazes de executar yarn build e ver uma pasta /lib criada para nosso novo pacote.

$ lerna exec --parallel -- babel --root-mode upward src -d lib --ignore **/*.stories.js,**/*.spec.js
lerna notice cli v3.20.2
lerna info versioning independent
lerna info Executing command in 1 package: "babel --root-mode upward src -d lib --ignore **/*.stories.js,**/*.spec.js"
button: Successfully compiled 1 file with Babel.
lerna success exec Executed command in 1 package: "babel --root-mode upward src -d lib --ignore **/*.stories.js,**/*.spec.js"
✨  Done in 2.45s.

Storybook

O Storybook nos fornece um playground de UI interativo para nossos componentes. Isso torna o desenvolvimento de components um charme. Vamos configurar o Storybook para visualizar nosso componente Button recém-criado .

$ yarn add --dev -W @storybook/react @storybook/addon-docs @storybook/preset-create-react-app

Também queremos configurar o Storybook para que ele saiba onde encontrar nossas histórias.

// Em .storybook/main.js
module.exports = {
  stories: ['../packages/**/*.stories.js'],
  addons: ['@storybook/addon-docs']
};

Então, podemos criar nossa primeira história para o botão recém-criado dentro /packages/button/src.

// Em ./packages/button/src/Button.stories.js

import React from 'react';

import Button from '.';

export default {
  component: Button,
  title: 'Design System/Button'
};

export const primary = () => <Button>{'Button'}</Button>;

Finalmente, vamos adicionar um script para iniciar o Storybook.

// Em ./package.json
"scripts": {
  "dev": "start-storybook -p 5555"
}

Então, podemos usar yarn devpara ver o nosso botão 🎉

Nosso botão

Testes

Antes de prosseguirmos, vamos configurar nosso ambiente de testes e criar um teste simples para nosso botão. Usaremos Jest para testes de unidade. Ele selecionará automaticamente todos os arquivos que terminam com .spec.js.

$ yarn add --dev -W jest jest-styled-components babel-jest react-test-renderer jest-resolve jest-haste-map

A seguir, vamos definir nossa configuração do Jest no diretório raiz.

// Em ./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
    }
  },
  testPathIgnorePatterns: ['<rootDir>/packages/(?:.+?)/lib/']
};

Você pode modificar isso como quiser. Também queremos adicionar alguns scripts ao nosso package.json.

// Em ./package.json
"scripts": {
  "coverage": "jest --coverage",
  "unit": "jest"
}

Finalmente, vamos criar nosso primeiro teste junto com nosso componente de botão. Utilizaremos o teste de snapshot, pois este é um componente puramente de apresentação.

// Em ./packages/button/src/Button.spec.js
import React from 'react';
import renderer from 'react-test-renderer';
import 'jest-styled-components';

import Button from '.';

describe('Button', () => {
  test('renders correctly', () => {
    const tree = renderer.create(<Button>{'Test'}</Button>).toJSON();
    expect(tree).toMatchSnapshot();
  });
});

Agora podemos executar nosso teste via yarn unit.

$ jest
 PASS  packages/button/src/Button.spec.js
  Button
    ✓ renders correctly (23ms)

 › 1 snapshot written.
Snapshot Summary
 › 1 snapshot written from 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 written, 1 total
Time:        1.25s
Ran all test suites.
✨  Done in 2.48s.

Múltiplos Pacotes

O principal motivo da estrutura de Monorepo é oferecer suporte a vários pacotes. Isso nos permite ter um único processo de lint, build, teste e lançamento para todos os pacotes. Vamos criar um pacote de entrada e adicionar um novo componente.

// Em ./packages/input/src/package.json
{
  "name": "input",
  "version": "1.0.0",
  "main": "lib/index.js",
  "module": "src/index.js",
  "dependencies": {
    "react": "latest",
    "react-dom": "latest",
    "styled-components": "latest"
  },
  "peerDependencies": {
    "react": "^16.0.0",
    "react-dom": "^16.0.0",
    "styled-components": "^5.0.0"
  }
}

E:

// Em ./packages/input/src/Input.js

import styled from 'styled-components';

const Input = styled.input`
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
  font-size: 16px;
  font-weight: 300;
  padding: 10px 40px 10px 10px;
  width: 150px;
`;

export default Input;

Ok, agora temos um componente Input. Vamos correr yarn bootstrap novamente para ligar nossos pacotes e criar uma nova história.

// Em ./packages/input/src/Input.stories.js

import React from 'react';

import Input from '.';

export default {
  component: Input,
  title: 'Design System/Input'
};

export const placeholder = () => <Input placeholder="user@gmail.com" />;

Nossa instância do Storybook ainda deve estar em execução via yarn dev, mas se não, execute novamente o comando. Podemos observar que nosso componente foi renderizado corretamente.

Livro de histórias

Finalmente, vamos garantir que o Babel funcione conforme o esperado para vários pacotes executando yarn build.

$ lerna exec --parallel -- babel --root-mode upward src -d lib --ignore **/*.stories.js,**/*.spec.js
lerna notice cli v3.20.2
lerna info versioning independent
lerna info Executing command in 2 packages: "babel --root-mode upward src -d lib --ignore **/*.stories.js,**/*.spec.js"
button: Successfully compiled 1 file with Babel.
input: Successfully compiled 1 file with Babel.
lerna success exec Executed command in 2 packages: "babel --root-mode upward src -d lib --ignore **/*.stories.js,**/*.spec.js"
✨  Done in 2.45s.

Ambos os pacotes foram compilados com sucesso 🎉

E os testes? Vamos criar outro teste para o componente Input.

// Em ./packages/input/src/Input.spec.js

import React from 'react';
import renderer from 'react-test-renderer';
import 'jest-styled-components';

import Input from '.';

describe('Input', () => {
  test('renders correctly', () => {
    const tree = renderer
      .create(<Input placeholder="user@gmail.com" />)
      .toJSON();
    expect(tree).toMatchSnapshot();
  });
});

Então podemos executar novamente yarn unit.

$ jest
 PASS  packages/button/src/Button.spec.js
 PASS  packages/input/src/Input.spec.js
 › 1 snapshot written.

Snapshot Summary
 › 1 snapshot written from 1 test suite.

Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   1 written, 1 passed, 2 total
Time:        1.96s, estimated 2s
Ran all test suites.
✨  Done in 3.22s.

Lançamento (Release)

Nota : Você precisa fazer o commit e push para o seu repositório antes de lançar uma nova versão. Se você ainda não fez isso, faça agora.

Vamos lançar a primeira versão de nossos pacotes. Podemos usar npx lerna changed para ver quais pacotes foram alterados. Você também pode usar npx lerna diff para ver especificamente quais linhas foram alteradas.

Nota: lerna pode ser instalado globalmente para remover a necessidade de uso npx. Você também pode adicionar scripts em seu, package.json pois lerna é uma devDependency.

$ npx lerna changed
lerna notice cli v3.20.2
lerna info versioning independent
lerna info Assuming all packages changed
button
input
lerna success found 2 packages ready to publish

Podemos ver que ele reconhece o Button e o Input. Agora, vamos simular a liberação deles com npx lerna version.

$ npx lerna version
lerna notice cli v3.20.2
lerna info versioning independent
lerna info Assuming all packages changed
? Select a new version for button (currently 1.0.0)
? Select a new version for button (currently 1.0.0)

Changes:
 -  button: 1.0.0 => 1.0.1
 -  input: 1.0.0 => 1.0.1

? Are you sure you want to create these releases? Yes
lerna info execute Skipping GitHub releases
lerna info git Pushing tags...
lerna success version finished

Parabéns! 🎉 As tags foram enviadas ao GitHub. Para lançar nossos pacotes para o npm, você pode usar o npx lerna publish.

Yarn Workspaces

Linting & Formatting

Deve fácil para os outros contribuírem. É por isso que toda a formatação e linting são tratados com:

Graças aos ganchos pré-commit do git com husky , temos a garantia de que todo o código está formatado corretamente antes de ser enviado ao GitHub. O lint e a formatação adequadas economizaram tempo e dores de cabeça ao revisar as pull-requests. Recomendamos que você desenvolva e chegue a um acordo sobre regras de linting e formatação para reduzir a quantidade de comentários de revisão de código desse tipo. Você pode ver nossas regras aqui .

Conclusão

Parabéns, agora você tem um monorepo completo! Se você quer ir ainda mais longe, aqui estão algumas outras idéias:

Créditos

Top comments (0)