Agado Dev

Modernisation d'une base de code - Mise en pratique

blogpost about codebase upgrade illustration

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

Un descriptif des différentes versions de 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 / ✗ 6903
checks.....................: 87.17% 46934 out of 53837
data_received..............: 41 MB 936 kB/s
data_sent..................: 10 MB 229 kB/s
http_reqs..................: 53837 1230.597694/s
iteration_duration.........: avg=5.79s min=1s med=1.54s max=42.5s p(90)=31s p(95)=31s
iterations.................: 53837 1230.597694/s

Et voici le résultat pour la nouvelle base de code:

✗ is status 200
98% — ✓ 77679 / ✗ 908
checks.....................: 98.84% 77679 out of 78587
data_received..............: 68 MB 1.1 MB/s
data_sent..................: 17 MB 277 kB/s
http_reqs..................: 78587 1309.607814/s
iteration_duration.........: avg=3.92s min=1.35s med=2.58s max=56.41s p(90)=3.16s p(95)=13.24s
iterations.................: 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

525 fichiers modifiés. 27712 ajout et 31125 suppressions