En este post hablaremos del concepto de los monorepositorios, enfocándonos en el ámbito del desarrollo web. Destacaremos ventajas e inconvenientes del uso de esta práctica.

Y, por último, haremos un pequeño tutorial con ejemplos sobre cómo gestionar un monorepo, incluyendo creación y configuración utilizando la herramienta Npm workspaces.

Nota. A lo largo del artículo se habla de proyecto y paquete de manera indistinta, ya que una vez que un proyecto se incluye dentro de un monorepositorio este se denomina paquete. Cuando se hable de instalación de dependencias nos referiremos a librerías como React, Express, etc.

¿Qué es un monorepositorio?

Los monorepositorios, o monorepos, son una práctica en el ámbito del desarrollo de software en la que todo el código fuente de un proyecto se almacena en un solo repositorio de control de versiones. Esto contrasta con la práctica más común de usar múltiples repositorios para diferentes componentes o módulos de un proyecto.

Ventajas y desventajas de los monorepos

En un monorepo, todos los componentes, librerías, microservicios y aplicaciones están organizados en una única estructura de carpetas. Esto tiene varias ventajas:

Sin embargo, los monorepos también pueden tener desventajas, como un mayor tamaño del repositorio y la necesidad de herramientas y prácticas específicas para gestionarlos de manera eficiente.

¿Cómo se gestiona un monorepo?

Para gestionar un monorepositorio existen numerosas herramientas dependiendo de la tecnología utilizada en el proyecto. Algunas de las más conocidas son Lerna, Yarn workspaces, Bazel, Rush, Nx y Npm workspaces.

En este artículo vamos a analizar la opción de Npm workspaces, ya que es muy sencilla de implementar en proyectos javascript, es independiente de frameworks y no requiere ninguna dependencia al ir incluida con Npm a partir de la versión 7.

Creación de un monorepo con Npm workspaces

Vamos a explicar la creación y configuración básica para conseguir un monorepositorio gestionado con Npm workspaces con un sencillo ejemplo.

Para empezar, creamos la carpeta raíz de nuestro proyecto “awesome-app” y, dentro de ella, crearemos dos carpetas: “api” y “app” (estos podrían ser proyectos ya existentes o creados desde cero en el momento de construir el monorepositorio).

En este caso partimos de un proyecto totalmente vacío, por lo que será necesario ejecutar npm init -y tanto en la carpeta raíz como dentro de cada uno de los proyectos.

Una vez hecho esto, ya tenemos los tres “package.json”. A partir de aquí, vamos a añadir la configuración necesaria para que este conjunto de carpetas se convierta en un monorepositorio con múltiples paquetes.

Dentro del “package.json” de la raíz añadimos la siguiente configuración:

"workspaces":[
    "api",
    "app"
  ],

Como alternativa podríamos ubicar todos los proyectos dentro de una carpeta “packages” y añadir la configuración de la siguiente manera:

"workspaces": ["./packages/*"]

Si optamos por la primera opción tendremos más flexibilidad para habilitar o deshabilitar cierto paquete para que no sea tenido en cuenta dentro del monorepositorio.

Si elegimos la segunda opción, no tendremos este control. Pero, por otro lado, no será necesario ir modificando el “package.json” con cada nuevo proyecto que añadamos al monorepositorio. Bastará con que se ubique dentro de la carpeta “packages”.

Instalación de dependencias

Para la instalación de dependencias optamos por editar cada uno de los “package.json” con las dependencias que necesitamos.

Vamos a añadir Express al proyecto de la api y React al proyecto de la app. Los “package.json” quedarían de la siguiente manera:

“package.json” del paquete api:

{
  "name": "api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \\"Error: no test specified\\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express":"^4.18.2"
  }
}

“package.json” del paquete app:

{
  "name": "app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \\"Error: no test specified\\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react":"^18.2.0"
  }
}

Ahora, al ejecutar npm install desde la raíz del monorepo se instalarán las dependencias de ambos paquetes. En este punto puede parecer poco útil, pero imaginemos un monorepositorio con decenas de proyectos dentro, nos estaríamos ahorrando muchas ejecuciones de npm install.

Podemos comprobar también que la carpeta “node_modules” se ha ubicado en la raíz. Esto ocurre porque React y Express, hasta el momento, son dependencias que no tienen varias versiones diferentes dentro del monorepositorio.

Instalar diferentes versiones de una dependencia

Como hemos dicho, nuestras dependencias tienen versiones únicas hasta el momento.

Imaginemos ahora que añadimos otra parte de la app (denominada “app-legacy”) que fue desarrollada tiempo atrás y tiene React 17.

Añadimos la carpeta y su referencia en el “package.json” global (en el array de workspaces). Ejecutamos npm install y comprobamos cómo React 18 y Express siguen apareciendo en el “node_modules” de la raíz.

Mientras que React 17 se ha instalado en un “node_modules” dentro de “app-legacy”, ya que es una versión diferente y podría generar conflictos.

La gran ventaja es que todo esto es controlado por Npm workspaces de manera transparente a nosotros. El proyecto quedaría de la siguiente manera:

.
├── api
│   └── package.json
├── app
│   └── package.json
├── app-legacy
│        ├── node_modules
│   └── package.json
├── node_modules
├── package.json
└── package-lock.json

Ejecución de scripts de Npm en múltiples paquetes

A la hora de ejecutar scripts de test, build, coverage, etc. Npm workspaces también nos provee de herramientas de línea de comandos muy interesantes (los flags —workspaces y —workspace). Gracias a ellas podremos ejecutar un script en múltiples paquetes con una única ejecución.

Veamos algunos ejemplos utilizando el script “test” que viene por defecto en los tres “package.json” de nuestros paquetes:

npm run test --workspaces

> api@1.0.0 test
> echo "Error: no test specified" && exit 1

> app@1.0.0 test
> echo "Error: no test specified" && exit 1

> app-legacy@1.0.0 test
> echo "Error: no test specified" && exit 1
npm run test --workspace app

> app@1.0.0 test
> echo "Error: no test specified" && exit 1
npm install cors --workspace api

added 1 package, and audited 71 packages in 1s

Estado final del monorepositorio

Tras instalar múltiples dependencias, ejecutar scripts y revisar algunas de las principales funciones que nos aporta Npm workspaces, ejecutamos npm list en la raíz para comprobar de qué manera se han instalado las dependencias y cómo controla Npm a qué paquete pertenece cada una de ellas.

npm list
awesome-app@1.0.0 /home/jfmoreno/dev/awesome-app
├─┬ api@1.0.0 -> ./api
│ ├── cors@2.8.5
│ └── express@4.18.2
├─┬ app-legacy@1.0.0 -> ./app-legacy
│ └── react@17.0.2
└─┬ app@1.0.0 -> ./app
  └── react@18.2.0

Podemos comprobar cómo ha detectado correctamente que tenemos dos versiones diferentes de React.

Conclusión

En este post hemos podido comprobar las ventajas que nos aporta una herramienta de gestión de monorepositorios como Npm workspaces, suponiendo un enfoque más eficiente a la hora de administrar código fuente en un solo lugar.

Hemos visto cómo facilita concretamente la instalación de dependencias y la ejecución de scripts, lo que supone una enorme ventaja a la hora de realizar despliegues o procesos de integración continua.

Además, puede evitar muchos problemas relacionados con conflictos entre versiones diferentes de una misma librería.

Npm workspaces fue lanzado en octubre de 2020 y continúa en desarrollo, por lo que es previsible que en un futuro ofrezca nuevas funcionalidades.

Referencias

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.