npmパッケージや monorepo の内部パッケージに "exports" フィールドを追加した途端、それまで動いていた import が全てエラーになる。
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './utils' is not defined
by "exports" in /node_modules/my-package/package.json
このエラーに遭遇したことがある人は多いはずだ。"exports" フィールドは Node.js 12 で導入されたが、2024年以降 TypeScript や Vite プロジェクトで積極的に使われるようになり、ハマる人が急増している。
"exports" フィールドとは何か
package.json の "exports" フィールドはパッケージの公開エントリーポイントを明示的に制御する仕組みだ。
"exports" を設定する前は、パッケージの任意のファイルをインポートできた。
// exports 設定前: どちらも動く
import { something } from 'my-package'
import { util } from 'my-package/src/utils'
"exports" を設定すると、そこに定義されていないパスへのアクセスは全てブロックされる。
{
"exports": {
".": "./dist/index.js"
}
}
// exports 設定後
import { something } from 'my-package' // ✅ OK("." で定義済み)
import { util } from 'my-package/src/utils' // ❌ ERR_PACKAGE_PATH_NOT_EXPORTED
これが「追加したら全滅した」の正体だ。
ハマりポイント 1: ディープインポートが全滅する
最も多いケースだ。
症状:
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './components/Button'
is not defined by "exports"
原因:
"exports" に "." だけを定義すると、my-package/components/Button のようなサブパスへのアクセスが全て禁止される。
解決策 1: サブパスを明示的に列挙する
{
"exports": {
".": "./dist/index.js",
"./components/Button": "./dist/components/Button.js",
"./components/Input": "./dist/components/Input.js"
}
}
解決策 2: ワイルドカードを使う(Node.js 12.20+ / 14.13+)
{
"exports": {
".": "./dist/index.js",
"./components/*": "./dist/components/*.js"
}
}
これで my-package/components/Button も my-package/components/Input も両方使える。
ハマりポイント 2: TypeScript の型定義が見つからない
症状:
Could not find a declaration file for module 'my-package'.
原因:
"exports" フィールドがある場合、TypeScript(4.7+)は "types" トップレベルフィールドを無視して "exports" 内の "types" 条件を探す。型定義を "exports" の外側だけに書いていると消えてしまう。
解決策: "exports" に "types" 条件を追加する
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js"
}
}
}
あわせて tsconfig.json の moduleResolution が "bundler" または "node16" / "nodenext" になっているか確認しよう。"node" のままだと "exports" フィールドを解釈しないため、型定義が見つからないままになる。
{
"compilerOptions": {
"moduleResolution": "bundler"
}
}
ハマりポイント 3: ESM と CJS の dual package 設定で "main" が無視される
症状:
CJS 環境(Jest や古いツール)から require() すると動かなくなった。
間違った設定:
{
"main": "./dist/index.cjs.js",
"module": "./dist/index.esm.js",
"exports": {
".": "./dist/index.esm.js"
}
}
"exports" を設定すると "main" は完全に無視される。CJS からの require() が壊れる。
正しい設定:
{
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js",
"types": "./dist/index.d.ts"
}
}
}
"import" 条件は import 文で読み込まれる場合、"require" 条件は require() で読み込まれる場合に使われる。これで ESM/CJS どちらからでも動くようになる。
ハマりポイント 4: monorepo 内部パッケージで設定が無視される
症状:
pnpm/yarn workspace の内部パッケージに "exports" を設定したのに、エラーにも警告にもならず全く効いていない。
原因:
バンドラー(Vite, webpack など)は Node.js の "exports" 解決ロジックを使わないことがある。Vite はデフォルトで "browser" や "module" 条件を優先し、プロジェクトによっては独自の解決順序を持つ。
解決策: Vite の resolve.conditions を設定する
// vite.config.ts
export default defineConfig({
resolve: {
conditions: ['import', 'module', 'browser', 'default'],
},
})
TypeScript 側では tsconfig.json の customConditions(TypeScript 5.0+)で条件を追加できる。
{
"compilerOptions": {
"moduleResolution": "bundler",
"customConditions": ["development"]
}
}
ハマりポイント 5: Node.js バージョンによるワイルドカード未対応
"exports" フィールドの機能はバージョンによって異なる。
| 機能 | 対応 Node.js |
|---|---|
"exports" 基本対応 |
12.7.0+ |
ワイルドカード (*) |
12.20.0 / 14.13.0+ |
"imports" フィールド |
12.19.0+ |
| Subpath patterns 完全対応 | 14.13.0+ |
Node.js 12 系でワイルドカードを使うと動かないことがある。.nvmrc や package.json の "engines" フィールドを確認しよう。
{
"engines": {
"node": ">=14.13.0"
}
}
まとめ: "exports" フィールド設定チェックリスト
□ ディープインポートを使っているか確認する
→ "exports" に "./components/*": "./dist/components/*.js" などワイルドカードで対応
□ TypeScript 型定義を "exports" 内の "types" 条件で設定する
→ "exports" 外の "types" フィールドは無視される
□ tsconfig.json の moduleResolution を "bundler" または "node16" 以上にする
→ "node" のままだと "exports" フィールドを解釈しない
□ ESM/CJS dual package が必要なら "import" と "require" 条件を設定する
→ "main" は "exports" がある場合に無視される
□ monorepo では Vite の resolve.conditions も確認する
→ バンドラーは Node.js の "exports" 解決と異なる動作をすることがある
□ Node.js のバージョンがワイルドカードに対応しているか確認する
→ ワイルドカードは 14.13.0+
"exports" フィールドはパッケージの公開 API を明確にし、内部実装の隠蔽と ESM/CJS の切り替えを実現する強力な機能だ。しかし既存プロジェクトへの追加は破壊的変更になりうる。既存ユーザーがいる場合は CHANGELOG に記載し、メジャーバージョンアップとして扱うのが安全だ。
