Mettre en place un monorepo pnpm et Turborepo pour les applications Node.js, étape par étape
Créez un monorepo TypeScript à partir d’un dossier vide en utilisant pnpm workspaces et Turborepo, puis exécutez, compilez et effectuez des tâches de filtrage pour une application web, une API ainsi que des packages partagés.
Lorsqu’un produit dispose à la fois d’une interface utilisateur, d’un backend et de code, l’utilisation de répertoires séparés devient problématique : les types partagés évoluent de manière incohérente, la configuration est copiée manuellement et une seule modification nécessite plusieurs demandes de fusion. pnpm workspaces associés à Turborepo résolvent ce problème sans devoir fusionner les projets en un seul. Cette démarche guide l’utilisateur depuis un répertoire vide jusqu’à la création de deux applications TypeScript ainsi qu’un package partagé, que l’on peut développer, compiler et gérer depuis une même racine.
Le schéma final :
my-monorepo/
├── apps/
│ ├── web/
│ └── api/
│
├── packages/
│ ├── types/
│ └── eslint-config/
│
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
├── tsconfig.json
└── pnpm-lock.yaml
Ce qu’un monorepo vous offre
frontend-repository
backend-repository
shared-types-repository
ui-library-repository
Dans un monorepo, ces éléments prennent la forme de dossiers, avec du code destiné au déploiement sous apps et du code réutilisable sous packages :
my-monorepo/
├── apps/
│ ├── web/
│ └── api/
│
└── packages/
├── types/
└── ui/
Le code est alors partagé directement au lieu d’être publié d’abord. Un package de types consommé à la fois par le frontend et le backend signifie qu’un changement dans le payload atteint les deux parties en une seule mise à jour :
apps/web
↓
packages/types
↑
apps/api
Le rôle de Turborepo
Les espaces de travail pnpm relient les packages ; Turborepo décide de la manière dont les tâches s’exécutent entre eux. Il offre une orchestration des tâches, un ordre déterminé en fonction des dépendances, une exécution parallèle, un cache local et distant, des builds incrémentaux ainsi que du support pour les espaces de travail.
Prenons un répertoire contenant trois espaces de travail :
apps/web
apps/api
packages/types
Chacun peut définir les mêmes scripts :
build
lint
test
dev
Au lieu d’entrer dans chaque répertoire dans le bon ordre, vous les exécutez depuis la racine et Turborepo parallélise ce qui est possible. Pour savoir quand cette approche est utile, consultez où Turborepo s’intègre dans un monorepo NestJS et quand l’ignorer.
Prérequis
node -v
Et pnpm :
pnpm -v
Si pnpm est manquant, Corepack, inclus avec Node.js, peut le fournir. Activez-le :
corepack enable
Activez la dernière version de pnpm :
corepack prepare pnpm@latest --activate
Vérifiez :
pnpm -v
Mise en place de l’espace de travail racine
Créez le répertoire :
mkdir my-monorepo
cd my-monorepo
Initialisez Git :
git init
Générez le manifeste racine :
pnpm init
Cela laisse :
my-monorepo/
└── package.json
Installez Turborepo à la racine
Turborepo gère l’ensemble du répertoire, donc --workspace-root l’installe à la racine plutôt que dans un package :
pnpm add turbo --save-dev --workspace-root
Le manifeste de la racine contient des scripts qui font appel à turbo run ; l’attribut private empêche la publication de la racine :
{
"name": "my-monorepo",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test"
},
"devDependencies": {
"turbo": "..."
}
}
Votre version de turbo dépend du moment de l’installation. Les versions récentes exigent également un champ packageManager dans le fichier package.json de la racine ; si turbo ne parvient pas à détecter votre gestionnaire de paquets, consultez la documentation.
Déclarez les espaces de travail
pnpm trouve les packages à travers un fichier de racine :
pnpm-workspace.yaml
Énumérez les chemins à traiter comme des packages :
packages:
- "apps/*"
- "packages/*"
Chaque dossier situé directement en dessous devient un espace de travail :
apps/*
packages/*
Créez les dossiers pour les deux applications ainsi que le package types :
mkdir -p apps/web
mkdir -p apps/api
mkdir -p packages/types
L’arborescence actuelle :
my-monorepo/
├── apps/
│ ├── web/
│ └── api/
│
├── packages/
│ └── types/
│
├── package.json
└── pnpm-workspace.yaml
Ajout des deux applications
L’application web
Afin de se concentrer sur le monorepo, l’application web reste pour l’instant une simple application Node.js. Entrez-la :
cd apps/web
Donnez-lui un manifeste :
pnpm init
La forme cible :
apps/web/
├── package.json
└── src/
└── index.ts
Créez le fichier d’entrée :
mkdir src
touch src/index.ts
Ajoutez un placeholder :
console.log("Hello from Web application");
Chaque espace de travail a besoin de TypeScript, donc retournez au répertoire racine :
cd ../..
Et installez-le une fois :
pnpm add typescript --save-dev --workspace-root
L’API
Même schéma : entrez et initialisez :
cd apps/api
pnpm init
Créez le fichier d’entrée :
mkdir src
touch src/index.ts
Ajoutez son placeholder :
console.log("Hello from API application");
apps/
├── web/
│ ├── src/
│ │ └── index.ts
│ └── package.json
│
└── api/
├── src/
│ └── index.ts
└── package.json
Partage de la configuration TypeScript
Retourner à la racine :
cd ../..
Créer une configuration de base :
tsconfig.json
Celle-ci contient les options partagées par tous les espaces de travail : une cible moderne, une résolution NodeNext et des vérifications strictes :
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
Chaque application l’étend et n’ajoute que des paramètres locaux. Pour l’application web, créer :
apps/web/tsconfig.json
Celui-ci fait référence au fichier racine, définit un dossier de sortie et ne compile que le répertoire src :
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
}
L’API obtient le même fichier :
apps/api/tsconfig.json
Avec un contenu identique :
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
}
Les modifications de strictesse ou de cible ont désormais lieu en un seul endroit.
Fournir des scripts de compilation à chaque espace de travail
Turborepo exécute les scripts définis par les espaces de travail. Ouvrir le manifeste web :
apps/web/package.json
Definir un nom ciblé et trois scripts : build via tsc, dev en mode surveillance, lint via ESLint :
{
"name": "@repo/web",
"private": true,
"scripts": {
"build": "tsc",
"dev": "tsx watch src/index.ts",
"lint": "eslint ."
}
}
Puis le manifeste de l’API :
apps/api/package.json
Avec son propre nom :
{
"name": "@repo/api",
"private": true,
"scripts": {
"build": "tsc",
"dev": "tsx watch src/index.ts",
"lint": "eslint ."
}
}
Les filtres et les dépendances de l’espace de travail font référence à ces noms @repo/.... Le script dev a besoin de tsx:
pnpm add tsx --save-dev --workspace-root
Le script lint suppose également que ESLint est installé et configuré ; ajoutez-le ou supprimez le script, sinon pnpm lint échouera.
Configuration du pipeline de tâches
turbo.json décrit le comportement de chaque tâche. Créez-le dans la racine :
turbo.json
Définissez les tâches :
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^lint"]
}
}
}
À noter :
"dependsOn": ["^build"]fait en sorte qu’un paquet attende la compilation des paquets de l’espace de travail sur lesquels il dépend ; le symbole caret indique des dépendances.
outputs indique ce qui doit être mémorisé en cache et restauré, afin que les paquets inchangés ne soient pas reconstruits.dev n’est pas mémorisé en cache et est persistent car un surveillant ne s’arrête jamais.lint exécute d’abord les dépendances.tasks est la clé actuelle ; les versions anciennes utilisaient pipeline, donc certains exemples plus anciens pourraient nécessiter des ajustements.
Démarrage et construction depuis la racine
Démarrez tous les processus de développement :
pnpm dev
Cela fonctionne parce que le script dev de la racine est :
"dev": "turbo run dev"
Turborepo trouve dev dans chaque espace de travail et les démarre ensemble, en utilisant un seul terminal pour l’application web :
cd apps/web
pnpm dev
Et un autre pour l’API :
cd apps/api
pnpm dev
Avec une seule commande depuis la racine :
pnpm dev
Construisez tout de la même manière :
pnpm build
Turborepo organise les builds en fonction des dépendances des packages, en commençant par les packages partagés :
pnpm build
│
▼
turbo run build
│
├── packages/types
│
├── apps/api
│
└── apps/web
Cet ordre provient des dépendances déclarées : tant que packages/types ne dispose pas de un package.json et que les applications n’en dépendent pas, Turborepo ne le compilera pas en premier.
Cibler un seul espace de travail
Le paramètre --filter de pnpm exécute un script dans un espace de travail, comme le serveur de développement API :
pnpm --filter @repo/api dev
Ou une compilation web :
pnpm --filter @repo/web build
Le filtre intégré de Turborepo gère le cache et l’ordre des builds :
pnpm turbo run build --filter=@repo/api
Commandes que vous utiliserez quotidiennement
Installer tout le contenu :
pnpm install
Démarrer le développement :
pnpm dev
Compiler tout :
pnpm build
Vérifier la syntaxe de tout :
pnpm lint
Compiler un seul package :
pnpm --filter @repo/api build
Exécuter une seule application :
pnpm --filter @repo/web dev
Ajouter une dépendance à un espace de travail :
pnpm --filter @repo/api add express
Dépendance à un package local ; workspace:* lie la copie du répertoire plutôt que de télécharger depuis le registre :
pnpm --filter @repo/api add @repo/types@workspace:*
Pourquoi ne pas s’en tenir aux workspaces de pnpm ?
On pourrait se fier uniquement aux workspaces :
apps/
packages/
Cependant, à mesure que le répertoire grandit, la coordination devient plus manuelle :
build
test
lint
typecheck
dev
dependencies
task ordering
caching
Turborepo ajoute une orchestration : une seule commande comprend les relations entre les packages, saute les tâches inchangées et exécute des tâches indépendantes en parallèle :
pnpm turbo run build
Pour une seule application et un seul package, les workspaces classiques peuvent suffire. Vous hésitez encore à choisir un gestionnaire de packages ? Consultez notre comparaison npm et pnpm.
En résumé
Un bon monorepo est un environnement partagé où les applications et les packages évoluent sous le même ensemble d’outils. La pile technique ici est simple :
pnpm
+
Turborepo
+
TypeScript
Démarrez avec deux applications :
apps/
├── web/
└── api/
Évoluez vers davantage de services :
apps/
├── web/
├── admin/
├── api/
└── worker/
Soutenu par des paquets partagés :
packages/
├── ui/
├── types/
├── database/
├── auth/
└── utils/
Le avantage réside dans le partage de code, de types, de configurations et de flux de travail, tout en maintenant une organisation indépendante pour chaque application. Lorsque vous l’élargissez :
- déclarez les dépendances locales avec
workspace:*afin que les compilations se fassent dans le bon ordre - listez tous les artefacts dans
outputs, sinon les restaurations du cache les manqueront - conservez la configuration de base à la racine et étendez-la
- ajoutez
packageManagerainsi que des outils comme ESLint avant de compter sur les scripts racine dans CI
Références : la documentation de Turborepo, le répertoire GitHub de Turborepo, la documentation de pnpm et la documentation de Node.js.