Monorepo con Lerna y Yarn Workspaces

Un monorepo, al contrario de lo que su nombre indica, no es un bonobo saltando de rama en rama de nuestro repositorio. Un monorepo es un repositorio que alberga múltiples proyectos.

Albergar varios proyectos en un único repositorio puede parecer un follón importante, pero nos ofrece algunas ventajas. Evidentemente, estamos hablando de proyectos relacionados, no tendría mucho sentido meter en el mismo repositorio proyectos para distintos clientes, por ejemplo.

monorepo

Ventajas

Supongamos que iniciamos un nuevo proyecto. Debe incluir una aplicación móvil creada con React Native, una aplicación web con Vue y una aplicación de escritorio creada con Electron. Además, queremos disponer de un servidor de mocks para poder trabajar en local, independientemente de que el backend esté por desarrollar. Como ya podéis suponer, la mayoría del código de las aplicaciones será en muchos casos el mismo, sólo se diferenciaría la presentación visual con los componentes, enrutadores, etc, propios de cada framework. Por otra parte hay muchas dependencias que también estarían repetidas en varios proyectos.

Trabajando en repositorios separados, es más que probable que repitamos un montón de código entre las tres aplicaciones, además de tener que montar un servidor de mocks para cada una, lo cual es de todo menos eficiente.

Herramientas para gestionar un monorepo

Lerna

Lerna es una herramienta monorepo orientada a Javascript/Typescript, usada entre otros por React o Jest. Entre otras cosas nos permite:

  • Ejecutar tareas, bien para todo el repositorio, bien para uno o varios paquetes.
  • Cacheo del resultado de las tareas. Podemos, por ejemplo, hacer un build global y posteriormente hacer modificaciones en un paquete concreto. Al volver a hacer la build, sólo se hará del paquete modificado, cacheando el resto.
  • Versionado y publicación. Lerna nos permite lanzar y publicar releases de nuestros paquetes, manteniendo el versionado semántico separado por cada uno de los paquetes.

Yarn Workspaces

Yarn Workspaces es utilizado por Lerna en un nivel más bajo. Nos permite lanzar una única instalación de dependencias y asegura las interdependencias entre paquetes. Utiliza un único yarn.lock para todos los paquetes, asegurando así una mejor estabilidad entre ellos. 

Cuando utilizamos uno de nuestros paquetes como dependencia en otro, Yarn crea un enlace simbólico en node_modules al paquete del que dependemos, utilizando siempre nuestro código más actualizado.

Creando un monorepo con Lerna

Vamos a crear una aplicación React y una aplicación de utilidades en Typescript puro. Primeramente instalaremos Lerna a nivel global:

Bash
$ npm i -g lerna

Para crear nuestro monorepo con Lerna, nos situamos en el directorio de nuestro proyecto y lanzamos el siguiente comando:

Bash
$ lerna init --independent --packages="packages/*" --skip-install

¿Qué son estos modificadores?

  • --independent. Con este flag indicamos a Lerna que mantenga un versionado independiente para los paquetes
  • --packages. Indicamos la ruta donde encontrar nuestros paquetes.
  • --skip-install. No realiza la instalación de dependencias. Por defecto Lerna usa npm, pero nosotros usaremos Yarn

Una vez inicializado, instalamos las dependencias con Yarn:

Bash
$ yarn install

Esto nos dejará el siguiente árbol de directorios:

vzsmfAwnvs93HJZ1I6NHFLZTxJTYNukvfTS WWP6PLzcB NEZUmWYD38KIHAWGU8sNHgMfsPW lTezbym7XI539sOcZnUDsb0r3qfuavk3KhR19YfqAhG5GwBdYynm2UAy sJPkYi0Pd sanJaZEVXk

Aplicación React

Crearemos nuestra carpeta packages, y a continuación, situándonos dentro del directorio recién creado, crearemos una aplicación React:

Bash
$ create-react-app awesome-react --template typescript

Una vez terminada toda la fiesta de create-react-app, observamos que no se ha creado un fichero .lock en el directorio de la aplicación React, y que además, la carpeta node_modules correspondiente a nuestra aplicación solo tiene una carpeta:

lBJZ4FJB94OaxKE fi OGHK8es jAXsHWoZ0eCotDV1cFTt07LHvivqCnTI NSj5BMFiMnY0pgJC60ve9nfpF

