こんにちは。開発環境の深淵へようこそ。
フロントエンドの現場で「なぜかライブラリが見つからない」「ビルドツールが警告を吐き続けている」といった怪奇現象に遭遇したことはありませんか?その原因の多くは、Node.jsのモジュール解決の歴史と、現代の標準である`exports`フィールドのミスマッチにあります。
今回は、単なる設定方法ではなく、「なぜ`exports`が生まれたのか」「どうすれば安全にCommonJSとESMを共存させられるのか」という、中級者以上でも意外と見落としがちな本質を紐解いていきます。
—
1. なぜ「exports」が必要になったのか?
かつてNode.jsは、`main`フィールドだけで世界が回っていました。しかし、これは「パッケージの中身を全公開する」という、カプセル化の観点からは非常に危険な仕様でした。
- かつての混沌: ユーザーが `import { x } from ‘pkg/dist/internal/private.js’` のように、ライブラリの内部実装を直接インポートできてしまいました。これにより、ライブラリ作者が内部構造を変えると、利用者のアプリが突然破壊されるという「依存関係の地雷」が頻発していたのです。
この状況を破壊し、「外部に公開するインターフェースを厳格に制御する」ために導入されたのが `exports` です。
—
2. 実践:exportsの設定指針
`exports`フィールドは、単なるパス指定ではありません。「条件」に応じた振り分けを行うルーターです。
最強のパッケージ設定テンプレート
まずは、CommonJSとESMを共存させるための、現場で最も堅牢な設定を見てみましょう。
{
“name”: “my-awesome-lib”,
“version”: “1.0.0”,
“main”: “./dist/index.cjs”, // 互換性維持のためのフォールバック
“module”: “./dist/index.mjs”, // 古いツール用
“exports”: {
“.”: {
“import”: “./dist/index.mjs”, // ES Modules (import文) の入り口
“require”: “./dist/index.cjs” // CommonJS (require関数) の入り口
},
“./package.json”: “./package.json” // 内部バージョン確認用に公開を許可
}
}
なぜこの設定が「劇的に楽」なのか?
- 名前空間の封印: `exports`で定義していないパスは、外部からインポートできません。これにより、「勝手に内部ファイルを触られて壊される」という心配から解放されます。
- 自動切り替え: ユーザーが `import` を使えば `mjs` が、`require` を使えば `cjs` が自動的に選ばれます。開発者は「どちらの形式で書くべきか」を悩む必要がなくなります。
—
3. HelloWorld的な動作確認:なぜ「見つからない」のか?
設定したはずなのに `Module not found` と出る場合、原因は「解決の優先順位」にあります。
ステップ1:環境を作る
まずは最小構成のディレクトリを作成しましょう。
mkdir test-exports && cd test-exports
npm init -y
ここで package.json に上記の exports を記述します
ステップ2:検証用スクリプト
`dist/index.mjs` を作成します。
// dist/index.mjs
export const hello = () => “Hello from ESM!”;
ステップ3:罠を見つける実験
もし、ユーザーが `import { hello } from ‘my-awesome-lib/dist/index.mjs’` と書いたらどうなるでしょうか?
結果:エラーになります。
なぜなら、`exports` で `.` (ルート) 以外を公開していないからです。これは意図的な仕様です。「ライブラリの内部パスを直接指定させない」という制約が、あなたの設計を守ってくれるのです。
—
4. アーキテクトからのアドバイス
「エラーが出たから、とりあえず `exports` を削除する」というのは、セキュリティとメンテナンス性を自らドブに捨てる行為です。
現場で最も多い失敗は、「ビルド成果物のパスと、`exports` のパスがズレている」ことです。これを防ぐために、以下の鉄則を守ってください。
1. 絶対パスを意識する: `exports` の値は常に `./` から始めること。
2. 型定義(types)も忘れずに: もしTypeScriptを使っているなら、`types` 条件を追加してください。
“types”: “./dist/index.d.ts”
これを `exports` の一番上に書くのが、現在のベストプラクティスです。
3. `main` フィールドは「遺物」だが「保険」: `exports` を使っていても、古いバージョンのNode.jsや古いビルドツールが `main` を見に来ることがあります。必ず `main` も併記して、二重の防御壁を構築しましょう。
—
まとめ:あなたのライブラリは、もっと優しくなれる
`exports` を理解することは、単なる設定の暗記ではありません。「自分のコードをどう公開し、どう守るか」というパッケージ設計の思想を持つことです。
これをマスターすれば、ライブラリの利用者は「ただインストールしてimportするだけ」の快適な体験を得られ、あなたは「内部実装を自由に変えられる」という平和な開発ライフを手に入れることができます。
さあ、あなたのパッケージの `package.json` を開いてみてください。そこには、まだ最適化の余地が眠っているはずです。