paint-brush
Monorepositorio en TypeScript: la historia de cómo rompimos todo y lo mejoramospor@devfamily
1,788 lecturas
1,788 lecturas

Monorepositorio en TypeScript: la historia de cómo rompimos todo y lo mejoramos

por dev.family13m2023/05/05
Read on Terminal Reader

Demasiado Largo; Para Leer

dev.family ha estado trabajando en un proyecto interesante durante casi seis meses y aún continúa. Comenzamos el proyecto como un programa de criptofidelidad que brinda a los usuarios finales recompensas por ciertas acciones, y los clientes reciben análisis sobre estos mismos usuarios. Todo está escrito en un idioma: TypeScript. Tenemos 8 repositorios, donde algunos deben comunicarse entre sí.
featured image - Monorepositorio en TypeScript: la historia de cómo rompimos todo y lo mejoramos
dev.family HackerNoon profile picture
0-item
1-item
2-item

Hola a todos, dev.family está en contacto. Nos gustaría hablarles de un proyecto interesante en el que hemos estado trabajando durante casi seis meses y aún continuamos. Durante este tiempo han pasado muchas cosas en él, han cambiado muchas cosas. Descubrimos algo interesante para nosotros mismos, logramos llenar los baches.


¿Cómo será nuestra historia?

  • Qué hicimos
  • como empezamos
  • ¿A dónde nos llevó esto?
  • ¿Qué problemas enfrentamos?
  • ¿Por qué monorepo?
  • ¿Por qué PNPM?
  • Por qué TS Cómo funciona ahora
  • Cuánto nos hemos hecho la vida más fácil

Un poco sobre el proyecto

Entonces, ¿en qué estamos trabajando todavía? De hecho, esta pregunta en algún momento se volvió muy relevante, como lo fue, por ejemplo, para el dueño de la corporación McDonalds en un momento dado. Comenzamos el proyecto como un programa de criptofidelidad que brinda a los usuarios finales recompensas por ciertas acciones, y los clientes reciben análisis sobre estos mismos usuarios. Sí, es bastante superficial, pero no importa.


Comienzo del trabajo

Era necesario desarrollar módulos de Shopify para conectarse a las tiendas Shopify, un portal para marcas, una extensión para Google Chrome, una aplicación móvil + un servidor con una base de datos (bueno, en realidad, sin ellos no hay nada). En general, con lo que necesitamos, decidimos y comenzamos a trabajar. Dado que el proyecto se asumió de inmediato como grande, todos entendieron que podía crecer como frijoles mágicos de acción retardada.



Se decidió hacer todo "correctamente" y "según todos los estándares". Es decir, todo está escrito en un idioma: TypeScript. Para que todos escriban de la misma manera, y no haya cambios innecesarios en los archivos, linters (muchos linters), para que todo sea “fácil” de reutilizar, poner TODO en módulos separados, y para que no roben bajo el token de acceso de Github.

Así que empezamos:

  • Repositorio para linters y ts config por separado (guía de estilo)

  • Repositorio para una aplicación móvil (react native) y una extensión de Chrome (react.js) (juntos, ya que repiten la misma funcionalidad, solo que dirigidos a diferentes usuarios)

  • Otro repositorio para el portal.

  • Dos repositorios para los módulos de Shopify

  • Repositorio para cosas de blockchain Repositorio API (express.js) Repositorio para infraestructura


Un ejemplo de nuestros repositorios en ese momento


Eh... creo que enumeré todo. Resultó un poco demasiado, pero está bien, sigamos rodando. Ah, sí, ¿por qué se asignaron dos repositorios para los módulos de Shopify? Porque el primer repositorio son módulos de interfaz de usuario. Ahí está toda la belleza de nuestros bebés y sus ambientes. Y el segundo es integraciones-Shopify. De hecho, esta es su implementación en Shopify con todos los archivos líquidos. En total, tenemos 8 repositorios, donde algunos deben comunicarse entre sí.


Como estamos hablando de desarrollo en TypeScript, también necesitamos administradores de paquetes para instalar módulos, bibliotecas. Pero todos trabajábamos de forma independiente en nuestros repositorios, ya nadie le importaba qué usar. Por ejemplo, mientras desarrollaba una aplicación móvil en React Native, no pensé demasiado y me quedé con YARN1. Alguien puede estar más acostumbrado a usar el viejo NPM, mientras que a otros les encanta todo lo nuevo y usan el nuevo YARN3. Por lo tanto, en algún lugar había NPM, en algún lugar YARN1 y en algún lugar YARN3.


