CommonJS から ES Modules への移行

多くの Node.js プロジェクトが CommonJS から ES Modules への移行を進めています。この記事では、移行の手順と注意点を解説します。

移行前の準備

まず、現在のプロジェクトの依存関係を確認します。使用しているパッケージが ES Modules に対応しているかを調べましょう。

# 依存パッケージのESM対応状況を確認
npm ls

基本的な移行手順

package.json に “type”: “module” を追加

require() を import に書き換え

module.exports を export に書き換え

__dirname / __filename を修正

Step 1: package.json の変更

{
  "name": "my-project",
  "type": "module",
  "version": "1.0.0"
}

この変更により、すべての .js ファイルが ES Modules として扱われます。

Step 2: require → import

// Before (CommonJS)
const express = require('express');
const { readFile } = require('fs');
const path = require('path');
const utils = require('./utils');

// After (ES Modules)
import express from 'express';
import { readFile } from 'fs';
import path from 'path';
import utils from './utils.js'; // 拡張子が必要

Step 3: exports → export

// Before (CommonJS)
function greet(name) {
  return `Hello, ${name}!`;
}
module.exports = { greet };
module.exports.VERSION = '1.0';

// After (ES Modules)
export function greet(name) {
  return `Hello, ${name}!`;
}
export const VERSION = '1.0';

Step 4: __dirname と __filename の修正

ES Modules では __dirname と __filename が使えません。

// Before (CommonJS)
const configPath = path.join(__dirname, 'config.json');

// After (ES Modules)
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const configPath = join(__dirname, 'config.json');

JSON ファイルの読み込み

ES Modules では JSON を直接 import できない環境もあります。

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

// After (ES Modules) - 方法1: assert を使用(Node.js 17.5+)
import config from './config.json' with { type: 'json' };

// After (ES Modules) - 方法2: fs で読み込む
import { readFileSync } from 'fs';
const config = JSON.parse(readFileSync('./config.json', 'utf-8'));

動的 require の移行

// Before (CommonJS)
const plugin = require(`./plugins/${name}`);

// After (ES Modules)
const plugin = await import(`./plugins/${name}.js`);

条件付き require の移行

// Before (CommonJS)
let db;
if (process.env.NODE_ENV === 'production') {
  db = require('./db-prod');
} else {
  db = require('./db-dev');
}

// After (ES Modules)
const dbModule = process.env.NODE_ENV === 'production'
  ? './db-prod.js'
  : './db-dev.js';
const db = await import(dbModule);

段階的な移行

一度にすべてを移行するのが難しい場合、段階的に進められます。

方法1: .mjs を使用

新しいファイルは .mjs で作成し、既存の .js は CommonJS のまま維持。

方法2: サブディレクトリごとに移行

移行済みのディレクトリに package.json を置いて “type”: “module” を設定。

互換性維持パターン

ESM と CJS の両方から使えるライブラリを公開する場合は、デュアルパッケージとして構成します。

{
  "name": "my-library",
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

よくある問題と解決策

問題解決策
拡張子エラーimport に .js を追加
__dirname が未定義import.meta.url から導出
JSON インポートエラーwith { type: 'json' } または fs.readFileSync
CJS パッケージが動かないdefault インポートを確認

移行は一度に完了させる必要はありません。プロジェクトの状況に応じて、段階的に進めていくのが現実的です。