Es conveniente dar un prefijo identificativo a nuestra app en su package.json:

JSON
"name": "@awesome/awesome-react",

Nuestras dependencias se encuentran en la carpeta node_modules del directorio raíz y registradas en el fichero yarn.lock también del directorio raíz.

También podemos ver que nuestra aplicación se encuentra referenciada en el fichero yarn.lock:

34YegCVItgVzcXFgnboJ3niLaLO4NqQV0GKlDWYFPERN9Orr9KjU0WQK8evTn6miIOYMiW3 OhU4t MbHghj5Y84FZ7JzVDJpkWizjz8CMnBRD9 cn1 W49cVtJfV jD5cNQMzCn5Zrp063h7iuE0Ec

Para lanzar nuestra aplicación podemos hacer como siempre, ir a su directorio y lanzar yarn start. Pero aprovechemos las bondades de Lerna.

En el fichero package.json del directorio raíz, podemos centralizar los scripts de todos nuestros paquetes utilizando el comando lerna exec. En el caso que nos ocupa crearemos el siguiente script para lanzar nuestra aplicación:

JSON
"scripts": {
   "start:react": "lerna exec --scope @awesome/awesome-react -- yarn start"
 }

¿Qué ocurre aquí? Mediante --scope indicamos que paquete es el que nos interesa, en este caso @awesome/awesome-react es el nombre de nuestra aplicación React. A continuación, después del doble guión, indicamos el comando que queremos lanzar del paquete en cuestión.

Paquete de utilidades

Vamos a crear un paquete de utilidades para utilizarlo en la aplicación React. Para ello crearemos una carpeta llamada utilities en el directorio packages. Situándonos en esa carpeta, inicializamos el proyecto Typescript. Vamos a crear primero el fichero package.json:

Bash
$ yarn init

Después de responder a unas preguntas tendremos creado nuestro package.json que tendremos que editar. Primero instalemos Typescript:

Bash
$ yarn add -D typescript

Editamos el package.json. Nos quedará de la siguiente manera:

JSON
{
 "name": "@awesome/utilities",
 "version": "0.1.0",
 "description": "Some awesome utilities",
 "main": "dist/AwesomeCounter.js",
 "types": "dist/Awesomecounter.d.ts",
 "license": "MIT",
 "private": true,
 "scripts": {
   "build": "tsc"
 },
 "devDependencies": {
   "typescript": "^5.2.2"
 }
}
  • En la propiedad main indicamos el fichero transpilado de nuestras utilidades.
  • La entrada types indica la ruta del fichero .d.ts que se generará al crear el build.
  • Creamos un script para hacer el build de nuestro paquete.

En el fichero tsconfig.json tenemos también que hacer alguna modificación:

JSON
"declaration": true,
"sourceMap": true,
"outDir": "./dist", 
  • declaration: true hará que se genere un fichero .d.ts.
  • sourceMap: true generará un fichero sourceMap para poder depurar.
  • outDir: "./dist" indica dist como directorio de los ficheros generados.

Una vez hecho esto, creamos una carpeta src y una clase de utilidades:

TypeScript
// AwesomeCounter.ts
export default class AwesomeCounter {
 static increment(amount: number): number {
   return amount + 1;
 }
}

Ya tenemos una utilidad magnífica sin la que no podremos vivir. El desarrollo de software no volverá a ser lo mismo.

A continuación hacemos la build. Desde la carpeta package/utilities:

Bash
$ yarn build

O también podemos crear un script en el fichero package.json de la raíz:

JSON
"build:utilities": "lerna exec --scope @awesome/utilities -- yarn build"
Bash
$ yarn build:utilities

Esta acción nos generará el siguiente árbol de ficheros:

pVPTnA 5mLhHd4ii0w2uluFPMozLuQQrnV1zcldyooXIqcM39P0SbGPFnU7FUua6hpbtb3G16y2veLtMzk P1ITCxselqNr vUAmPxyvbUbpxMPaJXHoURB7VQuNaiS8l38leCJOu63 cBfaZvs 6fQ

Uso del paquete de utilidades

Vamos a crear una asombrosa aplicación de contador en nuestra app React. Primeramente añadiremos nuestra dependencia interna en el package.json de nuestra aplicación de la siguiente manera:

JSON
"@awesome/utilities": "*",

