【入門編】npmパッケージの「条件付きエクスポート」の罠:exportsフィールドによるモジュール解決の挙動を理解する – ビルド・パッケージ管理ツール生産性向上バイブル

こんにちは。開発環境の深淵へようこそ。

フロントエンドの現場で「なぜかライブラリが見つからない」「ビルドツールが警告を吐き続けている」といった怪奇現象に遭遇したことはありませんか?その原因の多くは、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` を開いてみてください。そこには、まだ最適化の余地が眠っているはずです。

タイトルとURLをコピーしました