デュアルパッケージ(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"]
}| フィールド | 用途 |
|---|---|
| main | CommonJS のエントリーポイント(フォールバック) |
| module | ESM のエントリーポイント(バンドラー向け) |
| types | TypeScript 型定義 |
| 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);デュアルパッケージを提供することで、パッケージの互換性が向上し、より多くのプロジェクトで利用してもらえるようになります。