Para que nuestros paquetes estén disponibles para los demás paquetes de nuestro proyecto debemos lanzar un yarn install. Esto nos creará enlaces simbólicos en la carpeta node_modules del raíz del proyecto:

Una vez hecho esto, vamos a crear un componente en la aplicación React. Siendo extraordinariamente originales y creativos, lo llamaremos Counter, por ejemplo. Lo que vais a ver a continuación no tiene mucho sentido (ninguno, en realidad), pero es simplemente para ejemplificar el uso de un paquete dentro de otro paquete en un entorno monorepo:

TSX
import { useState } from 'react';
import AwesomeCounter from '@awesome/utilities'


const Counter = (): JSX.Element => {
 const [count, setCount] = useState<number>(0);


 const increaseCounter = (): void => {
   setCount(AwesomeCounter.increment(count));
 };


 return (
   <>
     <p>{count}</p>
     <button onClick={increaseCounter}>Click!</button>
   </>
 );
};


export default Counter;

La utilidad de contador se importa exactamente igual que cualquier otra dependencia al estar disponibles junto a las demás.

Importamos nuestro contador en el fichero App.tsx creado por defecto (porque tampoco hace falta complicarse más la vida):

TSX
import React from 'react';
import logo from './logo.svg';
import './App.css';
import Counter from './components/Counter';


function App() {
 return (
   <div className='App'>
     <header className='App-header'>
       <img src={logo} className='App-logo' alt='logo' />
       <p>
         Edit <code>src/App.tsx</code> and save to reload.
       </p>
       <a
         className='App-link'
         href='https://reactjs.org'
         target='_blank'
         rel='noopener noreferrer'
       >
         Learn React
       </a>
       <Counter />
     </header>
   </div>
 );
}


export default App;

Lanzamos la aplicación React, y si todo ha salido bien, podremos ver en nuestro navegador un asombroso contador que se incrementa cada vez que pulsamos en el botón. Fascinante.

imagen 2 1

Versionado

Lerna nos permite versionar automáticamente nuestros paquetes de forma independiente. Para ello debemos hacer previamente un commit con los ficheros que hemos modificado y a continuación ejecutamos el siguiente comando comando:

Bash
$ lerna version

Nos saldrá un prompt como el siguiente para cada paquete afectado por los cambios:

Bash
$ lerna version
info cli using local version of lerna
lerna notice cli v7.3.0
lerna info versioning independent
lerna info Looking for changed packages since @awesome/awesome-react@0.1.1
? Select a new version for @awesome/awesome-react (currently 0.1.1) (Use arrow keys)
 Patch (0.1.2)
  Minor (0.2.0)
  Major (1.0.0)
  Prepatch (0.1.2-alpha.0)
  Preminor (0.2.0-alpha.0)
  Premajor (1.0.0-alpha.0)
  Custom Prerelease
  Custom Version

Habíamos hecho una modificación en el paquete utilities, del que depende la aplicación React, por lo que nos pregunta por la versión de los dos paquetes. Seleccionamos el tipo de versión, por ejemplo ‘Minor’. Una vez seleccionado el tipo de versión en ambos casos nos pide confirmación.

Bash
? Select a new version for @awesome/utilities (currently 0.1.1) Minor (0.2.0)

Changes:
 - @awesome/awesome-react: 0.1.1 => 0.2.0 (private)
 - @awesome/utilities: 0.1.1 => 0.2.0 (private)

? Are you sure you want to create these versions? (ynH)y
lerna info execute Skipping releases
lerna info git Pushing tags...
lerna success version finished

Podemos comprobar en nuestro repositorio que se ha creado un nuevo commit y tenemos nuevas etiquetas:

hLc9Fb O cSOupa9T0WGfSudSNzuvhtQJL6gb43TC qBxKFUIENw7

2dRrwXUZX7E oF

Comandos deprecados

En muchos tutoriales de Lerna verás los comandos lerna add, lerna link y lerna bootstrap. Deberás tener en cuenta que estos comandos han sido eliminados en la versión 7 de Lerna.

Referencias

Lerna: https://lerna.js.org/

Yarn Workspaces: https://classic.yarnpkg.com/lang/en/docs/workspaces/

Semantic Versioning: https://semver.org/

Vendedor de crecepelos

Picture of Dani Cabal

Dani Cabal

Domador de becarios
Picture of Dani Cabal

Dani Cabal

Domador de becarios

We are HIRING!

What Can We Do