Como trabalhar com monorepos usando o Lerna
Monorepo (mono repository), como o próprio nome indica, consiste em um único repositório que contém o código de vários projetos. Esta estratégia tem sido adotada com relativo sucesso por várias empresas, como Google, Facebook e Microsoft. A própria RD Station mantém alguns monorepos em sua base de código.
Dentre as vantagens mais comuns do monorepo estão a gestão simplificada de dependências, a possibilidade de executar mudanças atômicas em vários projetos ao mesmo tempo e praticidade no compartilhamento de código.
Apesar de várias empresas adotarem essa estratégia, gerenciar monorepos não é algo simples e dificilmente é feito sem o apoio de alguma ferramenta especializada.
Para monorepos dentro do ecossistema Node, o Lerna é uma excelente ferramenta para fazer este tipo de gestão.
Lerna é uma ferramenta de código aberto que otimiza o fluxo de trabalho em torno do gerenciamento de vários pacotes com git e npm, facilitando a criação e execução de rotinas para os diferentes repositórios. Ele também pode reduzir o espaço necessário para cópias de pacotes em ambientes de desenvolvimento e produção.
Assim como muitas empresas têm adotado monorepos, muitos projetos têm adotado o Lerna como ferramenta de gerenciamento dos seus monorepos em Node, aqui se destacam o Babel, React, Angular, Ember, Meteor e o Jest.
Neste artigo teremos um panorama desta fantástica ferramenta, passando por suas principais funcionalidades e conceitos.
Inicializando um monorepo
Crie um repositório e inicialize o Lerna nele:
Este comando irá:
- Inicializar o package.json na raiz do repositório;
- Adicionar o lerna como dependência de desenvolvimento;
- Cria um arquivo de configuração lerna.json e;
- Cria a pasta packages para os pacotes do monorepo.
Por padrão os pacotes gerenciados pelo Lerna, quando atualizados, sempre terão a mesma versão, de modo que mudanças em qualquer um dos pacotes fará com que todos os demais mudem juntos para uma mesma nova versão.
Adicionar o parâmetro independent ao comando lerna init indicará o oposto, isto é, que a versão dos pacotes evoluirá de maneira independente, ou seja, o monorepo poderá ter pacotes com versões diferentes dependendo de como forem sendo atualizados durante o ciclo de desenvolvimento.
Por padrão o Lerna utilizará o npm como gestor de pacotes, contudo, é possível mudar para o yarn através da entrada npmClient no arquivo de configuração lerna.json:
Criando pacotes
Para adicionar um novo pacote basta executar o seguinte comando na raiz do repositório:
Uma nova pasta com o nome indicado para o pacote será criada dentro de packages. Este novo pacote nascerá com o package.json pré configurado bem como uma estrutura interna sugerida pelo Lerna que pode ser modificada livremente para atender outros modos de organização.
Após a criação do pacote é importante concluir a configuração do package.json preenchendo as propriedades adequadamente.
Quando for conveniente criar um pacote que não deva ser publicado, adicione o parâmetro private ao comando:
Este tipo de pacote geralmente é adequado para aplicações que irão rodar no servidor, como é o caso daquelas criadas pelo Create React App, Express e Storybook.
Apesar dos pacotes serem criados dentro de packages por padrão, caso uma estrutura alternativa seja necessária o Lerna pode ser configurado para identificar outros diretórios como contêineres de pacotes.
É possível incluir novos caminhos de contêineres alterando a propriedade packages no arquivo de configuração lerna.json:
Para criar pacotes em um contêiner diferente basta incluir um novo argumento após o nome do pacote com o caminho da pasta:
Gerenciando dependências internas
Para incluir dependências entre pacotes internos, isto é, aqueles criados através do comando lerna create (como visto na seção anterior), basta utilizar o comando lerna add.
Suponha que os pacotes package-b e package-c são dependências do package-a, sendo todos internos ao monorepo, bastaria rodar os seguintes comandos na raiz do repositório para se obter esse tipo de relação desejada:
O comando lerna add só permite a inclusão de um pacote por vez (diferente do que pode ser feito com o npm install e yarn add, por exemplo).
Ao fim do processo os pacotes serão adequadamente referenciados no package.json do package-a e um link simbólico dentro do node_modules será criado para o package-b e package-c. Com um link simbólico o Lerna permite que eventuais mudanças nestes pacotes sejam refletidas imediatamente nos pacotes dependentes sem a necessidade de publicação, fora o ganho de espaço.
Gerenciando dependências externas
O mesmo comando lerna add pode ser utilizado para adicionar pacotes externos hospedados no npm ou em qualquer outro servidor de pacotes devidamente configurado no seu ambiente:
Para instalar dependências numa versão específica adicione o número da versão após o nome do pacote:
Como o Lerna consegue analisar todo o monorepo ao invés de apenas cada pacote individualmente, não é aconselhado instalar pacotes através do npm install (ou yarn add), já que esses comandos não irão prover o mesmo tipo de otimização proporcionado pelo lerna add.
Trabalhando com workspaces
Uma configuração extremamente comum é a adoção do Lerna em conjunto com os workspaces para poupar espaço de disco através do compartilhamento de pacotes externos comuns.
Os workspaces se popularizaram primeiro com o yarn, mas também está disponível no npm a partir da versão 7.
Com o uso dos workspaces dependências de mesma versão que são utilizados em diferentes pacotes serão instalados apenas uma vez, ao invés de serem instalados em cada node_modules dos pacotes dos quais são dependências.
A configuração dos workspaces deve ser feita no package.json global, incluindo uma entrada workspaces contendo uma lista com os caminhos dos pacotes do monorepo, como no exemplo abaixo:
Também é preciso incluir a seguinte entrada no arquivo lerna.json:
Caso esta configuração tenha sido feita depois do comando lerna init já ter sido rodado, é preciso rodar os comandos lerna clean e lerna bootstrap em sequência para que as configurações do monorepo passem a considerar os workspaces.
Executando scripts globais
O Lerna permite que se rode um script específico para cada pacote do monorepo. Para isto basta que se execute o seguinte comando na raiz do repositório:
Pacotes que não possuem o script indicado serão ignorados. Também é possível ignorar explicitamente um pacote através do parâmetro ignore, como no exemplo:
Ou até mesmo rodar um script especificamente para um conjunto de pacotes, através do parâmetro scope:
Os scripts serão executados em ordem, um após o outro, contudo, é possível indicar que rodem paralelamente, através do parâmetro parallel:
É conveniente abstrair a execução do lerna run para cenários comuns, inserindo entradas para o atributo script no package.json global:
É mais simples se lembrar de comandos como npm run test e npm run watch do que seus equivalentes utilizando o Lerna.
Tratando a publicação de pacotes
O processo de publicação de pacotes é geralmente uma das etapas mais complexas dentro de um projeto. Isto ocorre porque atualmente o código de desenvolvimento dificilmente é o mesmo que será enviado para os serviços de gerenciamento de pacotes. Um projeto escrito em TypeScript, por exemplo, precisa antes ser compilado, e mesmo um projeto em JavaScript puro geralmente precisa ser transpilado para um versão mais antiga da linguagem para dar suporte a um número maior de ambientes e navegadores. Em alguns cenários há também a necessidade de garantir suporte a diferentes infraestruturas de módulos como UDM, CJS e ESM.
Todo esse esforço é feito com a ajuda de um conjunto de scripts que utilizam ferramentas como Babel, Rollup e Webpack, e o Lerna pode ser extremamente útil na coordenação destes scripts para tornar todo processo mais simples e automatizado.
O comando lerna publish, rodado na raiz do repositório, disponibiliza uma extensa rotina para o processo de publicação com mais de 30 passos possíveis e 20 pontos de customização através de scripts de ciclo de vida.
Com este comando é possível configurar e executar mecanismos para identificação de novas versões, geração build, criação de tags, envio das mudanças para o repositório remoto, geração automática de arquivos de releases entre várias outras tarefas úteis. Junto com outras ferramentas de integração contínua é possível publicar, além das versões definitivas, versões beta ou canary baseado na branch em que as mudanças ocorreram.
Toda essa flexibilidade permite um enorme número de possibilidades de automação, contudo, também exige um esforço de configuração significativo dependendo do grau de sofisticação do monorepo.
Conclusão
Monorepos possuem grandes vantagens a depender do tipo de projeto que se está trabalhando, contudo, também impõe uma série de desafios técnicos. É extremamente satisfatório poder contar no ecossistema Node com a existência de uma ferramenta tão completa como o Lerna.
A RD Station adota o Lerna em vários de seus projetos e tem colhido excelentes resultados, tanto em eficiência de gestão destes monorepos quanto em experiência de desenvolvimento.
Se você estiver procurando um ambiente formidável, onde terá contato com uma grande variedade de tecnologias modernas para se desenvolver, incluindo o Lerna, consulte as vagas abertas pela RD Station, são várias oportunidades disponíveis, cada uma com um desafio diferente!