Retour au blog

ESM en 2026 : La Fin de CommonJS et l'Ère du JavaScript Moderne

Salut HaWkers, 2026 est officiellement l'année de l'adoption complète des ES Modules (ESM) dans l'écosystème JavaScript. Avec Node.js 20+ permettant d'importer ESM depuis du code CommonJS, la dernière grande barrière est tombée.

Comprenons cette transition et comment elle affecte votre code.

Ce Qui a Changé en 2026

Node.js a Unifié les Mondes

// Le grand changement : CJS peut importer ESM

// Avant (jusqu'en 2025) : Cela donnait une erreur
// fichier.cjs
const esModule = require('./module-esm.mjs'); // ❌ Ne fonctionnait pas

// Maintenant (2026) : Ça fonctionne !
// fichier.cjs
const esModule = require('./module-esm.mjs'); // ✅ Fonctionne

// Node.js 20+ (rétro-porté) permet cela
// Cela supprime le principal mal de tête de migration

Pourquoi C'est Important

// Le problème historique

const esmAdoptionBlocker = {
  before: {
    problem: 'Les bibliothèques ESM ne pouvaient pas être utilisées dans les projets CJS',
    symptom: 'ERR_REQUIRE_ESM',
    workaround: 'Dynamic import() - moche et asynchrone',
    result: 'Beaucoup de libs maintenaient dual publish'
  },

  after: {
    solution: 'CJS peut require() ESM directement',
    impact: 'Les libs peuvent être ESM-only sans casser les utilisateurs',
    trend: 'Les nouvelles libs sont ESM-only par défaut'
  },

  timeline: {
    node22: 'Fonctionnalité ajoutée',
    node20: 'Rétro-portée',
    node18: 'Rétro-portée (LTS)',
    adoption: 'Tous les Node.js non-EOL le supportent'
  }
};

CommonJS vs ESM : Les Différences

Syntaxe

// CommonJS (l'ancienne façon)

// Exporter
module.exports = { foo, bar };
// ou
exports.foo = foo;

// Importer
const { foo, bar } = require('./module');
const fs = require('fs');

// ESM (la façon moderne)

// Exporter
export { foo, bar };
export default myFunction;

// Importer
import { foo, bar } from './module.js';
import fs from 'node:fs';

Différences Importantes

// 1. Timing de chargement

// CommonJS : Synchrone, à l'exécution
const a = require('./a'); // S'exécute maintenant
if (condition) {
  const b = require('./b'); // S'exécute conditionnellement
}

// ESM : Asynchrone, avant l'exécution
import a from './a.js'; // Chargé avant l'exécution du code
// import conditionnel nécessite dynamic import
const b = condition ? await import('./b.js') : null;


// 2. __dirname et __filename

// CommonJS : Disponible automatiquement
console.log(__dirname);  // /chemin/vers/dossier
console.log(__filename); // /chemin/vers/dossier/fichier.js

// ESM : Besoin de créer
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);


// 3. Imports JSON

// CommonJS
const config = require('./config.json'); // Fonctionne

// ESM
import config from './config.json' with { type: 'json' }; // Nouvelle syntaxe
// ou
import { readFileSync } from 'node:fs';
const config = JSON.parse(readFileSync('./config.json', 'utf8'));


// 4. Top-level await

// CommonJS : Non autorisé
// await fetch(...); // ❌ SyntaxError

// ESM : Fonctionne !
const data = await fetch('...'); // ✅ Top-level await
export const result = await processData(data);

Pourquoi Migrer Vers ESM

Avantages Techniques

// Avantages d'ESM

const esmBenefits = {
  // 1. Tree shaking
  treeShaking: {
    what: 'Les bundlers suppriment le code inutilisé',
    requirement: 'Imports statiques (ESM)',
    impact: 'Bundles plus petits',
    example: `
      // Vous importez seulement ce que vous utilisez
      import { debounce } from 'lodash-es';
      // Le bundler inclut seulement debounce, pas lodash entier
    `
  },

  // 2. Analyse statique
  staticAnalysis: {
    what: 'Les outils comprennent les imports avant l\'exécution',
    benefits: [
      'Meilleur autocomplete dans les IDEs',
      'Détection des imports cassés',
      'Refactoring plus sûr'
    ]
  },

  // 3. Natif au navigateur
  browserNative: {
    what: 'ESM fonctionne dans le navigateur sans bundler',
    use: 'Développement local, petites apps',
    syntax: '<script type="module" src="app.js"></script>'
  },

  // 4. Top-level await
  topLevelAwait: {
    what: 'await en dehors des fonctions async',
    useful: 'Initialisation de modules',
    example: 'export const db = await connectDB();'
  },

  // 5. Pérenne
  futureProof: {
    what: 'ESM est le standard officiel',
    cjs: 'CommonJS est spécifique à Node.js',
    trend: 'L\'écosystème migre vers ESM'
  }
};

L'Écosystème Change

// État de l'écosystème en 2026

const ecosystem2026 = {
  // Libs qui sont ESM-only maintenant
  esmOnly: [
    'chalk 5+',
    'node-fetch 3+',
    'execa 6+',
    'got 12+',
    'globby 13+',
    'p-limit 4+',
    'nanoid 4+',
    // Et beaucoup d'autres...
  ],

  // Frameworks qui préfèrent ESM
  frameworks: {
    vite: 'ESM-first',
    nuxt3: 'ESM par défaut',
    astro: 'ESM par défaut',
    sveltekit: 'ESM par défaut'
  },

  // Ce qui est encore CommonJS
  stillCJS: {
    express: 'Fonctionne dans les deux',
    lodash: 'A lodash-es pour ESM',
    moment: 'Utilisez dayjs ou date-fns',
    legacy: 'Projets non maintenus'
  }
};

Comment Migrer Vers ESM

Étape 1 : Configurer le package.json

// Ajoutez "type": "module"

// package.json
{
  "name": "mon-projet",
  "version": "1.0.0",
  "type": "module",  // ← Cela fait que .js est traité comme ESM
  "main": "index.js",
  "exports": {
    ".": "./index.js",
    "./utils": "./utils/index.js"
  }
}

// Avec "type": "module" :
// - les fichiers .js sont ESM
// - les fichiers .cjs sont CommonJS (si nécessaire)

// Sans "type": "module" (ou "type": "commonjs") :
// - les fichiers .js sont CommonJS
// - les fichiers .mjs sont ESM

Étape 2 : Mettre à Jour les Imports

// Transformations nécessaires

// De :
const path = require('path');
const { readFile } = require('fs/promises');
const myModule = require('./my-module');

// Vers :
import path from 'node:path';
import { readFile } from 'node:fs/promises';
import myModule from './my-module.js'; // Notez le .js !

// IMPORTANT : ESM requiert l'extension de fichier
import utils from './utils';     // ❌ Ne fonctionne pas
import utils from './utils.js';  // ✅ Fonctionne

// Pour les modules Node, utilisez le préfixe node:
import fs from 'node:fs';     // ✅ Recommandé
import fs from 'fs';          // Fonctionne, mais node: est plus clair

Étape 3 : Mettre à Jour les Exports

// Transformations d'export

// De :
module.exports = myFunction;
module.exports = { foo, bar };
exports.something = something;

// Vers :
export default myFunction;
export { foo, bar };
export const something = something;

// Exports mixtes
export default mainFunction;
export { helper1, helper2 };

Étape 4 : Corriger __dirname/__filename

// Créez un helper ou ajoutez en haut des fichiers qui en ont besoin

// esm-utils.js
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

export function getDirname(importMetaUrl) {
  return dirname(fileURLToPath(importMetaUrl));
}

// utilisation dans n'importe quel fichier :
import { getDirname } from './esm-utils.js';
const __dirname = getDirname(import.meta.url);

// Ou inline (plus courant) :
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

Étape 5 : Mettre à Jour les Imports JSON

// Option 1 : Import assertion (Node 20+)
import config from './config.json' with { type: 'json' };

// Option 2 : Lecture manuelle (plus compatible)
import { readFileSync } from 'node:fs';
const config = JSON.parse(
  readFileSync(new URL('./config.json', import.meta.url), 'utf8')
);

// Option 3 : createRequire (hack utile)
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const config = require('./config.json');

Stratégies de Migration

Migration Graduelle

// Pour les grands projets, migrez graduellement

const migrationStrategy = {
  // Phase 1 : Préparer
  phase1: {
    actions: [
      'Identifier les dépendances ESM-only',
      'Vérifier la version de Node (18+)',
      'Configurer ESLint pour ESM'
    ]
  },

  // Phase 2 : Mode dual
  phase2: {
    how: 'Utilisez .mjs pour les nouveaux fichiers ESM',
    keep: 'Fichiers existants .js comme CJS',
    benefit: 'Migration incrémentale'
  },

  // Phase 3 : Le basculement
  phase3: {
    action: 'Ajoutez "type": "module"',
    rename: 'Fichiers CJS restants en .cjs',
    test: 'Exécutez tous les tests'
  },

  // Phase 4 : Nettoyage
  phase4: {
    actions: [
      'Renommez .mjs en .js',
      'Supprimez les workarounds',
      'Mettez à jour la documentation'
    ]
  }
};

Pour les Bibliothèques

// Si vous maintenez une lib npm

const libraryMigration = {
  // Option 1 : ESM-only (recommandé en 2026)
  esmOnly: {
    packageJson: {
      "type": "module",
      "exports": {
        ".": "./dist/index.js"
      }
    },
    pros: 'Simple, moderne',
    cons: 'Casse les utilisateurs CJS sur Node < 20',
    when: 'Nouvelle lib ou version majeure'
  },

  // Option 2 : Dual package (CJS + ESM)
  dual: {
    packageJson: {
      "main": "./dist/index.cjs",
      "module": "./dist/index.js",
      "exports": {
        ".": {
          "import": "./dist/index.js",
          "require": "./dist/index.cjs"
        }
      }
    },
    pros: 'Compatibilité maximale',
    cons: 'Build plus complexe, dual package hazard',
    when: 'Lib populaire avec utilisateurs legacy'
  }
};

Outils Qui Aident

Codemods et Linters

// Outils pour la migration

const migrationTools = {
  // ESLint
  eslint: {
    plugin: 'eslint-plugin-import',
    rules: {
      'import/extensions': ['error', 'always'],
      'import/no-commonjs': 'error'
    }
  },

  // Codemod
  codemod: {
    tool: 'jscodeshift',
    transforms: 'Transformations cjs-to-esm disponibles',
    command: 'npx cjs-to-esm ./src/**/*.js'
  },

  // TypeScript
  typescript: {
    config: {
      "compilerOptions": {
        "module": "ESNext",
        "moduleResolution": "NodeNext"
      }
    },
    benefit: 'TS génère ESM automatiquement'
  },

  // Bundlers
  bundlers: {
    vite: 'ESM natif',
    rollup: 'ESM-first',
    esbuild: 'Excellent support'
  }
};

Erreurs Courantes et Solutions

Dépannage

// Erreurs courantes lors de la migration

const commonErrors = {
  // Erreur 1 : Cannot use import statement outside a module
  error1: {
    message: 'Cannot use import statement outside a module',
    cause: 'Fichier traité comme CJS',
    solutions: [
      'Ajoutez "type": "module" dans package.json',
      'Utilisez l\'extension .mjs',
      'Vérifiez qu\'il n\'y a pas de package.json parent'
    ]
  },

  // Erreur 2 : ERR_MODULE_NOT_FOUND
  error2: {
    message: 'ERR_MODULE_NOT_FOUND',
    cause: 'Extension manquante dans l\'import',
    solution: 'import x from "./file.js" (pas "./file")'
  },

  // Erreur 3 : __dirname is not defined
  error3: {
    message: '__dirname is not defined',
    cause: '__dirname n\'existe pas en ESM',
    solution: 'Utilisez import.meta.url + fileURLToPath'
  },

  // Erreur 4 : require is not defined
  error4: {
    message: 'require is not defined',
    cause: 'Utilisation de require dans un module ESM',
    solution: 'Utilisez import ou createRequire'
  },

  // Erreur 5 : L\'import JSON ne fonctionne pas
  error5: {
    message: 'Unknown file extension ".json"',
    cause: 'JSON nécessite une assertion',
    solution: 'import x from "./x.json" with { type: "json" }'
  }
};

Conclusion

2026 marque le point de basculement pour ESM en JavaScript. Avec Node.js supprimant la dernière grande barrière (CJS important ESM), il n'y a plus d'excuse pour ne pas migrer.

Actions recommandées :

  1. Nouveaux projets : Utilisez ESM dès le départ
  2. Projets existants : Planifiez une migration graduelle
  3. Bibliothèques : Envisagez ESM-only pour la prochaine majeure
  4. Mettez à jour Node.js : 20+ pour la meilleure expérience

CommonJS a bien servi pendant 15 ans, mais son temps touche à sa fin. ESM est l'avenir - et l'avenir c'est maintenant.

Pour en savoir plus sur l'écosystème JavaScript moderne, lisez : VoidZero 2026.

Allons-y ! 🦅

Commentaires (0)

Cet article n'a pas encore de commentaires. Soyez le premier!

Ajouter des commentaires