Monorepo Com Javascript- Vários projetos em um repositório
Contextualizando
Monorepo é uma estratégia de desenvolvimento que consiste em por todos os projetos do software dentro de um repositório. Estes projetos podem ser frontend, backend, mobile, etc. Isso pode gerar muitos pontos a favor e alguns contra, durante o post vou falando sobre eles.
Nos exemplos do post eu uso o código do next.js, que é o framework mais utilizado de React, já escrevi um post falando sobre ele se quiserem dar uma olhada:
Yarn workspace
É a melhor maneira de lidar com monorepos, o que isso faz é basicamente linkar a dependências de todos os projetos (veremos mais sobre dependências daqui a pouco).
Antes precisávamos dar
yarn link
Mas o workspace faz isso sozinho, assim tirando a necessidade de instalar 2 ou mais vezes a mesma coisa.
Começando um Monorepo
No arquivos package.json precisamos definir o workspace, que é um array dentro vai ter aonde estão o projetos que estão contidos no monorepo
Esses projetos vão ter as dependências linkadas
Vamos ver o exemplo do repositório do Next.js:
{ "name": "nextjs-project", "private": true, // Private deve ser true "workspaces": [ "packages/*" ],
...
...
}
E a estrutura de pasta está assim:
Obs: A pasta package é um padrão da comunidade, mas você pode nomear do jeito que preferir e fizer mais sentido pra você
Começando projetos
A idéia de começar um projeto em um monorepo é a mesma que fora, criamos uma pasta dentro do packages (no meu caso, se escolheu outro nome vai ser dentro dessa pasta ai), damos:
yarn init -y
Lá dentro e depois começamos o projeto normalmente, instalando dependências, criando pastas, etc.
Algo que é comum vermos é o pessoal no package.json adicionando
"name" : "@pai/nome"
Onde pai é o diretório pai daquele, normalmente o raiz ou o core dentro do package, no caso do next, a maioria dos projetos tem no package.json:
{
"name" : "@next/nome-projeto",
....
}
Dependências com Monorepo
Todo projeto hoje em dia usa dependências, elas podem ser instaladas com yarn ou npm, como estamos utilizando yarn os exemplos vão ser em yarn;
Após criar esta estrutura do packages e colocar isso no workspace do package.json, quando instalarmos uma dependência no nosso projeto ele irá colocá-la no node_modules global;
Se você já baixou um monorepo, provavelmente viu alguns node_modules dentro do projeto em packages, isso acontece quando a versão da dependência instalada não bate com a recém adicionada, assim ele guarda no escopo local e tem inteligência de sempre procurar ali antes do global.
Dependência compartilhadas é uma grande vantages do monorepo, porque tira a tamanho da pasta node_modules diminui consideravelmente e a consistência é maior;
Dependências globais
Se quisermos instalar alguma dependência na raiz do projeto teremos um erro, isso acontece porque ele não aceita quando instalar alguma dependência na raiz.
Um exemplo de dependência que podemos querer adicionar é o typescript, faz sentido adicionarmos global, então fazemos:
yarn add typescript -DW
O -D para dizer que é uma dev dependencie e o W para dizer:
“Tenho certeza que quero instalar no workspace”
Obs: Só é recomendado adicionarmos algo global quando for ser usado em pelo menos 90% do projeto
Testes
Para testes vou estar utilizando o jest assim como o next.js usa. Para isso precisamos criar um arquivo
jest.config.js
Na raiz do nosso projeto, nele vai ter a configuração global, isto é:
- Aonde estão os testes
- Quais arquivos devem ser testados
- Configurações adicionais…
O next usa assim:
Eles dizem basicamente que dentro de qualquer pasta tem que pegar arquivos que terminam com:
.test.js
As outras configurações ajudam em outros aspectos, mas o principal é isso;
Caso quisessemos também poderíamos colocar uma linha
...
"projects" : [
"<rootDir>/packages/**/jest.config.js
],
...
Isso fará com que ele pegue todos os arquivos jest.config.js dentro das pastas de package e na configuração desses jest.config podemos colocar displayName por exemplo, o que nos facilitará a descobrir qual o projeto está sendo testado.
Obs: Isso que acabei de falar não é algo essencial, porém pode ser útil para ajudar na organização dos testes
Lerna
Quando falamos de monorepo o Lerna é algo que já vem na mente de muitos, esta tecnologia é usada por projetos como:
- React
- Babel
- Next.js
- Angular
- Entre outros
O lerna pode nos ajudar a migrar projetos que não eram um monorepo para um monorepo, mantendo seu histórico de commits, como se ele já tivesse sido criado como monorepo.
O próprio site se classifica como:
“Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm.”
Para começar a usar o Lerna, primeiro instalamos:
npm i -g lerna
Recomendo também instalar no projeto em questão, com:
yarn add lerna (-W) // -W se já tiver um monorepo, se não tira
O Lerna le um arquivo lerna.json na raiz do projeto, mas ele pode configurar tudo isso sozinho, basta rodar:
npx lerna init
Ele deve criar algo assim:
Agora toda vez que quisermos adicionar um dependência global podemos fazer:
npx lerna add <dependência> (--dev) // --dev para desenvolvimento
Isso fará o Lerna instalar no package.json de cada um dos projetos que está especificado no diretório do lerna.json.
Obs: Lembrando que instalar em todos os package.json não significa instalar várias vezes, mas sim deixar uma versão padrão para todos, a dependência vai ser instalar no node_modules global 1 só vez
Criar novos pacotes
O lerna pode automatizar a criação de novos pacotes, basta rodarmos:
npx lerna create <nome do pacote> -y
Isso fará que ele crie essa dentro de packages essa estrutura:
Lerna bootstrap
Esse foi um comando que me causou um pouco de confusão durante meu processo de aprendizagem, porém ele é muito simples;
Ele apenas instala as dependências de todos os projetos e linka elas, porém o Lerna pode ser utilizado juntamente com o yarn workspace, se esse for o caso, um simples
yarn
Resolve todo o nosso problema.
Obs: Caso esteja utilizando npm ou yarn sem o workspace tem que dar o bootstrap para instalar tudo certinho e linkar tudo
Configurando Lerna com workspace
Precisamos apenas adicionar algumas configs a mais no json do lerna, são elas:
Publicar no npm
Podemos automatizar o processo de publicação no npm, assim como o next.js faz, vamos ver o lerna.json deles:
O que automatiza é o “publish”, nele o pessoal do next diz que quer que o cliente seja npm e habilita as branchs para que quando o commit for ali ele vá pro npm automático
Lerna exec
Quando fazemos
lerna exec — <comando>
Podemos rodar em todos os projetos dentro do monorepo;
Obs: O <comando> pode ser qualquer comando, não precisa ter a ver com lerna, ele é apenas o comando que vai rodar dentro de todos
Exemplo:
lerna exec — yarn build
Nohoist
Quando estava estudando seu comportamento suou um pouco esquisito, porém basicamente o que ele faz é dar um unlink, ou seja, desassociar as dependências de um projeto, assim previnindo a incompatibilidade.
Tudo que tiver em “nohoist” no package.json não vai ser linkada, exemplo:
“workspaces”: {
“packages”: [“packages/*”],
“nohoist”: [“**/react-native”, “**/react-native/**”]
}
Todos os lugares que tiverem a dependência do react-native vão manter a versão para si, não linkando;
Vamos ver isso acontecendo. Vou por o seguinte código no meu package.json:
Agora vou instalar a dependência do react em 2 dos meus projetos e vou deixar 1 sem;
obs: Instalei na pasta “frotend” e “server”
Esse foi o resultado:
A página “outra” só preicsou armazenar um bin, por isso tem o node_modules;
Mas resumindo, todas as dependências foram linkadas, menos o react, que está próprio dentro da pastas “frotend” e “server”;
Espero que tenha dado pra entender 😃
Agora basta rodar yarn que ele instala e linka tudo
Publicar no npm
Podemos automatizar o processo de publicação no npm, assim como o next.js faz, vamos ver o lerna.json deles:
O que automatiza é o “publish”, nele o pessoal do next diz que quer que o cliente seja npm e habilita as branchs para que quando o commit for ali ele vá pro npm automático
Lerna exec
Quando fazemos
lerna exec -- <comando>
Podemos rodar em todos os projetos dentro do monorepo;
Obs: O <comando> pode ser qualquer comando, não precisa ter a ver com lerna, ele é apenas o comando que vai rodar dentro de todos
Exemplo:
lerna exec -- yarn build
Nohoist
Quando estava estudando seu comportamento suou um pouco esquisito, porém basicamente o que ele faz é dar um unlink, ou seja, desassociar as dependências de um projeto, assim previnindo a incompatibilidade.
Tudo que tiver em “nohoist” no package.json não vai ser linkada, exemplo:
"workspaces": {
"packages": ["packages/*"],
"nohoist": ["**/react-native", "**/react-native/**"]
}
Todos os lugares que tiverem a dependência do react-native vão manter a versão para si, não linkando;
Vamos ver isso acontecendo. Vou por o seguinte código no meu package.json:
Agora vou instalar a dependência do react em 2 dos meus projetos e vou deixar 1 sem;
obs: Instalei na pasta “frotend” e “server”
Esse foi o resultado:
A página “outra” só preicsou armazenar um bin, por isso tem o node_modules;
Mas resumindo, todas as dependências foram linkadas, menos o react, que está próprio dentro da pastas “frotend” e “server”;
Espero que tenha dado pra entender 😃
Pontos que passaram batido, mas são importantes
O nome do package.json dos nossos projetos dentro do monorepo são importantes, porque com eles podemos ter uma hierarquia melhor definida. Por exemplo, tenho um projeto que é o core, e vários projeto que são filhos, porém divido todos em projetos diferentes.
Calma, sei que ficou confuso, mas vamos olhar o next.js:
Eu tenho vários projetos, mas o core é a pasta “next”, então todos os projetos que forem filho de next vão ter o name do package json assim:
"name: "@next/<nome do projeto>",
...
Isso trás uma hierarquia interessante.
Outro ponto legal é que nós não precisamos entrar na pasta de um projeto pra rodar comandos lá dentro, como estamos utilizando o workspace podemos fazer:
yarn workspace <nome do projeto> <comando>
Exemplo:
yarn workspace @next/react-dev-overlay add axios
O yarn entrará lá dentro, dará um add no axios e pronto.
Instalar um projeto dentro de outro
Isso é muito legal, ainda mais se mexemos com a hierarquia comentada antes, a organização fica show de bola.
Se quisessemos adicionar o projeto “next-env” dentro de “next”, o que faríamos?
Simples, apenas colocamos no JSON do “next”:
"dependencies" : {
...,
"@next/next-env" : "*"
}
Isso fará ele instalar qualquer versão.
Depois para usar basta fazer:
import { Algo } from "@next/next-env"
Isto é bem legal projetos externos, que vão utilizar o monorepo, porque conseguimos tirar a responsabilidade de um lugar só, fazendo que o usuário instale apenas o necessário.