Moderniser un monorepo TypeScript peut être un processus complexe et exigeant. Dans cet article, nous vous présentons la méthodologie que nous avons adoptée. Vous découvrirez, étape par étape, comment mettre à jour les outils et les environnements d'exécution, tout en surmontant les principaux obstacles rencontrés en cours de route.
Bien que chaque projet soit unique, les stratégies présentées ici offrent un cadre adaptable à vos besoins. À la fin, vous aurez une meilleure compréhension de la façon d’aborder la modernisation d’une base de code et de réaliser des améliorations progressivement pour rendre vos projets plus maintenables.
1. Comprendre le point de départ
La base de code est un monorepo TypeScript, initié en décembre 2020 et resté inactif depuis novembre 2022. Ce qui était autrefois une stack technique moderne s’est progressivement transformé en un ensemble de bibliothèques et de configurations obsolètes nécessitant une mise à jour.
Le monorepo contient 4 packages:
api: une API GraphQL basée sur Koa
report-generator: un worker Puppeteer pour la génération de PDF
model: un module commun pour les contrats GraphQL et les utilitaires
front: un frontend React construit avec Create React App.
La gestion des dépendances reposant sur Yarn 1, comportait de nombreuses configurations nohoist et des "selective dependency resolutions", rendant les mises à jour laborieuses.
L’outil Cloc nous fournit des informations utiles sur la taille de la base de code:
cloc --vcs git .
Language | Files | Blank | Comment | Code |
---|---|---|---|---|
TypeScript | 560 | 3304 | 380 | 4 0623 |
JSON | 27 | 34 | 0 | 4 131 |
GraphQL | 31 | 257 | 9 | 2 044 |
Markdown | 21 | 548 | 0 | 1 247 |
YAML | 16 | 101 | 21 | 7 18 |
SQL | 50 | 295 | 351 | 5 44 |
Prisma Schema | 1 | 49 | 0 | 4 39 |
XML/Shell/HTML/ ... | 31 | 58 | 39 | 8 32 |
SUM | 738 | 4 646 | 800 | 50 580 |
Malgré sa taille modeste (50 580 lignes de code), la base de code étant en sommeil depuis deux ans. Le dernier commit remonte à novembre 2022. Plusieurs composants critiques sont devenus obsolètes:
Create React App n’est plus maintenu activement
Node.js 18 est arrivé en fin de vie
De nombreuses bibliothèques (avec plus de 4 versions de retard) ont adopté les modules ECMAScript (ESM), rendant le monorepo incompatible avec les outils modernes.
Jettons un oeil à la couverture de tests automatisés en executant la commande yarn test:all
Package | api | report-generator | model | front | Total |
---|---|---|---|---|---|
Cas de test | 14 | 0 | 3 | 23 | 40 |
Avec un total de 40 cas de test pour plus de 50 000 LOC. Les tests automatisés ne nous apporteront pas de filet de sécurité pendant le processus de migration 😥.
Pourquoi mettre à jour?
L'analyse précédente à mis en évidence que la mise à jour est indispensable. Elle permettra non seulement de mettre le projet à jour avec les pratiques de développement modernes, mais également de garantir la sécurité, une meilleure expérience développeurs et la compatibilité avec les outils modernes.
2. Chemin de migration : étape par étape
Étape 1: Migrer le gestionnaire de packages vers pnpm
La première étape a consisté à remplacer Yarn 1 par pnpm. Cette décision a été motivée par l’approche unique de pnpm en matière de gestion des dépendances. Contrairement à la structure aplatie des node_modules
de Yarn, PNPM utilise une structure arborescente, qui empêche l’accès accidentel aux dépendances non déclarées dans le package.json
. Cette fonctionnalité à elle seule permet de résoudre de nombreux problèmes de dépendance transitive, nous permettant de supprimer de nombreuses configurations nohoist
et des dependency resolutions
.
De plus, la gestion des monorepo de pnpm est excellente.
La migration a été simple : nous avons supprimé les configurations Yarn existantes, initialisé les workspaces pnpm, ajusté les scripts pour la compatibilité et installé toutes les dépendances explicites manquantes dans chaque package.
En tirant parti de l'efficience de l'usage du disque et des temps d'installation plus rapides, les développeurs bénéficient d'une augmentation significative de leur productivité.
Étape 2 : Mettre à jour Node.js

