Cómo crear un monorepo con PNPM workspaces. Ideal para grandes proyectos y muchos developers.
Existen diversas estrategias para organizar la arquitectura de una aplicación web. Las más comunes son:
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.
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:
mkdir pnpm-monorepocd pnpm-monorepopnpm init
Inicializamos también GIT para tener un control de los cambios que vamos haciendo.
git init
La aplicación tendrá la siguiente estructura:
pnpm-monorepo├── apps└── packages
Podemos crear los directorios con este comando en la 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:
packages: - 'apps/*' - 'packages/*'
/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…
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.
{ "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>
pnpm --filter main-app start
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
:
cd packagesmkdir my-buttoncd my-buttonpnpm initcd ../../
Al ejecutar pnpm init nos generará un archivo package.json con este contenido:
{ "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:
pnpm add --filter my-button reactpnpm add --filter my-buttontypescript -D
Ahora añadamos el código del componente React:
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:
export * from './Button';
Configuramos tsconfig.json
para que al transpilar los archivos TypeScript, se generen los archivos JavaScript en el directorio dist
:
{ "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 :
{ "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
:
pnpm --filter my-button build
Perfecto! ahora con el package creado ya podemos usarlo en la aplicación principal. Podemos ejecutar stee comando desde la raíz del proyecto:
pnpm add my-button --filter main-app --workspace
Este comando añadirá una dependencia en el package.json
de la aplicación principal.
"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:
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.
pnpm --filter main-app start
Si te ha funcionado el proyecto ya puedes añadir los cambios en una branch y hacer commit.
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.
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!