Português (Portugal)
  • typescript
  • monorepo

TypeScript tudo-em-um: Monorepo com suas dores e ganhos

Neste artigo, não vou comparar monorepo e polyrepo, pois tudo se trata de filosofia. Em vez disso, vou me concentrar na experiência de construção e evolução e supor que você está familiarizado com o ecossistema JS / TS.

Gao
Gao
Founder

Introdução

Eu sempre tive um sonho de monorepo.

Vi a abordagem do monorepo enquanto trabalhava para o Airbnb, mas era apenas para o frontend. Com um profundo amor pelo ecossistema JavaScript e a experiência de desenvolvimento "feliz" do TypeScript, comecei a alinhar o código frontend e backend na mesma linguagem cerca de ~ três anos atrás. Foi ótimo (para contratação), mas não tão bom para o desenvolvimento, já que nossos projetos ainda estavam espalhados por vários repositórios.

Como se costuma dizer, "a melhor maneira de refatorar um projeto é começar um novo". Portanto, quando comecei minha startup cerca de um ano atrás, decidi usar uma estratégia total de monorepo: colocar projetos frontend e backend, até mesmo esquemas de banco de dados, em um único repositório.

Neste artigo, não vou comparar monorepo e polyrepo, pois tudo se trata de filosofia. Em vez disso, vou me concentrar na experiência de construção e evolução e supor que você está familiarizado com o ecossistema JS / TS.

O resultado final está disponível no GitHub.

Por que TypeScript?

Falando francamente, sou fã de JavaScript e TypeScript. Amo a compatibilidade de sua flexibilidade e rigorosidade: você pode voltar para unknown ou any (embora tenhamos banido qualquer forma de any em nosso código-fonte), ou usar um conjunto de regras de lint super rigoroso para alinhar o estilo de código em toda a equipe.

Quando estávamos falando sobre o conceito de "fullstack" antes, geralmente imaginamos pelo menos dois ecossistemas e linguagens de programação: um para frontend e um para backend.

Um dia, percebi de repente que poderia ser mais simples: Node.js é rápido o suficiente (acredite em mim, na maioria dos casos, a qualidade do código é mais importante do que a velocidade de execução), TypeScript é maduro o suficiente (funciona bem em grandes projetos frontend), e o conceito de monorepo tem sido praticado por várias equipes famosas (React, Babel, etc.) - então, por que não combinamos todo o código junto, do frontend ao backend? Isso pode fazer com que os engenheiros façam os trabalhos sem trocar de contexto em um repositório e implementem um recurso completo em (quase) uma linguagem.

Escolhendo o gerenciador de pacotes

Como desenvolvedor, e como de costume, mal podia esperar para começar a codificar. Mas desta vez, as coisas eram diferentes.

A escolha do gerenciador de pacotes é fundamental para a experiência do dev em um monorepo.

A dor da inércia

Foi em Julho de 2021. Comecei com [email protected] já que o estou usando há muito tempo. O Yarn era rápido, mas logo encontrei vários problemas com os Workspaces do Yarn. Por exemplo, não elevando as dependências corretamente, e toneladas de problemas estão marcados com "fixed in modern", que me redirecionam para a versão 2 (berry).

"Está bem, estou atualizando agora." Parei de lutar com a versão 1 e comecei a migrar. Mas o longo guia de migração do berry me assustou e desisti depois de várias tentativas fracassadas.

Simplesmente funciona

Então a pesquisa sobre os gerenciadores de pacotes começou. Fui absorvido pelo pnpm depois de um teste: rápido como a lã, suporte nativo a monorepo, comandos semelhantes a npm, links duros, etc. O mais importante é que ele simplesmente funciona. Como desenvolvedor que quer começar com um produto, mas NÃO desenvolver um gerenciador de pacotes, eu só queria adicionar algumas dependências e iniciar o projeto sem saber como um gerenciador de pacotes funciona ou quaisquer outros conceitos extravagantes.

Com base na mesma ideia, escolhemos um velho amigo lerna para executar comandos em todos os pacotes e publicar pacotes de espaço de trabalho.

Definindo escopos de pacote

É difícil descobrir claramente o escopo final de cada pacote no início. Basta começar com a sua melhor tentativa de acordo com o status quo e lembre-se de que você pode sempre refatorar durante o desenvolvimento.

Nossa estrutura inicial contém quatro pacotes:

  • core: o serviço monolítico de backend.
  • phrases: i18n key → recursos de frase.
  • schemas: os bancos de dados e esquemas TypeScript compartilhados.
  • ui: uma SPA na web que interage com core.

Pilha de tecnologia para fullstack

Como estamos abraçando o ecossistema JavaScript e usando TypeScript como nossa principal linguagem de programação, muitas escolhas são diretas (baseadas na minha preferência 😊):

  • koajs para o serviço de backend (core): tive uma experiência difícil usando async / await em express, então decidi usar algo com suporte nativo.
  • i18next / react-i18next para i18n (phrases / ui): gosto de sua simplicidade de APIs e bom suporte ao TypeScript.
  • react para SPA (ui): apenas preferência pessoal.

E quanto aos esquemas?

Ainda falta algo aqui: sistema de banco de dados e esquema <-> Mapeamento da definição TypeScript.

Geral v.s. opinionado

Naquele ponto, tentei duas abordagens populares:

  • Use ORM com muitos decoradores.
  • Use um construtor de consultas como o Knex.js.

Mas ambos produzem uma sensação estranha durante o desenvolvimento anterior:

  • Para ORM: não sou fã de decoradores, e outra camada abstrata do banco de dados causa mais esforço de aprendizado e incerteza para a equipe.
  • Para construtor de consulta: é como escrever SQL com algumas restrições (de uma boa maneira), mas não é SQL real. Assim, precisamos usar .raw() para consultas cruas em muitos cenários.

Então vi este artigo: "Pare de usar Knex.js: usar o construtor de consultas SQL é um anti-padrão". O título parece agressivo, mas o conteúdo é ótimo. Ele me lembra fortemente que "SQL é uma linguagem de programação", e percebi que poderia escrever SQL diretamente (assim como CSS, como pude perder isso!) Para aproveitar a linguagem nativa e os recursos do banco de dados, em vez de adicionar outra camada e reduza o poder.

Em conclusão, decidi ficar com Postgres e Slonik (um cliente Postgres de código aberto), como afirma o artigo:

...o benefício de permitir que o usuário escolha entre os diferentes dialetos de banco de dados é marginal e o custo de desenvolver para vários bancos de dados ao mesmo tempo é significativo.

SQL <-> TypeScript

Outra vantagem de escrever SQL é que podemos facilmente usá-lo como a única fonte de verdade das definições TypeScript. Escrevi um gerador de código para transpiler esquemas SQL para código TypeScript que usaremos em nosso backend, e o resultado parece bom: