backend / nodejs
Modules CommonJS et ESM
Explication
Ce que vous allez apprendre
- Distinguer CommonJS (
require/module.exports) et ESM (import/export) - Comprendre pourquoi ESM permet le top-level
awaitet pas CommonJS - Configurer le type de module d'un projet via
"type": "module"danspackage.json - Importer un module ESM depuis du CommonJS avec un
import()dynamique - Reconstruire
__dirname/__filenameen ESM avecimport.meta.url
Dans quel contexte ?
Une équipe migre un vieux projet Node écrit entièrement en CommonJS (require) vers un starter moderne qui n'exporte que des modules ESM. Le build échoue avec une erreur cryptique dès qu'un fichier CommonJS tente de require() un de ces nouveaux modules. Cette leçon explique pourquoi cette interopérabilité n'est pas automatique dans les deux sens, et comment migrer sans tout casser d'un coup.
Une histoire d'héritage à connaître d'abord
Quand Node.js est né, JavaScript n'avait pas encore de système de modules officiel. CommonJS (require/module.exports) a été inventé à ce moment-là pour combler ce vide.
Des années plus tard, le langage lui-même a adopté un standard officiel : l'ESM (import/export). Résultat aujourd'hui : un développeur Node croise encore les deux systèmes, et doit comprendre leurs différences pour éviter des erreurs déroutantes.
Ce n'est pas qu'une question de syntaxe différente — voyons pourquoi. CommonJS charge les modules de façon SYNCHRONE : le require bloque littéralement jusqu'à ce que le module soit entièrement chargé.
ESM, à l'inverse, est conçu pour être asynchrone dès le départ. C'est ce qui permet le "top-level await" — utiliser await directement en dehors d'une fonction async — quelque chose d'impossible en CommonJS classique.
| Caractéristique | CommonJS | ESM |
|---|---|---|
| Mots-clés | require, module.exports | import, export |
| Chargement | Synchrone | Asynchrone |
Top-level await | Non | Oui |
| Extension / config | .cjs (ou défaut) | .mjs ou "type": "module" |
Prérequis
Cette leçon suppose que tu es à l'aise avec l'event loop vu à la leçon précédente : le chargement synchrone de CommonJS et asynchrone d'ESM s'appuie directement sur cette distinction.
Comment Node sait-il alors quel système utiliser pour un fichier donné ? Il regarde l'extension (.cjs = CommonJS, .mjs = ESM) ou, plus couramment, le champ "type" du package.json : "type": "module" fait que tous les .js du projet sont interprétés comme de l'ESM.
Une fois cette base posée, un piège fréquent guette dès qu'on mélange les deux systèmes. Importer un module CommonJS depuis de l'ESM fonctionne généralement sans problème.
Mais l'inverse est plus délicat. Utiliser require() pour charger un module ESM pur NE fonctionne PAS de façon synchrone — il faut alors un import() dynamique, qui renvoie une Promise, une source fréquente de confusion en migrant un vieux projet.
Piège fréquent
const mod = require("./pure-esm-module.mjs") échoue avec une ERR_REQUIRE_ESM si ce module est écrit en ESM pur. La seule solution correcte est un import() dynamique dans une fonction async, jamais un require() classique.
Un dernier détail surprend souvent les développeurs habitués à CommonJS. __dirname et __filename, disponibles nativement en CommonJS, n'existent PAS en ESM : il faut les reconstruire manuellement via import.meta.url.
Et la suite ? Cette leçon complète directement la précédente : comprendre comment le code s'exécute (l'event loop) va de pair avec comprendre comment il est organisé et chargé en mémoire.
Commandes & code
Modules CommonJS et ESM
// math.cjs — module CommonJS (require/module.exports)
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = { add, multiply };
// ou : module.exports.add = add; export nommé individuel// app.cjs — consommation CommonJS
const { add, multiply } = require("./math.cjs");
const path = require("path"); // modules natifs Node aussi en CommonJS
console.log(add(2, 3));
console.log(path.join(__dirname, "data"));// math.mjs — module ES (ESM), le standard moderne
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
export default function subtract(a, b) {
return a - b;
}// app.mjs — consommation ESM
import subtract, { add, multiply } from "./math.mjs";
import { readFile } from "node:fs/promises"; // préfixe "node:" recommandé pour les modules natifs
console.log(add(2, 3), subtract(5, 2));
const content = await readFile("./data.txt", "utf-8"); // top-level await disponible en ESM
console.log(content);// package.json — déclarer le type de module par défaut du projet
{
"name": "mon-projet",
"type": "module",
"main": "./src/index.js",
"exports": {
".": "./src/index.js",
"./utils": "./src/utils.js"
}
}// Interopérabilité : importer un module CommonJS depuis de l'ESM (souvent OK)
import express from "express"; // fonctionne même si express est en CommonJS
// L'inverse (require un module ESM pur) NE fonctionne PAS directement en CommonJS synchrone :
// il faut un import() dynamique, qui retourne une Promise
async function loadEsmModule() {
const mod = await import("./pure-esm-module.mjs");
return mod.default;
}| CommonJS | ESM | |
|---|---|---|
| Import | require() | import |
| Export | module.exports | export / export default |
| Chargement | Synchrone | Asynchrone (top-level await possible) |
__dirname | Disponible | Non disponible (utiliser import.meta.url) |
// Équivalent de __dirname en ESM
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);Résumé
"type": "module"danspackage.jsonfait de.jsde l'ESM par défaut (sinon CommonJS).- ESM supporte le top-level
await, contrairement à CommonJS. __dirname/__filenamen'existent pas nativement en ESM : reconstruits viaimport.meta.url.- Préférer le préfixe
node:pour les imports de modules natifs (node:fs,node:path).
Exercices pratiques
Mission : réparer une migration CommonJS vers ESM cassée
Objectif : Diagnostiquer une erreur ERR_REQUIRE_ESM et migrer un module de configuration CommonJS vers un module ESM fonctionnel, sans __dirname natif.
Contexte
Une équipe migre progressivement son projet vers l'ESM. Un fichier config.mjs pur ESM a été créé, mais un vieux fichier server.cjs encore en CommonJS tente de le charger avec const config = require("./config.mjs") et le build échoue immédiatement au démarrage avec ERR_REQUIRE_ESM. Le fichier config.mjs a aussi besoin de connaître le chemin absolu de son propre dossier pour résoudre un fichier data.json voisin.