Une fois le gestionnaire de package mis à jour. L'étape suivante a consisté à mettre à jour le runtime. La base de code était sur Node.js 18, qui a maintenant atteint sa fin de vie. La mise à niveau vers Node.js 22, la version actuelle de support à long terme (LTS), a apporté plusieurs avantages.
Outre une sécurité et des performances améliorées, Node.js 22 offre une meilleure prise en charge ESM. Une fonctionnalité plus qu'essentielle pour la prochaine phase de la migration.
Cette mise à niveau impliquait également la mise à jour des pipelines CI/CD, la vérification de la compatibilité des dépendances et la refactorisation des API obsolètes. Ces étapes ont assuré une transition vers un environnement d'exécution plus moderne en conservant les fonctionnalités existantes.
Étape 3 : Transition de CommonJS vers les modules ECMAScript (ESM)
La transition de CommonJS (CJS) vers ESM a peut-être été l'étape la plus difficile mais aussi la plus impactante. Les bibliothèques modernes abandonnant de plus en plus la prise en charge de CJS pour se concentrer sur ESM, ce changement était inévitable.
La migration a commencé par la mise à jour de la configuration TypeScript tsconfig.json
{"compilerOptions": {"module": "preserve", // We use external tools for transpiling"moduleResolution": "Node16",// ...}//...}
Pour en savoir plus sur la configuration du tsconfig.json
, consultez: https://www.totaltypescript.com/tsconfig-cheat-sheet
La migration vers ESM nécessite des mises à jour des package.json
pour définir correctement les types de modules et les entry points. Ci-dessous, nous fournissons un exemple de package.json
avant et après la migration ESM. Les ajustements clés incluent la spécification de "type": "module"
et l'utilisation de champs exports
mis à jour . Pour plus de détails sur la configuration des points d'entrée de package, reportez-vous à la documentation officielle de Node.js: Package Entry Points
{"name": "@myapp/model","private": true,"author": "Kevin Pennarun",- "main": "dist/index.js",- "source": "src/index.ts",- "typings": "dist/index.d.ts",- "scripts":{+ "type": "module",+ "exports": {+ "types": "./dist/myapp-model.d.ts",+ "import": "./dist/myapp-model.mjs",+ "require": "./dist/myapp-model.umd.js"+ },+ "files": [+ "dist"+ ],+ "scripts": {"graphql:codegen": "graphql-codegen --config codegen.yml",- "build": "yarn graphql:codegen && tsc"+ "build": "pnpm graphql:codegen && vite build"},"dependencies":{// ...},"devDependencies" :{// ...}}
Un effort important a été consacré à la refactorisation des imports dans l'ensemble de la base de code afin d'inclure des extensions .js
explicites, une exigence pour ESM. Pour automatiser cette tâche fastidieuse, nous avons utilisé Biome, un outil moderne qui combine linting et formatage avec la prise en charge native de TypeScript. Grace à une seule commande, Biome a corrigé des milliers d'imports, rendant la transition plus rapide et moins sujette aux erreurs.
Il a fallu ajouter la règle useImportExtensions et lancer biome lint . --unsafe --write
pour ajouter toutes les extensions manquantes.
Beaucoup de temps et d'efforts ont été nécessaires pour la mise à niveau de nombreuses bibliothèques obsolète vers leur dernière version afin de garantir qu'elles soient à jour, maintenables et qu'elles bénéficient d'un bon support ESM.
Simultanément, nous avons remplacé les outils de build obsolètes par des alternatives modernes. Le frontend est passé de Create React App à Vite, propulsé par le très efficace esbuild.
Jest a été échangé contre Vitest, qui s'intègre mieux avec ESM.
Pour l'API GraphQL, nous avons introduit SWC, un compilateur ultra-rapide qui a considérablement amélioré les temps de build.
3. Résultat : une stack modernisée
L’effort de modernisation a abouti à une stack technique entièrement mise à jour. Voici un aperçu de la transformation:
Aspect | Avant | Après |
---|---|---|
Node.js | 18 (EOL) | 22 (LTS) |
Package Manager | Yarn 1 | PNPM 9 |
Modules | CommonJS | ESM |
Linting | ESLint | Biome |
Frontend | Create React App | Vite |
Ces changements ont considérablement amélioré l’expérience développeur, simplifié la gestion des dépendances et assuré la compatibilité avec les outils modernes.
Évaluons rapidement un potentiel gain de performance de la mise à jour de la base de code.
Avec l'aide de k6, nous pouvons facilement exécuter un test de charge entre l'ancienne et la nouvelle base de code et comparer les résultats.
Le test sera très simple et ne couvrira pas tous les cas, mais il devrait nous donner une idée approximative du gain de performance.
Nous exécuterons le test avec 10 000 utilisateurs essayant de se connecter pendant 30 secondes.
import http from "k6/http";import { sleep } from "k6";import { config } from "./config";export const options = {vus: 10_000,duration: "30s",};export default function () {http.post(`${config.baseUrl}/auth/login`, {email: config.userEmail,password: config.userPassword,});check(res, {"is status 200": (r) => r.status === 200,});sleep(1);}
Voici le résultat pour l'ancienne base de code:
✗ is status 200↳ 87% — ✓ 46934 / ✗ 6903checks.....................: 87.17% 46934 out of 53837data_received..............: 41 MB 936 kB/sdata_sent..................: 10 MB 229 kB/shttp_reqs..................: 53837 1230.597694/siteration_duration.........: avg=5.79s min=1s med=1.54s max=42.5s p(90)=31s p(95)=31siterations.................: 53837 1230.597694/s
Et voici le résultat pour la nouvelle base de code:
✗ is status 200↳ 98% — ✓ 77679 / ✗ 908checks.....................: 98.84% 77679 out of 78587data_received..............: 68 MB 1.1 MB/sdata_sent..................: 17 MB 277 kB/shttp_reqs..................: 78587 1309.607814/siteration_duration.........: avg=3.92s min=1.35s med=2.58s max=56.41s p(90)=3.16s p(95)=13.24siterations.................: 78587 1309.607814/s
Il y a des amélioration significatives avec la nouvelle base de code:
Le nombre de requêtes total augment de ~46%
Le temps de réponse P(95) décroit de 31s à 13.24s. C'est une amélioration de 134%
Le taux de requêtes en succès augment de 87% à 98%
4. Leçons clés
Tout au long du process, plusieurs enseignements clés ont émergé:
Les outils modernes permettent de gagner du temps: Des outils comme PNPM et Biome simplifient les tâches complexes, économisant des heures de travail manuel. Les règles de dépendance strictes de pnpm garantissent la cohérence entre les packages, tandis que l’intégration transparente de Biome avec TypeScript élimine le besoin de configurer plusieurs plugins de linting.
La compatibilité est importante: L’adoption d’ESM est inévitable. Une transition précoce évite les futurs obstacles et garantit l’accès aux dernières fonctionnalités de la bibliothèque.
Les tests sont essentiels: L’absence de tests d’intégration complets a ralenti le process de migration et introduit des régressions. Investir dans une suite de tests robuste est essentiel pour minimiser les risques lors des migrations à grande échelle.
La mise à jour de la base de code doit être continue: une base de code n’est jamais un actif statique. Elle évolue parallèlement aux outils, aux dépendances et aux technologies sur lesquelles elle s’appuie. Laisser un projet stagner peut entraîner une dette technique s'aggravante, rendant les mises à jour futures beaucoup plus difficiles et chronophages. La mise à jour régulière des dépendances, des frameworks et des environnements d’exécution garantit non seulement la compatibilité et la sécurité, mais permet également aux équipes de profiter des améliorations de performances et des nouvelles fonctionnalités. En adoptant une approche de maintenance continue, vous pouvez atténuer les risques, réduire la complexité des mises à niveau et maintenir votre base de code conforme aux normes du secteur. La modernisation ne doit pas être un effort ponctuel, mais plutôt un processus continu intégré à votre flux de travail de développement.
Moderniser une base de code abandonnée depuis deux ans n'est pas une mince affaire. Cet effort a entrainé la modification de 525 fichiers, avec 27 712 lignes de code ajoutées et 31 125 lignes supprimées. Malgré les défis, le résultat est une base plus légère et plus facile à maintenir pour les évolutions à venir
