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"
}
}
- キー(
".","./utils")= 利用者が書くパス(my-package,my-package/utils) - 値 = 実際に読み込まれるファイル。オブジェクトにすると条件付きexportsになり、
import/require/typesなどで出し分けられる "types"は必ず先頭、"default"を使うなら必ず末尾に置く(キーの順序が優先順位になる)
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 フィールド設定チェックリスト
- 公開したいパスはすべて
"exports"に書いたか → ディープインポートが必要ならサブパスかワイルドカード("./components/*")で公開 "types"条件を各エントリーの先頭に置いたか → 後ろに書くと使われない"default"を末尾に置いたか → 先に書くと他の条件が評価されないrequire()からも使うなら"require"条件があるか → ないとNo "exports" main definedtsconfig.jsonのmoduleResolutionがbundler/node16/nodenextか →node10は exports / imports を解釈しない- 内部エイリアスは
"imports"の#始まりで書いたか → 未定義ならERR_PACKAGE_IMPORT_NOT_DEFINED - monorepo ではカスタム条件と
defaultClientConditionsを併用したか "./package.json"を公開したか → ツールが package.json を読めなくなるのを防ぐ
"exports" フィールドはパッケージの公開 API を明確にし、内部実装の隠蔽と ESM/CJS の切り替えを実現する強力な機能だ。しかし既存パッケージへの追加は破壊的変更になりうる。既存ユーザーがいる場合は CHANGELOG に記載し、メジャーバージョンアップとして扱うのが安全だ。