package.json exports/imports とは?書き方と import できない時の対策【ERR_PACKAGE_PATH_NOT_EXPORTED】

公開日:2026-04-14/カテゴリ:Node.js・JavaScript・パッケージ管理/AnchorUp 技術ブログ

npmパッケージや monorepo の内部パッケージに "exports" フィールドを追加した途端、それまで動いていた import が全てエラーになる。

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './dist/utils' is not defined by "exports" in /path/to/node_modules/my-package/package.json

このエラーに遭遇したことがある人は多いはずだ。この記事ではまず "exports" / "imports" フィールドとは何か・どう書くか を整理し、そのうえで ERR_PACKAGE_PATH_NOT_EXPORTED をはじめとする「import できない」トラブルの原因と対策をまとめる。エラーメッセージは Node.js v22.22 で実際に再現して取得したものだ。


結論: package.json の exports とは?どう書くのか

"exports" は「このパッケージを外から import / require するとき、どのパスでどのファイルを読ませるか」を定義するフィールドだ。 "main" の後継で、"exports" があると Node.js は "main" より "exports" を優先し、定義されていないパスへのアクセスはすべて ERR_PACKAGE_PATH_NOT_EXPORTED で拒否する。

{
  "name": "my-package",
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.js",
      "require": "./dist/utils.cjs"
    },
    "./package.json": "./package.json"
  }
}

main / exports / imports の違い

フィールド 役割 誰が使うか 例
"main" 旧来のエントリーポイント(1つだけ) 外部の利用者 "main": "./dist/index.js"
"exports" 公開するパスと条件ごとのファイルを定義。未定義パスはブロック 外部の利用者 "exports": { ".": "./dist/index.js" }
"imports" パッケージ内部だけで使う # 始まりのエイリアス パッケージ自身 "imports": { "#utils/*": "./src/utils/*.js" }

exports フィールドの書き方(パターン別)

パターン1: エントリーポイントが1つだけ

{
  "exports": "./dist/index.js"
}

これは { ".": "./dist/index.js" } の省略形だ。

パターン2: サブパスを公開する

{
  "exports": {
    ".": "./dist/index.js",
    "./utils": "./dist/utils.js"
  }
}

import { x } from 'my-package/utils' が使えるようになる。my-package/dist/utils.js のような実ファイルパスでの import は逆に禁止される点に注意。

パターン3: ワイルドカード(サブパスパターン)

{
  "exports": {
    ".": "./dist/index.js",
    "./components/*": "./dist/components/*.js",
    "./internal/*": null
  }
}

* は文字列置換として扱われる。値を null にすると、そのパスを明示的に非公開にできる。

パターン4: 条件付きexports(ESM / CJS / 型定義の出し分け)

Node.js が解釈する主な条件は次のとおり(Node.js 公式ドキュメントより)。

条件 マッチするとき
"node-addons" Node.js 環境(ネイティブアドオンの出し分け用)
"node" Node.js 環境すべて
"import" import / import() で読み込まれたとき
"require" require() で読み込まれたとき
"module-sync" 読み込み方法を問わない(トップレベル await のない ESM)
"default" 常にマッチするフォールバック。必ず最後に置く

これに加えて TypeScript は "types"、バンドラーは "browser"・"module"・"development" / "production" などの条件を解釈する。React の react パッケージのように、Server Components 用の "react-server" 条件を持つパッケージもある。

キーの順序がそのまま優先順位になるのが最大の注意点だ。上から順に評価され、最初にマッチした条件が採用される。

{
  "exports": {
    ".": {
      "import": {
        "types": "./dist/index.d.mts",
        "default": "./dist/index.mjs"
      },
      "require": {
        "types": "./dist/index.d.cts",
        "default": "./dist/index.cjs"
      }
    }
  }
}

ESM と CJS で型定義ファイルを分けるこのネスト構造は、TypeScript 公式ドキュメントで示されている書き方だ。

パターン5: package.json 自体を公開する

"exports" を定義すると my-package/package.json も読めなくなる。バージョン取得などでツールが package.json を読む場合に備え、"./package.json": "./package.json" を加えておくと安全だ。


package.json の imports フィールドとは?exports との違い

"imports" はパッケージ内部専用のエイリアスで、キーは必ず # で始める。 "exports" が「外に見せる入口」なのに対し、"imports" は「自分のコード内で ../../../utils を短く書くための仕組み」で、tsconfig の paths と違いビルドツールなしで Node.js がそのまま解決する。

{
  "name": "app",
  "type": "module",
  "imports": {
    "#utils/*": "./src/utils/*.js",
    "#config": {
      "development": "./src/config.dev.js",
      "default": "./src/config.js"
    }
  }
}
// src/index.js
import { x } from '#utils/math'   // → ./src/utils/math.js

定義していない # パスを import すると次のエラーになる。

