← 記事一覧に戻る

package.json exports で import できない・動かない時の原因と対策【ERR_PACKAGE_PATH_NOT_EXPORTED】

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/Buttonmy-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.jsonmoduleResolution"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.jsoncustomConditions(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 系でワイルドカードを使うと動かないことがある。.nvmrcpackage.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 に記載し、メジャーバージョンアップとして扱うのが安全だ。