Guía de Monorepos para código front-end

Publicado: 2022-03-11

Monorepos son un tema candente para una discusión. Ha habido muchos artículos recientemente sobre por qué debería y no debería usar este tipo de arquitectura para su proyecto, pero la mayoría de ellos están sesgados de una forma u otra. Esta serie es un intento de recopilar y explicar la mayor cantidad de información posible para comprender cómo y cuándo usar monorepos.

Un Monorepositorio es un concepto arquitectónico, que básicamente contiene todo el significado en su título. En lugar de administrar múltiples repositorios, mantiene todas sus partes de código aisladas dentro de un repositorio. Tenga en cuenta la palabra aislada : significa que monorepo no tiene nada en común con las aplicaciones monolíticas. Puede mantener muchos tipos de aplicaciones lógicas dentro de un repositorio; por ejemplo, un sitio web y su aplicación para iOS.

Comparación de un monorepo, un solo repo y un multirepo

Este concepto es relativamente antiguo y apareció hace aproximadamente una década. Google fue una de las primeras empresas que adoptó este enfoque para administrar sus bases de código. Puede preguntarse, si ha existido durante una década, ¿por qué es un tema tan candente solo ahora? Sobre todo, en el transcurso de los últimos 5-6 años, muchas cosas han sufrido cambios dramáticos. ES6, preprocesadores SCSS, administradores de tareas, npm, etc. Hoy en día, para mantener una pequeña aplicación basada en React, debe lidiar con paquetes de proyectos, conjuntos de pruebas, scripts de CI/CD, configuraciones de Docker y quién sabe qué más. Y ahora imagine que en lugar de una aplicación pequeña, necesita mantener una plataforma enorme que consta de muchas áreas funcionales. Si está pensando en la arquitectura, querrá hacer dos cosas principales: separar las preocupaciones y evitar los códigos falsos.

Para que esto suceda, probablemente querrá aislar funciones grandes en algunos paquetes y luego usarlas a través de un único punto de entrada en su aplicación principal. Pero, ¿cómo gestionas esos paquetes? Cada paquete deberá tener su propia configuración de entorno de flujo de trabajo, y esto significa que cada vez que desee crear un nuevo paquete, deberá configurar un nuevo entorno, copiar todos los archivos de configuración, etc. O, por ejemplo, si tiene que cambiar algo en su sistema de compilación, tendrá que revisar cada repositorio, hacer una confirmación, crear una solicitud de extracción y esperar cada compilación, lo que lo ralentiza mucho. En este paso nos encontramos con los monorepos.

En lugar de tener muchos repositorios con sus propias configuraciones, solo tendremos una fuente de verdad: el monorepo: un ejecutor de conjunto de pruebas, un archivo de configuración de Docker y una configuración para Webpack. Y todavía tiene escalabilidad, oportunidad de separar preocupaciones, código compartido con paquetes comunes y muchas otras ventajas. Suena bien, ¿verdad? Bueno, lo es. Pero también hay algunos inconvenientes. Echemos un vistazo de cerca a los pros y los contras exactos de usar el monorepo en la naturaleza.

Ventajas de Monorepo:

  • Un lugar para almacenar todas las configuraciones y pruebas. Dado que todo se encuentra dentro de un repositorio, puede configurar su CI/CD y el paquete una vez y luego simplemente reutilizar las configuraciones para compilar todos los paquetes antes de publicarlos en forma remota. Lo mismo ocurre con las pruebas unitarias, e2e y de integración: su CI podrá iniciar todas las pruebas sin tener que lidiar con una configuración adicional.
  • Refactorice fácilmente funciones globales con confirmaciones atómicas. En lugar de hacer una solicitud de extracción para cada repositorio, averiguar en qué orden crear sus cambios, solo necesita realizar una solicitud de extracción atómica que contendrá todas las confirmaciones relacionadas con la función en la que está trabajando.
  • Publicación de paquetes simplificada. Si planea implementar una nueva función dentro de un paquete que depende de otro paquete con código compartido, puede hacerlo con un solo comando. Es una función que necesita algunas configuraciones adicionales, que se discutirán más adelante en una parte de revisión de herramientas de este artículo. Actualmente, hay una rica selección de herramientas, incluidas Lerna, Yarn Workspaces y Bazel.
  • Gestión de dependencias más sencilla. Solo un paquete.json . No es necesario volver a instalar las dependencias en cada repositorio cada vez que desee actualizar sus dependencias.
  • Reutilice el código con paquetes compartidos mientras los mantiene aislados. Monorepo le permite reutilizar sus paquetes de otros paquetes mientras los mantiene aislados unos de otros. Puede usar una referencia al paquete remoto y consumirlos a través de un único punto de entrada. Para usar la versión local, puede usar enlaces simbólicos locales. Esta función se puede implementar a través de scripts bash o mediante la introducción de algunas herramientas adicionales como Lerna o Yarn.