Así que todos empezamos a hacer nuestras aplicaciones. Y casi de inmediato comenzó la diversión, pero no tan completa. En primer lugar, algunos no pensaron para qué era TypeScript, y usaron "Cualquiera" donde estaban demasiado flojos, o donde "no entendían" cómo no podían escribirlo. Alguien no se dio cuenta de todo el poder de TypeScript y del hecho de que en algunos lugares todo se puede hacer mucho más fácil. Por lo tanto, los tipos salieron de dimensiones cósmicas. Sí, se me olvidó decir que decidimos usar Hasura GraphQL como base de datos. La tipificación manual de todas las respuestas a veces parecía otra cosa. Y en un caso, algunos incluso escribieron en Javascript. Sí, la situación resultó ser genial: el primer chico puso "Cualquiera" una vez más para no esforzarse demasiado, el segundo escribe lienzos de tipos con sus propias manos y el tercero todavía no escribe tipos en absoluto.



Más tarde resulta que en los casos en que repetimos la lógica y, en el buen sentido, debería haberse sacado en un paquete separado, nadie iba a hacer esto. Todos escriben y escriben código para sí mismos, para todo lo demás: escupir desde un alto campanario.

¿Adónde nos llevó esto?

¿Que tenemos? Tenemos 8 repositorios con diferentes aplicaciones. Algunos se necesitan en todas partes, otros se comunican entre sí. Por lo tanto, todos creamos archivos .NPMrc, prescribimos créditos, creamos un token de github y luego a través del módulo del administrador de paquetes. En general una molestia leve, aunque desagradable, pero nada inusual.


Solo en el caso de actualizar algo en el paquete, debe actualizar su versión, luego cargarlo, luego actualizarlo en su aplicación/módulo, y solo entonces verá qué ha cambiado. ¡Pero esto es totalmente inapropiado! Especialmente si puedes cambiar el color en alguna parte. Además, parte del código se repite y no se reutiliza, sino que simplemente se reescribe silenciosamente. Si estamos hablando de una aplicación móvil y una extensión del navegador, la tienda redux y todo el trabajo con la API se repiten por completo allí, algo se reescribe por completo o se modifica ligeramente.


En total, lo que nos queda: un montón de repositorios, un lanzamiento bastante largo de aplicaciones/módulos, muchas de las mismas cosas escritas por las mismas personas, mucho tiempo dedicado a probar e introducir nuevas personas en el proyecto, y otros problemas derivados de lo anterior.



En resumen, esto nos llevó al hecho de que las tareas se realizaron durante mucho tiempo. Por supuesto, esto provocó que se incumplieran los plazos, fue bastante difícil introducir a alguien nuevo en el proyecto, lo que una vez más afectó la velocidad de desarrollo. Todo iba a ser bastante aburrido y largo, en algunos casos, gracias a webpack por eso.


Entonces quedó claro que nos estábamos alejando de donde nos esforzábamos, pero quién sabe dónde. Después de analizar todos los errores, tomamos una serie de decisiones, que se discutirán ahora.

¿Por qué monorepo?

Probablemente, lo más importante que influyó mucho en el futuro fue darnos cuenta de que no estamos construyendo una aplicación específica, sino una plataforma. Tenemos varios tipos de usuarios, hay diferentes aplicaciones para ellos, pero operan dentro de la misma plataforma. Así que inmediatamente cerramos el problema con una gran cantidad de repositorios: si estamos trabajando en una plataforma, ¿por qué dividirla en repositorios cuando es más fácil trabajar en uno?


Quiero decir que trabajar en un monorepo nos hizo la vida muchísimo más fácil. Algunas aplicaciones o módulos tenían una relación directa entre sí, y ahora puedes trabajar en ellos con tranquilidad en la misma rama en el mismo repositorio. Pero esto está lejos de ser la principal ventaja.


Continuemos. Hemos movido todo a un repositorio. ¡Fresco! Continuamos trabajando al mismo ritmo hasta que llegó la reutilización. De hecho, esta es una “regla de buen gusto” que tenemos en nuestro trabajo. Al darnos cuenta de que en algunos lugares usamos los mismos algoritmos, funciones, código y, en algunos lugares, paquetes separados que instalamos a través de github, decidimos que todo esto "no huele muy bien" y comenzamos a poner todo en paquetes separados dentro de un monorepo. utilizando espacios de trabajo.


