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.
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:
$ npm i -g lerna
Para crear nuestro monorepo con Lerna, nos situamos en el directorio de nuestro proyecto y lanzamos el siguiente comando:
$ 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:
$ yarn install
Esto nos dejará el siguiente árbol de directorios:
Aplicación React
Crearemos nuestra carpeta packages, y a continuación, situándonos dentro del directorio recién creado, crearemos una aplicación React:
$ 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:
Es conveniente dar un prefijo identificativo a nuestra app en su package.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
:
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:
"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
:
$ yarn init
Después de responder a unas preguntas tendremos creado nuestro package.json
que tendremos que editar. Primero instalemos Typescript:
$ yarn add -D typescript
Editamos el package.json.
Nos quedará de la siguiente manera:
{
"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:
"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:
// 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
:
$ yarn build
O también podemos crear un script en el fichero package.json
de la raíz:
"build:utilities": "lerna exec --scope @awesome/utilities -- yarn build"
$ yarn build:utilities
Esta acción nos generará el siguiente árbol de ficheros:
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:
"@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:
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):
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.
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:
$ lerna version
Nos saldrá un prompt como el siguiente para cada paquete afectado por los cambios:
$ 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.
? 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:
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/