Desventajas de Monorepo:

  • No hay forma de restringir el acceso solo a algunas partes de la aplicación. Desafortunadamente, no puede compartir solo una parte de su monorepo; tendrá que dar acceso a todo el código base, lo que podría generar algunos problemas de seguridad.
  • Bajo rendimiento de Git cuando se trabaja en proyectos a gran escala. Este problema comienza a aparecer solo en aplicaciones enormes con más de un millón de confirmaciones y cientos de desarrolladores que hacen su trabajo simultáneamente todos los días en el mismo repositorio. Esto se vuelve especialmente problemático ya que Git usa un gráfico acíclico dirigido (DAG) para representar la historia de un proyecto. Con una gran cantidad de confirmaciones, cualquier comando que recorra el gráfico podría volverse lento a medida que se profundiza en el historial. El rendimiento también se ralentiza debido a la cantidad de referencias (es decir, ramas o etiquetas, que se pueden resolver eliminando las referencias que ya no necesita) y la cantidad de archivos rastreados (así como su peso, aunque el problema de los archivos pesados ​​se puede resolver usando Git LFS).

    Nota: hoy en día, Facebook intenta resolver problemas con la escalabilidad de VCS parcheando Mercurial y, probablemente pronto, esto no será un problema tan grande.

  • Mayor tiempo de construcción. Debido a que tendrá una gran cantidad de código fuente en un solo lugar, su CI tardará mucho más tiempo en ejecutar todo para aprobar cada PR.

Revisión de herramientas

El conjunto de herramientas para la gestión de monorepos está en constante crecimiento y, actualmente, es muy fácil perderse en toda la variedad de sistemas de construcción para monorepos. Siempre puede estar al tanto de las soluciones populares utilizando este repositorio. Pero por ahora, echemos un vistazo rápido a las herramientas que se usan mucho hoy en día con JavaScript:

  • Bazel es el sistema de compilación orientado a monorepo de Google. Más sobre Bazel: impresionante-bazel
  • Yarn es una herramienta de administración de dependencias de JavaScript que admite monorepos a través de espacios de trabajo.
  • Lerna es una herramienta para administrar proyectos de JavaScript con múltiples paquetes, construida sobre Yarn.

La mayoría de las herramientas utilizan un enfoque muy similar, pero hay algunos matices.

Ilustración del proceso de CI/CD del repositorio monorepo git

Profundizaremos en el flujo de trabajo de Lerna, así como en las otras herramientas en la Parte 2 de este artículo, ya que es un tema bastante amplio. Por ahora, veamos una descripción general de lo que hay dentro:

Lerna

Esta herramienta realmente ayuda cuando se trata de versiones semánticas, configura el flujo de trabajo de construcción, empuja sus paquetes, etc. La idea principal detrás de Lerna es que su proyecto tiene una carpeta de paquetes, que contiene todas sus partes de código aisladas. Y además de los paquetes, tiene una aplicación principal que, por ejemplo, puede vivir en la carpeta src. Casi todas las operaciones en Lerna funcionan a través de una regla simple: itera a través de todos sus paquetes y realiza algunas acciones sobre ellos, por ejemplo, aumenta la versión del paquete, actualiza la dependencia de todos los paquetes, crea todos los paquetes, etc.

Con Lerna, tiene dos opciones sobre cómo usar sus paquetes:

  1. Sin empujarlos a control remoto (NPM)
  2. Empujando sus paquetes a control remoto

Mientras usa el primer enfoque, puede usar referencias locales para sus paquetes y, básicamente, no le importan los enlaces simbólicos para resolverlos.

Pero si está utilizando el segundo enfoque, se ve obligado a importar sus paquetes de forma remota. (p. ej., import { something } from @yourcompanyname/packagename; ), lo que significa que siempre obtendrá la versión remota de su paquete. Para el desarrollo local, deberá crear enlaces simbólicos en la raíz de su carpeta para que el paquete resuelva los paquetes locales en lugar de usar los que están dentro de su node_modules/ . Por eso, antes de iniciar Webpack o su paquete favorito, deberá iniciar lerna bootstrap , que vinculará automáticamente todos los paquetes.