TypeError [ERR_PACKAGE_IMPORT_NOT_DEFINED]: Package import specifier "#nope" is not defined in package /path/to/app/package.json imported from /path/to/app/b.js
項目 exports imports
キーの形式 "." または "./xxx" "#xxx"(# 必須)
使う人 パッケージの利用者 パッケージ自身のコード
未定義パスのエラー ERR_PACKAGE_PATH_NOT_EXPORTED ERR_PACKAGE_IMPORT_NOT_DEFINED
条件付き指定 できる できる
ワイルドカード できる できる

なお、Node.js v25.4.0 / v24.14.0 からは #/ で始まるキー("#/*": "./src/*")も使えるようになった。それ以前のバージョンではこの形式は使えないので、#utils/* のように # の直後に名前を入れておくのが無難だ。

TypeScript で "imports" を解決させるには、moduleResolution を "node16" / "nodenext" / "bundler" のいずれかにする必要がある(後述)。


ERR_PACKAGE_PATH_NOT_EXPORTED が出る原因一覧

結論: Node.js が「exports に定義されていないパス」または「どの条件にもマッチしないエントリー」を読もうとしたときに出る。 メッセージの形で原因を見分けられる。

エラーメッセージ 原因 対策
Package subpath './xxx' is not defined by "exports" ディープインポート(未公開サブパス) サブパスを exports に追加/公開パスに書き換え
No "exports" main defined in .../package.json "." の条件が今の読み込み方法にマッチしない(例: import 条件しかないのに require()) require / default 条件を追加、または ESM で import
Could not find a declaration file for module(TS) TypeScript が exports 内の型を見つけられない "types" 条件を先頭に追加、moduleResolution を見直す
Failed to resolve entry for package(Vite) exports が指すファイルが存在しない(未ビルドなど) ビルドする/ソースを指す条件を追加

実際に Node.js v22.22 で再現したメッセージは次のとおり。

# require('my-package/dist/utils') の場合
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './dist/utils' is not defined by "exports" in /path/to/node_modules/my-package/package.json

# import 'my-package/dist/utils.js' の場合("imported from" が付く)
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './dist/utils.js' is not defined by "exports" in /path/to/node_modules/my-package/package.json imported from /path/to/app/index.js

# exports が { ".": { "import": "./index.mjs" } } のパッケージを require() した場合
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: No "exports" main defined in /path/to/node_modules/esm-only/package.json

以下、ケース別に詳しく見ていく。


ハマりポイント 1: ディープインポートが全滅する(Package subpath is not defined)

最も多いケースだ。

症状:

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './components/Button' is not defined by "exports" in /path/to/node_modules/my-package/package.json

原因:

"exports" に "." だけを定義すると、my-package/components/Button のようなサブパスへのアクセスが全て禁止される。"exports" を設定する前は任意のファイルを import できたため、「追加したら全滅した」という現象になる。

対策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 も両方使える。

対策3: 利用側(自分がパッケージを編集できない場合)

サードパーティのパッケージで出た場合は、パッケージ側が公開しているパスに書き換えるのが正解だ。node_modules/<pkg>/package.json の "exports" を開き、公開されているキーを確認しよう。ライブラリのメジャーアップデートで exports が追加され、lib/xxx のような内部パスの import が使えなくなるのはよくあるパターンだ。


ハマりポイント 2: TypeScript の型定義が見つからない(types 条件の順序)

症状:

Could not find a declaration file for module 'my-package'.

原因:

moduleResolution が node16 / nodenext / bundler のとき、TypeScript は "exports" を Node.js と同じ仕様で解決する。そのため "exports" 内に型の場所がないと、トップレベルの "types" に頼れず型が見つからないことがある。さらに TypeScript 4.7 のリリースノートには 「"types" 条件は常に "exports" の先頭に置くべき」 と明記されている。"import" などより後ろに書くと、先にマッチした条件が採用されて "types" が使われない。

対策: "types" 条件を先頭に追加する

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.esm.js",
      "require": "./dist/index.cjs.js"
    }
  }
}

あわせて tsconfig.json の moduleResolution を確認しよう。

moduleResolution exports imports
node10(旧 node) 解釈しない 解釈しない
node16 / nodenext 解釈する 解釈する
bundler 解釈する 解釈する

"node"(node10)のままだと "exports" を解釈しないため、サブパスの型が見つからないままになる。

{
  "compilerOptions": {
    "moduleResolution": "bundler"
  }
}

古い TypeScript や node10 設定の利用者も考慮するなら、トップレベルの "types" と "main" もフォールバックとして残しておくとよい(TypeScript 4.7 リリースノートの例もこの形)。


ハマりポイント 3: ESM と CJS の dual package で require() が動かない(No “exports” main defined)

症状:

CJS 環境(Jest や古いツール)から require() すると動かなくなった。

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: No "exports" main defined in /path/to/node_modules/my-package/package.json

間違った設定:

{
  "main": "./dist/index.cjs.js",
  "module": "./dist/index.esm.js",
  "exports": {
    ".": {
      "import": "./dist/index.esm.js"
    }
  }
}

"exports" があると Node.js は "main" より "exports" を優先する。"import" 条件しかないため、require() ではどの条件にもマッチせず上のエラーになる。また "module" フィールドはバンドラー向けの慣習で、Node.js は解釈しない。

正しい設定:

{
  "main": "./dist/index.cjs.js",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.esm.js",
      "require": "./dist/index.cjs.js"
    }
  }
}

"import" 条件は import 文で読み込まれる場合、"require" 条件は require() で読み込まれる場合に使われる。これで ESM/CJS どちらからでも動く。ESM 専用パッケージを require() して出る ERR_REQUIRE_ESM については Node.js で ERR_REQUIRE_ESM が出る時の原因と対策 で詳しく解説している。


ハマりポイント 4: monorepo の内部パッケージが解決できない(未ビルドの dist を指している)

症状:

pnpm / npm workspaces の内部パッケージを Vite アプリから import すると、次のエラーになる。

Failed to resolve entry for package "@acme/ui". The package may have incorrect main/module/exports specified in its package.json

原因:

内部パッケージの "exports" が ./dist/index.js を指しているのに、まだビルドしていない(dist が存在しない)。開発中は毎回ビルドするのが面倒なので、ソースを直接読ませたいケースが多い。

対策: 開発用のカスタム条件でソースを指す

{
  "name": "@acme/ui",
  "exports": {
    ".": {
      "source": "./src/index.ts",
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    }
  }
}

Vite 側で source 条件を有効にする。Vite 6 以降は resolve.conditions を指定するとデフォルト値が置き換わるため、vite からエクスポートされている defaultClientConditions を展開して残すのがポイントだ(import / require / default は指定しなくても常に適用される)。

// vite.config.ts
import { defineConfig, defaultClientConditions } from 'vite'

export default defineConfig({
  resolve: {
    conditions: ['source', ...defaultClientConditions],
  },
})

TypeScript 側では customConditions(TypeScript 5.0+、moduleResolution が bundler / node16 / nodenext のとき有効)で同じ条件を追加する。

{
  "compilerOptions": {
    "moduleResolution": "bundler",
    "customConditions": ["source"]
  }
}

pnpm で「依存に書いていないパッケージが import できてしまう/できない」という別の問題が絡んでいる場合は pnpm で Cannot find module になる幽霊依存の原因と対策 も確認してほしい。


「Attempted import error: is not exported from」は exports フィールドのエラーなのか?

結論: 別物だ。 Attempted import error: 'X' is not exported from 'Y' は webpack 系(Next.js など)のビルド時メッセージで、「モジュールは見つかったが、その名前付きエクスポート X が存在しない」という意味。ERR_PACKAGE_PATH_NOT_EXPORTED(パス自体が非公開)とは原因が異なる。

ただし、exports の条件付き解決が原因で「別のファイル」が読まれているケースはある。

確認ポイント 内容
default と named の取り違え export default しかないのに import { X } している(またはその逆)
条件によって読まれるファイルが違う 例: react は "react-server" 条件で Server Components 用ファイルを返す。このファイルには createContext や useState が含まれないため、Server Component から使うと「not exported」になる → 'use client' を付ける
バージョン違い 依存の重複やアップデートで、そのエクスポートがないバージョンが解決されている
ビルドキャッシュ .next や node_modules/.cache の古いキャッシュ。削除して再ビルド

ハマりポイント 5: Node.js バージョンによる機能差

"exports" 関連の機能は Node.js のバージョンによって異なる(Node.js 公式ドキュメントの履歴より)。

機能 対応 Node.js
"exports" / サブパスexports 12.7.0+
条件付きexports 13.2.0+(フラグなしは 13.7.0+)
"imports" フィールド 14.6.0+ / 12.19.0+
サブパスパターン(*) 14.13.0+ / 12.20.0+
パターンの後方一致(trailers) 16.9.0+ / 14.19.0+
#/ で始まる imports 25.4.0+ / 24.14.0+

現行の LTS(Node.js 22 / 24)であれば #/ 以外はすべて使える。古い Node.js をサポートする必要があるなら "engines" で明示しよう。

{
  "engines": {
    "node": ">=18"
  }
}

まとめ: exports / imports フィールド設定チェックリスト

  1. 公開したいパスはすべて "exports" に書いたか → ディープインポートが必要ならサブパスかワイルドカード("./components/*")で公開
  2. "types" 条件を各エントリーの先頭に置いたか → 後ろに書くと使われない
  3. "default" を末尾に置いたか → 先に書くと他の条件が評価されない
  4. require() からも使うなら "require" 条件があるか → ないと No "exports" main defined
  5. tsconfig.json の moduleResolution が bundler / node16 / nodenext か → node10 は exports / imports を解釈しない
  6. 内部エイリアスは "imports" の # 始まりで書いたか → 未定義なら ERR_PACKAGE_IMPORT_NOT_DEFINED
  7. monorepo ではカスタム条件と defaultClientConditions を併用したか
  8. "./package.json" を公開したか → ツールが package.json を読めなくなるのを防ぐ

"exports" フィールドはパッケージの公開 API を明確にし、内部実装の隠蔽と ESM/CJS の切り替えを実現する強力な機能だ。しかし既存パッケージへの追加は破壊的変更になりうる。既存ユーザーがいる場合は CHANGELOG に記載し、メジャーバージョンアップとして扱うのが安全だ。

技術ブログの記事一覧へ