Los espacios de trabajo (workspaces) son conjuntos de funciones en la cli de NPM, con las que puede administrar varios paquetes desde un solo paquete raíz de nivel superior.


De hecho, estos son paquetes dentro de un paquete que se vinculan a través de un administrador de paquetes específico (cualquier YARN / NPM / PNPM) y luego se usan en otro paquete. A decir verdad, no reescribimos inmediatamente todo en los espacios de trabajo, pero lo hicimos según fuera necesario.

Esto es lo que parece:

De un archivo


{ "type": "module", "name": "package-name-1", ... "types": "./src/index.ts", "exports": { ".": "./src/index.ts" }, },


A otro archivo


{ "type": "module", "name": "package-name-2", ... "dependencies": { "package-name-1": "workspace:*", }, },


Un ejemplo usando PNPM


Nada complicado, en realidad si lo piensas bien: escribe un par de comandos y líneas, y luego usa lo que quieras y donde quieras. Pero “hay una advertencia, camaradas”. Anteriormente escribí que todos usaban el administrador de paquetes que querían. En definitiva, tenemos un repositorio con diferentes gestores. En algunos lugares, fue divertido cuando alguien escribió que no podía vincular este o aquel paquete, teniendo en cuenta el hecho de que usa NPM, y hay YARN.

Agregaré que el problema no se debió a diferentes administradores, sino a que las personas usaron los comandos incorrectos o configuraron algo mal. Por ejemplo, algunas personas a través de YARN 3 simplemente hicieron un enlace de YARN y eso es todo, pero para YARN 1 no funcionó de la manera que querían debido a la falta de compatibilidad con versiones anteriores.

Después de cambiar a monorepo


¿Por qué PNPM?

En este punto, quedó claro que es mejor usar el mismo administrador de paquetes. Pero debe elegir cuál, por lo que en ese momento consideramos solo 2 opciones: YARN y PNPM . Descartamos NPM de inmediato, porque era más lento que otros y más feo. Había una opción entre PNPM y YARN.


Inicialmente, YARN funcionó bien: era más rápido, más simple y más comprensible, razón por la cual todos lo usaban entonces. Pero la persona que hizo YARN dejó Facebook y el desarrollo de las siguientes versiones se transfirió a otros. Así aparecieron YARN 2 y YARN 3 sin retrocompatibilidad con el primero. Además, además del archivo yarn.lock, generan una carpeta yarn, que a veces pesa como node_modules y almacena cachés en sí misma.


Por lo tanto, nosotros, como muchos otros desarrolladores, centramos nuestra atención en PNPM. Resultó ser tan conveniente como el primer YARN en su momento. Los espacios de trabajo se pueden usar fácilmente aquí, algunos comandos tienen el mismo aspecto que en el primer YARN. Además, el elevador vergonzoso resultó ser una buena opción adicional: es más conveniente instalar node_modules en todas partes a la vez que ir a alguna carpeta cada vez y hacer la instalación de PNPM.


Turborepo y reutilización de código

Además, decidimos probar turborepo. Turborepo es una herramienta de CI/CD que tiene su propio conjunto de opciones, cli y configuración a través del archivo turbo.json. Instalado y configurado lo más fácil posible. Pasamos una copia global del turbo cli


PNPM add turbo --global.


Agregar turbo.json al proyecto


turbo.json


