Error [ERR_PACKAGE_PATH_NOT_EXPORTED]

ERR_PACKAGE_PATH_NOT_EXPORTED の直し方(パッケージのサブパスを import して落ちるとき)

FIX SUMMARY verified
Applies when
node:20uuid v8+deep subpath import blocked by package exports field

Verified: reproduced in node:20, then the ERR_PACKAGE_PATH_NOT_EXPORTED signature was gone after the fix (exit 0).

require(または import)でパッケージの中の深い場所を読み込もうとすると、次のエラーで止まることがあります。

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './v4' is not defined by "exports" in /app/node_modules/uuid/package.json

このメッセージは「パッケージが公開している入口の一覧(exports)に、あなたが読もうとしたサブパスが載っていない」という報告です。上の例なら、require('uuid/v4') のように uuid の中の ./v4 を直接読もうとしたのに、いまの uuid はそのサブパスを公開していないために起きます。

直し方は、サブパスを直接読むのをやめて、パッケージが公開している入口から取り出すことです。多くの場合、パッケージのバージョンを上げたことが引き金になっています。

なぜ出るのか:exports が入口を制限する

package.jsonexports フィールドは、そのパッケージの外から読み込んでよいパスを明示的に指定する仕組みです。exports があると、そこに書かれていないパスはすべて非公開になり、require('パッケージ名/内部のパス') のような深い読み込みが ERR_PACKAGE_PATH_NOT_EXPORTED で弾かれます。

uuid はこの典型です。古い記事や Stack Overflow の回答には、次の書き方が大量に残っています。

// 古い書き方(uuid v7 以前)
const uuidv4 = require('uuid/v4');

uuidv8.0.0 で package.jsonexports を導入し、uuid/v4 のようなサブパスの読み込みを廃止しました。そのため、uuid を v8 以降に上げると、この書き方が ERR_PACKAGE_PATH_NOT_EXPORTED で落ちます。コードは変えていないのに、パッケージを上げた瞬間に止まるのが特徴です。その一つ手前の v7 では、同じ書き方が非推奨(DeprecationWarning)になっただけで、まだ読み込めます

uuid のバージョンrequire('uuid/v4')
v7 以前(例:3.4.0 / 7.0.3)読める(v7 は DeprecationWarning つきで読める)
v8 以降(例:8.0.0 / 9.0.1)ERR_PACKAGE_PATH_NOT_EXPORTED

(境界の版は uuid が exports を導入した v8.0.0 です。errfix は 3.4.0・7.0.3 で読めること、8.0.0・9.0.1・14.0.1 で落ちることを実測し、「v8 で exports 導入」は uuid の CHANGELOG を出典にしています。v7 は非推奨警告のみで、封鎖は v8 からです。)

直し方:公開されている入口から取り出す

uuid の場合、いまはパッケージの入口(uuid そのもの)から、名前付きで取り出すのが正しい書き方です。

// 新しい書き方
const { v4: uuidv4 } = require('uuid');

const id = uuidv4();

ESM なら次のとおりです。

import { v4 as uuidv4 } from 'uuid';

これで uuid/v4 という深いパスを読まずにすみ、エラーは消えます。バージョンは下げません。 uuid を v3 に戻せば require('uuid/v4') は通りますが、それはモダンな API を捨てる後退です。今のバージョンのまま、読み込み方だけを直します。

自分のパッケージではなく他のパッケージで出たときの読み方

uuid に限らず、exports を導入・厳格化したパッケージの深い読み込みは、どれも同じエラーになります。読む場所は決まっています。

  • Package subpath './xxx' … あなたが読もうとした、公開されていないサブパス。
  • is not defined by "exports" in .../パッケージ名/package.json … どのパッケージが弾いているか。

この2つで「どのパッケージの、どのサブパスが塞がれたか」が分かります。あとは、そのパッケージが公開している入口を確認するだけです。パッケージの README や型定義、package.jsonexports を見れば、どのパスが読めるかが書かれています。多くの場合、深いサブパスの代わりにトップの入口から名前付きで取り出すか、exports に載っている別のサブパスに読み替えれば直ります。

切り分け(うまくいかないとき)

  • 公開されている入口が見当たらないnode_modules/パッケージ名/package.json を開いて exports を直接見ると、読めるパスの一覧が分かります。読み方の目印として、exports の中で ../ で始まるキーが外から読めるサブパスimport / require / node / browser などは環境ごとに実体のファイルを振り分ける条件(読めるパスではありません)です。"."(トップ)だけが公開されていることも多く、その場合はトップから必要なものを名前付きで取り出します。
  • どうしてもそのサブパスが必要exports を後から緩めることはできません。パッケージの新しいメジャーバージョンで入口が変わった可能性が高いので、README の移行ガイドを見て新しい読み込み方に合わせます。古いバージョンに固定すれば深い読み込みは通りますが、それは後退なので最後の手段です。
  • 綴りを間違えても同じエラーが出るexports があるパッケージでは、uuid/v-4 のようにサブパス名を打ち間違えても、ERR_PACKAGE_PATH_NOT_EXPORTED になります(存在しないサブパスは、公開の有無を問わず一律このエラーです)。このエラーが出ても綴りが正しいとは限らないので、サブパス名のつづりも一度確認してください。
  • エラーが ERR_PACKAGE_PATH_NOT_EXPORTED ではなく Cannot find moduleERR_MODULE_NOT_FOUND:これはパッケージ自体がインストールされていないか、exports を持たない(古い)パッケージで実ファイルが無いときに出ます。exports があるパッケージでの綴り違いは、こちらではなく上記の ERR_PACKAGE_PATH_NOT_EXPORTED になります。
  • 手元では通るのに別環境で落ちる:手元と別環境でパッケージのバージョンが違うことがあります(package-lock.json を共有していない、CI が最新を引いた等)。npm ls パッケージ名 で両方の版を比べてください。
  • 同じ require でも、ネイティブ addon が NODE_MODULE_VERSION で落ちる:これは exports の入口の問題ではなく、C/C++ で書かれた .node を別の Node 版で読み込んだ ABI のズレです。読み込む対象がネイティブ addon で、メッセージが NODE_MODULE_VERSION の数字の食い違いなら、NODE_MODULE_VERSION mismatch の直し方 が対象になります。そもそも npm install の段階で ERESOLVE で止まるなら、ERESOLVE unable to resolve dependency tree の直し方 を見てください。

検証(machine-verified)

この修正は node:20 のバージョン固定コンテナ内で再現し、修正後に ERR_PACKAGE_PATH_NOT_EXPORTED のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs node/exports-subpath
● reproduce ERR_PACKAGE_PATH_NOT_EXPORTED present ✓
● apply fix exit 0
● re-run ERR_PACKAGE_PATH_NOT_EXPORTED gone ✓
PASS verified · node:20 · signature gone