デュアルパッケージ(CJS/ESM 両対応)

npm パッケージを公開する際、ES Modules(ESM)と CommonJS(CJS)の両方に対応させることで、より多くの環境で利用できるようになります。このような両対応パッケージを「デュアルパッケージ」と呼びます。

なぜデュアルパッケージが必要か

JavaScript のエコシステムは、CommonJS から ES Modules への移行期にあります。

CommonJS を使用するプロジェクト

既存の Node.js プロジェクト、レガシーなコードベース、ESM 未対応のツール

ES Modules を使用するプロジェクト

モダンなフロントエンド、新規の Node.js プロジェクト、Tree Shaking を活用したいケース

両方のユーザーに対応するために、デュアルパッケージが有効です。

基本的なディレクトリ構成

my-package/
├── package.json
├── src/
│   └── index.js        # ソースコード
├── dist/
│   ├── index.mjs       # ESM 版
│   └── index.cjs       # CJS 版
└── types/
    └── index.d.ts      # TypeScript 型定義

package.json の設定

exports フィールドを使って、モジュールシステムに応じたファイルを提供します。

{
  "name": "my-package",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./types/index.d.ts",
  "exports": {
    ".": {
      "types": "./types/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  },
  "files": ["dist", "types"]
}
フィールド用途
mainCommonJS のエントリーポイント(フォールバック)
moduleESM のエントリーポイント(バンドラー向け)
typesTypeScript 型定義
exports条件付きエクスポート(推奨)

ビルド設定の例

Rollup を使用して、ESM と CJS の両方をビルドする設定例です。

// rollup.config.js
export default [
  {
    input: 'src/index.js',
    output: {
      file: 'dist/index.mjs',
      format: 'es'
    }
  },
  {
    input: 'src/index.js',
    output: {
      file: 'dist/index.cjs',
      format: 'cjs'
    }
  }
];

tsup を使った簡単なビルド

tsup を使うと、設定が非常にシンプルになります。

{
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts"
  }
}
# tsup のインストール
npm install -D tsup

デュアルパッケージの注意点

ステートの問題

ESM と CJS で別々にモジュールが読み込まれると、状態が共有されない可能性がある

解決策

状態を持つコードは極力避けるか、どちらか一方をラッパーにする

// dist/index.cjs(CJS をラッパーにする例)
module.exports = require('./index.mjs');

ラッパー方式

片方をメインにし、もう片方をラッパーとして実装する方法もあります。

// dist/index.cjs
// ESM をメインにして、CJS は動的インポートでラップ
async function load() {
  return import('./index.mjs');
}

module.exports = load;
// または同期的に動作させたい場合は別の戦略が必要

サブパスのエクスポート

複数のエントリーポイントを持つパッケージの場合です。

{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./utils": {
      "import": "./dist/utils.mjs",
      "require": "./dist/utils.cjs"
    }
  }
}

テストの考慮

両方の形式で正しく動作するかテストすることが重要です。

// test/esm.test.mjs
import { add } from 'my-package';
console.assert(add(1, 2) === 3);

// test/cjs.test.cjs
const { add } = require('my-package');
console.assert(add(1, 2) === 3);

デュアルパッケージを提供することで、パッケージの互換性が向上し、より多くのプロジェクトで利用してもらえるようになります。