{ "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { "dependsOn": ["^build"] } } }


Después de eso, podemos usar todas las funciones disponibles de turborepo. Lo que más nos atrajo fueron sus características y la posibilidad de usarlo en un monorepo.

Lo que nos enganchó:

  • Construcciones incrementales (construcciones incrementales: recopilar compilaciones es bastante doloroso, Turborepo recordará lo que se construyó y omitirá lo que ya se calculó);

  • Hashing con reconocimiento de contenido (Hashing con reconocimiento de contenido: Turborepo analiza el contenido de los archivos, no las marcas de tiempo, para averiguar qué se debe construir);

  • Almacenamiento en caché remoto (hashing remoto: comparta un caché de compilación remota con el equipo y CI/CD para compilaciones aún más rápidas);

  • Canalizaciones de tareas (una canalización de tareas que define las relaciones entre las tareas y luego optimiza qué y cuándo crear).

  • Ejecución en paralelo (realiza compilaciones utilizando cada núcleo con el máximo paralelismo, sin desperdiciar CPU inactivas).


También tomamos la recomendación para organizar un monorepo de la documentación y la implementamos en nuestra plataforma. Es decir, dividimos todos nuestros paquetes en aplicaciones y paquetes. Para hacer esto, también creamos el archivo PNPM-workspace.yaml y escribimos:


PNPM-espacio de trabajo.yaml

packages:

'apps/**/*'

'packages/**/*'


Aquí puedes ver un ejemplo de nuestra estructura antes y después:



Ahora tenemos un monorep con espacios de trabajo personalizados y una conveniente reutilización de código. Agregaré algunos puntos más que hicimos en paralelo. Mencioné dos cosas antes: teníamos una extensión de Chrome y decidimos que estábamos creando una plataforma.


Dado que nuestra plataforma funcionaba con Shopify como una prioridad, decidimos que en lugar de una extensión para Chrome o además de ella, sería bueno hacer otro módulo para Shopify, que puede instalarse simplemente en el sitio, para no perder ni una vez. Obligar nuevamente a las personas a descargar una aplicación móvil o una extensión de Chrome. Pero debe repetir completamente la extensión. Inicialmente, los hacíamos en paralelo, pero nos dimos cuenta de que algo estábamos haciendo mal, porque simplemente duplicamos el código. En todos los sentidos escribimos lo mismo en diferentes lugares. Pero como ahora tenemos configurados todos los espacios de trabajo y la reutilización, movimos todo fácilmente a un solo paquete, al que llamamos el módulo de Shopify y la extensión de Chrome. Así, nos ahorramos mucho tiempo.


Ahora esto e index.html son la extensión completa de Chrome



Lo segundo que nos ahorró mucho tiempo fue la eliminación del paquete web y, en algunos lugares, de las compilaciones en general. ¿Qué tiene de malo el paquete web? De hecho, hay dos puntos críticos: la complejidad y la velocidad. Lo que hemos elegido es vite. ¿Por qué? Es más fácil de configurar, está ganando popularidad rápidamente y ya tiene una gran cantidad de complementos que funcionan, y un ejemplo de los muelles es suficiente para la instalación. En comparación, la compilación en el paquete web de nuestra extensión web de Chrome tomó alrededor de 15 segundos, en vite.js



unos 7 segundos (con generación de archivos dts).



Siente la diferencia. ¿Qué pasa con el rechazo de las compilaciones? Todo es simple, como resultó, realmente no los necesitábamos, ya que estos son módulos reutilizables y en package.json, en las exportaciones, simplemente podría reemplazar dist/index.js con src/index.ts.


Cómo fue


{... "exports": { "import": "./dist/built-index.js" }, ... }


como es ahora


{ ... "types": "./src/index.ts", "exports": { ".": "./src/index.ts" }, ... }


Por lo tanto, nos deshicimos de la necesidad de ejecutar PNPM watch para rastrear las actualizaciones de aplicaciones relacionadas con esos módulos y hacer PNPM build para obtener actualizaciones. No creo que valga la pena explicar cuánto tiempo nos ahorró.

De hecho, una de las razones por las que recopilamos compilaciones fue TypeScript, más precisamente archivos index.d.ts. Para que al importar nuestros módulos/paquetes, sepamos qué tipos se esperan en ciertas funciones o qué tipos nos devolverán otras, como aquí:


Todos los parámetros esperados son inmediatamente visibles


Pero dado que simplemente puede exportar desde index.tsx, había otra razón para abandonar las compilaciones.

Mecanografiado + GraphQL

Pero aún así, ¿por qué TypeScript? Creo que ahora no tiene sentido describir todas las ventajas de TS: la seguridad de tipos, la facilitación del proceso de desarrollo debido a la escritura, la presencia de interfaces y clases, el código fuente abierto, los errores cometidos durante la modificación del código son visibles de inmediato y no en tiempo de ejecución. , etcétera.


Como dije al principio, decidimos escribir todo en un solo idioma para que si alguien deja de trabajar o se va, podamos apoyarlo o asegurarlo. Primero elegimos JS. Pero JS no es muy seguro, y sin pruebas en grandes proyectos es bastante doloroso. Por lo tanto, decidimos a favor de TS. Como ha demostrado la práctica, es muy conveniente en monorepo, debido al hecho de que simplemente puede exportar archivos *.ts, y al usar componentes, los datos esperados y sus tipos son claros de inmediato.


Pero una de las principales características útiles fue la generación automática de tipos para consultas y mutaciones de GraphQl. Para todos los que no tienen mucho conocimiento, GraphQl es una tecnología que le permite ir a la base de datos a través de la misma consulta (para obtener datos) y mutación (para cambiar datos), y se parece a esto:


query getShop {shop { shopName shopLocation } }


A diferencia de la API REST, donde hasta que no la recibas, no sabrás lo que te llegará, aquí tú mismo determinas los datos que necesitas.


Volvamos a nuestro presidente electo. Usamos Hasura, que es un contenedor de GraphQL sobre PostgreSQL. Dado que estamos trabajando con TS, en el buen sentido debemos escribir los datos de ambas solicitudes y las que enviamos a la carga útil. Si estamos hablando del código del ejemplo anterior, no debería haber problemas, más o menos. Pero en la práctica, una consulta puede llegar a las cien líneas, además algunos campos pueden venir o no, o tener distintos tipos de datos. Y escribir tales lienzos es una tarea muy larga e ingrata.


¿Alternativa? ¡Claro que tengo! Deje que los tipos se generen a través de comandos. En nuestro proyecto, hicimos lo siguiente:


  • Utilizamos las siguientes bibliotecas: graphql y graphql-request

  • Primero se crearon archivos con resolución *.graphql, en los cuales se escribieron consultas y mutaciones.


    Por ejemplo:


prueba.graphql


query getAllShops {test_shops { identifier name location owner_id url domain type owner { name owner_id } } }


  • Luego creamos codegen.yaml


codegen.yaml


schema: ${HASURA_URL}:headers: x-hasura-admin-secret: ${HASURA_SECRET}

emitLegacyCommonJSImports: false

config: gqlImport: graphql-tag#gql scalars: numeric: string uuid: string bigint: string timestamptz: string smallint: number

genera: src/infrastructure/api/graphQl/operations.ts: documentos: plugins: - TypeScript - TypeScript-operations - TypeScript-graphql-request generates: src/infrastructure/api/graphQl/operations.ts: documents: 'src/**/*.graphql' : - TypeScript - TypeScript-operations - TypeScript-graphql-request


Allí indicamos hacia dónde íbamos y, al final, dónde guardamos el archivo con la API generada (src/infrastructure/api/graphQl/operations.ts) y de dónde obtenemos nuestras solicitudes (src/**/*. gráficoql).


Después de eso, se agregó un script a package.json que generó los mismos tipos para nosotros:


paquete.json


{... "scripts": { "generate": "HASURA_URL=http://localhost:9696/v1/graphql HASURA_SECRET=secret graphql-codegen-esm --config codegen.yml", ... }, ... }


Indicaron la URL a la que accedió el script para obtener información, el secreto y el comando en sí.


  • Finalmente, creamos el cliente:


import { GraphQLClient } from "graphql-request"; import { getSdk } from "./operations.js"; export const createGraphQlClient = ({ getToken }: CreateGraphQlClient) => { const graphQLClient = new GraphQLClient('your url goes here...'); return getSdk(graphQLClient); };


Así, obtenemos una función que genera un cliente con todas las consultas y mutaciones. La bonificación en operation.ts establece todos nuestros tipos que podemos exportar y usar, y hay una tipificación completa de toda la solicitud: sabemos qué se debe dar y qué vendrá. No necesita pensar en nada más, excepto en ejecutar el comando y disfrutar de la belleza de escribir.

Conclusión

Por lo tanto, nos deshicimos de una gran cantidad de repositorios innecesarios y de la necesidad de impulsar constantemente los cambios más pequeños para verificar cómo funcionan las cosas. En cambio, idearon uno en el que todo está estructurado, descompuesto según su propósito, y todo se reutiliza fácilmente. Así que hicimos nuestra vida más fácil y redujimos el tiempo para introducir nuevas personas al proyecto, para lanzar la plataforma y los módulos/aplicaciones por separado. Todo ha sido escrito, y ahora no hay necesidad de ir a cada carpeta y ver qué quiere esta o aquella función/componente. Como resultado, el tiempo de desarrollo se ha reducido.



En conclusión, quiero decir que nunca debes tener prisa. Es mejor entender lo que estás haciendo y cómo hacerlo más fácilmente que complicarte la vida deliberadamente. Los problemas están en todas partes y siempre, tarde o temprano aparecerán en alguna parte, y luego la complicación deliberada te disparará en la rodilla, pero no ayudará de ninguna manera.

El equipo de dev.family estuvo contigo, ¡hasta pronto!