Una ilustración del espacio de nombres de sus módulos dentro de un paquete de un solo nodo

Hilo

Yarn inicialmente es un administrador de dependencias para paquetes NPM, que inicialmente no se creó para admitir monorepos. Pero en la versión 1.0, los desarrolladores de Yarn lanzaron una función llamada Workspaces . En el momento del lanzamiento, no era tan estable, pero después de un tiempo, se volvió utilizable para proyectos de producción.

Workspace es básicamente un paquete, que tiene su propio paquete.json y puede tener algunas reglas de compilación específicas (por ejemplo, un tsconfig.json separado si usa TypeScript en sus proyectos). De hecho, puede administrarlo de alguna manera sin Yarn Workspaces usando bash y tener exactamente la misma configuración, pero esta herramienta ayuda a facilitar el proceso de instalación y actualización de las dependencias por paquete.

De un vistazo, Yarn con sus espacios de trabajo proporciona las siguientes características útiles:

  1. Carpeta única node_modules en la raíz para todos los paquetes. Por ejemplo, si tiene packages/package_a y packages/package_b con su propio package.json , todas las dependencias se instalarán solo en la raíz. Esa es una de las diferencias entre cómo funcionan Yarn y Lerna.
  2. Enlace simbólico de dependencia para permitir el desarrollo de paquetes locales.
  3. Archivo de bloqueo único para todas las dependencias.
  4. Actualización de dependencia enfocada en caso de que desee reinstalar dependencias para un solo paquete. Esto se puede hacer usando el indicador -focus .
  5. Integración con Lerna. Puede hacer que Yarn maneje fácilmente toda la instalación/vinculación simbólica y dejar que Lerna se encargue de la publicación y el control de versiones. Esta es la configuración más popular hasta ahora, ya que requiere menos esfuerzo y es fácil trabajar con ella.

Enlaces útiles:

  • Espacios de trabajo de hilo
  • Cómo construir un proyecto mono-repo de TypeScript

bazel

Bazel es una herramienta de compilación para aplicaciones a gran escala, que puede manejar dependencias de varios idiomas y admite una gran cantidad de lenguajes modernos (Java, JS, Go, C++, etc.). En la mayoría de los casos, usar Bazel para aplicaciones JS pequeñas y medianas es excesivo, pero a gran escala puede proporcionar muchos beneficios debido a su rendimiento.

Por su naturaleza, Bazel se parece a Make, Gradle, Maven y otras herramientas que permiten compilaciones de proyectos basadas en el archivo que contiene una descripción de las reglas de compilación y las dependencias del proyecto. El mismo archivo en Bazel se llama BUILD y se encuentra dentro del espacio de trabajo del proyecto Bazel. El archivo BUILD usa su Starlark, un lenguaje de construcción de alto nivel legible por humanos que se parece mucho a Python.

Por lo general, no tendrá que lidiar mucho con BUILD porque hay muchos repetitivos que se pueden encontrar fácilmente en la web y que ya están configurados y listos para el desarrollo. Siempre que desee construir su proyecto, Bazel básicamente hace lo siguiente:

  1. Carga los archivos BUILD relevantes para el objetivo.
  2. Analiza las entradas y sus dependencias, aplica las reglas de compilación especificadas y produce un gráfico de acción.
  3. Ejecuta las acciones de compilación en las entradas hasta que se producen las salidas de compilación finales.

Enlaces útiles:

  • JavaScript y Bazel: documentos para configurar un proyecto de Bazel para JS desde cero.
  • Reglas de JavaScript y TypeScript para Bazel – Boilerplate para JS.

Conclusión

Monorepos son solo una herramienta. Hay muchos argumentos sobre si tiene futuro o no, pero lo cierto es que en algunos casos esta herramienta hace su trabajo y lo trata de manera eficiente. En el transcurso de los últimos años, esta herramienta evolucionó, ganó mucha más flexibilidad, superó muchos problemas y eliminó una capa de complejidad en términos de configuración.

Todavía hay muchos problemas por resolver, como el bajo rendimiento de Git, pero con suerte, esto se resolverá en un futuro cercano.

Si desea aprender a crear una canalización de CI/CD sólida para su aplicación, le recomiendo Cómo crear una canalización de implementación inicial efectiva con GitLab CI .

Relacionado: Explicación del flujo de Git mejorado