Crea un Monorepo con PNPM workspaces

Cómo crear un monorepo con PNPM workspaces. Ideal para grandes proyectos y muchos developers.

monorepo-pnpm-workspaces

Existen diversas estrategias para organizar la arquitectura de una aplicación web. Las más comunes son:

  • Single-repo: Toda la aplicación se encuentra en un único repositorio, lo que puede simplificar la gestión de proyectos pequeños o de equipos reducidos.
  • Multi-repo: La aplicación se divide en varios paquetes o módulos, y cada uno de ellos se almacena en repositorios separados. Esta estrategia es útil cuando diferentes equipos trabajan en componentes independientes o si se requiere versionado individual para cada paquete.
  • Monorepo: Toda la base de código, incluyendo múltiples paquetes o aplicaciones, se organiza en un solo repositorio.

En este artículo nos centraremos en los monorepos, una estrategia que ha demostrado ser especialmente efectiva para aplicaciones grandes en las que trabajan numerosos desarrolladores. Esta estructura permite compartir código de manera más eficiente, facilita la coordinación entre equipos y simplifica la gestión de dependencias internas.

Concretamente usaremos PNPM workspaces con una aplicación principal y luego con varios paquetes de ejemplo como dependencias.

Inicializar un proyecto nuevo con PNPM

Hace poco escribimos un artículo sobre qué es PNPM y cómo instalarlo que necesitarás crear la area de trabajo.

Podemos empezar creando el proyecto desde la línea de comandos:

Terminal
mkdir pnpm-monorepo
cd pnpm-monorepo
pnpm init

Inicializamos también GIT para tener un control de los cambios que vamos haciendo.

Terminal
git init

Estructura del Monorepo

La aplicación tendrá la siguiente estructura:

pnpm-monorepo
├── apps
└── packages

Podemos crear los directorios con este comando en la terminal:

Terminal
mkdir apps packages

En el directorio apps guardaremos la aplicación principal y en packages las dependencias o librerías que usará la App.

Para que PNPM reconozca que es un monorepo deberemos crear el archivo pnpm-workspace.yaml e indicarle esta estructura de directorios añadiendo el siguiente contenido:

pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'

Añadir una aplicación en /apps

En este directorio podemos añadir la aplicación principal. Para el ejemplo crearemos una app en React pero puede ser de cualquier tipo, React, Vue, Svelte, Remix JS…

Terminal
pnpm create react-app apps/main-app

lo importante es asegurarnos que en el archivo package.json le añadamos un campo name para definir el nombre de la app.

package.json
{
"sideEffects": false,
"name": "main-app",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
}
}

Esto servirá para poder gestionar los scripts de la App desde la raíz del directorio con PNPM: pnpm --filter <package-name> <command>

Terminal
pnpm --filter main-app start

Crear un package

Ahora que ya tenemos una aplicación principal, en el directorio packages podemos añadir librerías. Vamos a añadir un ejemplo en donde crearemos un componente Button como si fuera un package. Para ello crearemos una carpeta en /packages/my-button:

Terminal
cd packages
mkdir my-button
cd my-button
pnpm init
cd ../../

Al ejecutar pnpm init nos generará un archivo package.json con este contenido:

package.json
{
"name": "my-button",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

Situados en la raíz del proyecto podemos añadir dependencias al package my-button, podemos instalar React:

Terminal
pnpm add --filter my-button react
pnpm add --filter my-buttontypescript -D

Ahora añadamos el código del componente React:

packages/my-button/Button.tsx
export function Button(props: any) {
return <button onClick={() => props.onClick()}>{props.children}</button>;
}
export default Button;

Y creamos un archivo un archivo index.ts para exportar los componentes:

packages/my-button/index.ts
export * from './Button';

Build del package

Configuramos tsconfig.json para que al transpilar los archivos TypeScript, se generen los archivos JavaScript en el directorio dist:

packages/my-button/tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": false,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"jsxImportSource": "react",
"strictNullChecks": true,
"incremental": true,
"allowSyntheticDefaultImports": true,
"outDir": "./dist",
"baseUrl": ".",
"paths": {
"~/*": ["./*"],
"@/*": ["src/*"]
}
},
"include": ["."],
"exclude": ["node_modules"]
}

Y modificamos el package.json de my-button añadiendo el archivo de entrada y una nueva tarea dentro de scripts para hacer build del componente :

packages/my-button/package.json
{
"main": "dist/index.js",
"scripts": {
"build": "rm -rf dist && tsc"
},

Ahora desde la raíz del monorepo ya podemos hacer build, ejecutamos este comando para generar los archivos en dist:

Terminal
pnpm --filter my-button build

Usar el package en la aplicación principal

Perfecto! ahora con el package creado ya podemos usarlo en la aplicación principal. Podemos ejecutar stee comando desde la raíz del proyecto:

Terminal
pnpm add my-button --filter main-app --workspace

Este comando añadirá una dependencia en el package.json de la aplicación principal.

apps/main-app/package.json
"dependencies": {
"my-button": "workspace:^",

En la versión aparece workspace:^ que indica que la dependencia se resuelve de forma local en el mismo proyecto. Podemos usar * en vez de ^ si deseamos que siempre use la última versión.

Ahora ya podemos importar el componente Button en la aplicación principal:

pnpm-monorepo/apps/main-app/src/App.js
import logo from './logo.svg';
import './App.css';
import { Button } from 'my-button';
function App() {
return (
<div className="App">
<Button>Mi Botón</Button>
</div>
)
}
export default App

Si ejecutamos la aplicación principal desde la terminal, deberíamos ver el botón.

Terminal
pnpm --filter main-app start

Hacer commit en Git

Si te ha funcionado el proyecto ya puedes añadir los cambios en una branch y hacer commit.

Terminal
git checkout -b "monorepo_pnpm_workspaces"
git add .
git commit -m "feat: crear un monorepo con PNPM workspaces"

Así completamos el tutorial, guardando todo el proyecto (aplicación y packages) en un Monorepo.

Conclusión

Genial! Hemos creado un proyecto con una aplicación y una dependencia en el mismo repositoro con PNPM, usando PNPM Workspaces. Es un proyecto sencillo pero sienta las bases para que sea escalable. Grandes proyectos con cientos de developers se basan en esta arquitectura.

Aún quedaría trabajo para mejorarlo, como automatizar procesos de build o integrarlo en pipelines de CI/CD con GitHub Actions o parecido, pero esto lo veremos otro día. Espereo que te haya sido útil!