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.json の exports フィールドは、そのパッケージの外から読み込んでよいパスを明示的に指定する仕組みです。exports があると、そこに書かれていないパスはすべて非公開になり、require('パッケージ名/内部のパス') のような深い読み込みが ERR_PACKAGE_PATH_NOT_EXPORTED で弾かれます。
uuid はこの典型です。古い記事や Stack Overflow の回答には、次の書き方が大量に残っています。
// 古い書き方(uuid v7 以前)
const uuidv4 = require('uuid/v4');
uuid は v8.0.0 で package.json に exports を導入し、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.json の exports を見れば、どのパスが読めるかが書かれています。多くの場合、深いサブパスの代わりにトップの入口から名前付きで取り出すか、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 moduleやERR_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 の直し方 を見てください。