Error [ERR_PACKAGE_IMPORT_NOT_DEFINED]

ERR_PACKAGE_IMPORT_NOT_DEFINED の直し方(#始まりの import を package.json の imports で宣言する)

FIX SUMMARY verified
Applies when
node:22.23.1-alpineNode.js subpath imports (# specifiers must be declared in the package.json imports field)importing a #-prefixed specifier without an imports map in package.json — copying code that used internal imports, or relying on tsconfig paths which Node does not read at runtime

Verified: reproduced in node:22.23.1-alpine, then the ERR_PACKAGE_IMPORT_NOT_DEFINED signature was gone after the fix (exit 0).

#foo のように # で始まる名前を import すると、次のエラーで止まることがあります。

node:internal/modules/esm/resolve:301
  return new ERR_PACKAGE_IMPORT_NOT_DEFINED(
         ^

TypeError [ERR_PACKAGE_IMPORT_NOT_DEFINED]: Package import specifier "#foo" is not defined in package /app/package.json imported from /app/index.mjs
    at importNotDefined (node:internal/modules/esm/resolve:301:10)
    at packageImportsResolve (node:internal/modules/esm/resolve:751:9)

終了コードは 1 です。原因は、**# で始まる import(subpath imports)を使うには、それを package.jsonimports フィールドで宣言する必要があり、その宣言が無い(またはパターンに一致しない)**ことです。#foo は相対パスでも依存パッケージ名でもなく、自分のパッケージ内部向けの別名で、その解決先は imports に書きます。直し方は imports#foo を足すことです。

// package.json
{
  "type": "module",
  "imports": {
    "#foo": "./src/foo.js"
  }
}
// index.mjs — imports に宣言されていれば解決できる
import value from "#foo";
console.log(value);

なぜ起きるのか:# は「内部 import」で、解決先は imports に書く

Node のモジュール解決は、import する名前の形で経路を分けます。

  • ./foo.js(相対パス) … ファイルをそのまま辿る。
  • foo(bare specifier)node_modules の依存パッケージ、あるいはその exports
  • #foo# 始まり)自分のパッケージの imports フィールドで解決する「内部 import」。

# で始まる名前は、imports フィールドのマップにある宣言だけを経由して解決されます。宣言が無ければ、Node は解決先を持たないので、モジュールを読み込む前(packageImportsResolve の段階)で ERR_PACKAGE_IMPORT_NOT_DEFINED を投げます。エラー文の "#foo" is not defined in package .../package.json は、まさに「その #fooimports に定義していない」という意味です。

この内部 import は、深い相対パス(../../../src/foo.js)を安定した別名に置き換えるための機能です。そのため、ファイルシステム上のパスとして自動で辿られることはなく、imports での宣言が前提になっています。

なぜ imports が無いと出るのか

境界は package.json の設定で、Node の版でもコードの中身でもありません。同じ import ... from "#foo" でも、imports の宣言があるかどうかで通るか止まるかが決まります。

  • imports を持つプロジェクトからコードだけ持ってきた#foo を使うファイルをコピーしても、コピー先の package.json に対応する imports が無ければ解決できません。# の別名は各パッケージの package.json にひも付くので、ファイル単位では動きません。
  • TypeScript の paths#foo を解決していたtsconfig.jsonpathstsc(と一部のバンドラ)の型解決の設定で、出力する import 文は書き換えず、実行時の Node も見ません。Node は package.jsonimports を見るので、imports が無ければ実行時に落ちます。旧 moduleResolution: "node"(node10)などで paths だけを頼りに型解決していた構成では「tsc は通るのに Node で落ちる」という食い違いになります(一方 moduleResolution: "node16"/"nodenext"/"bundler" は tsc も package.jsonimports を解決に使うので、imports が無ければ tsc 側でも型エラーになります)。いずれにせよ実行時に要るのは imports なので、まず imports を宣言します。
  • imports はあるが、パターンが一致していない"#internal/*": "./src/internal/*.js" のようなワイルドカード宣言で、import 側の形(拡張子の有無など)がパターンに合わないと、宣言があっても未定義として弾かれます。

まず package.jsonimports があるか、その中に該当する # の名前(またはそれに一致するパターン)があるかを確認します。

直し方:imports に宣言を足す

package.jsonimports に、# の名前とその解決先を書きます。

{
  "type": "module",
  "imports": {
    "#foo": "./src/foo.js"
  }
}

複数の内部モジュールをまとめて別名にするなら、ワイルドカードを使います。

{
  "imports": {
    "#internal/*": "./src/internal/*.js"
  }
}

この場合、import x from "#internal/util"./src/internal/util.js に解決されます。import 側とパターンの形(* が受ける部分、拡張子の付け方)を合わせます。imports のキーは必ず # で始める決まりです(# の無い別名は使えません)。値(右辺)は ./ 始まりのローカルファイルだけでなく、依存パッケージ名にも向けられます(例:{ "#crypto": { "node": "crypto", "default": "./src/crypto-browser.js" } } のように、実行環境で切り替える条件つき)。

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

  • エラーが ERR_PACKAGE_PATH_NOT_EXPORTEDPackage subpath ... is not defined by "exports")だった:それは # の内部 import ではなく、依存パッケージの exports が公開していないサブパスを読んだケースです。対処が別なので ERR_PACKAGE_PATH_NOT_EXPORTED の直し方 を参照してください。見分けは、# で始まる名前(=自分の imports)か、パッケージ名/サブパス(=相手の exports)か、です。
  • imports はあるのに一致しない:ワイルドカード(#x/*)のパターンと import 側の形がずれています。"#internal/*": "./src/internal/*.js" なら import 側は #internal/util(拡張子なし)と書きます。ローカルファイルへ向ける値は ./ 始まりにします(外部パッケージ名へ向けることも可)。
  • tsc は通るのに Node で落ちるtsconfigpaths は実行時の Node の解決には効きません。実行時に要るのは package.jsonimports なので、そちらに宣言します。moduleResolutionnode16/nodenext/bundler なら tsc も imports を解決に使うので、imports を書けば tsc 側も解決できます(この構成では paths の重ね書きは必須ではありません)。
  • #foo は相対パスではない:単に近くのファイルを読みたいだけなら ./foo.js と書けば imports は要りません。内部 import(#)を使うのは、深い相対パスを安定した別名にしたいときです。

検証環境

  • node:22.23.1-alpine、ネットワーク不要
  • 再現:package.jsonimports が無い状態で import value from "#foo" を実行すると、TypeError [ERR_PACKAGE_IMPORT_NOT_DEFINED]: Package import specifier "#foo" is not defined in package ... で終了コード 1
  • 修正:package.json"imports": { "#foo": "./src/foo.js" } を足すと、import 文も対象モジュールも変えずに終了コード 0・シグネチャ消滅(42 を出力)

再現から修正までは errfix の検証ハーネスが機械的に確認しています。reproduce と fix の差は package.jsonimports ブロックだけです。この停止は Node の版ではなくパッケージの設定で決まり、imports を宣言しない #foo は Node 18.20.8 / 20.20.2 / 22.23.1 のいずれでも同じ ERR_PACKAGE_IMPORT_NOT_DEFINED(終了コード 1)になることを実測して verification/probes.txt に記録しています。

検証(machine-verified)

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

verify — run-case.mjs
$ node run-case.mjs node/package-import-not-defined
● reproduce ERR_PACKAGE_IMPORT_NOT_DEFINED present ✓
● apply fix exit 0
● re-run ERR_PACKAGE_IMPORT_NOT_DEFINED gone ✓
PASS verified · node:22.23.1-alpine · signature gone

確認したのは上のイメージの中だけです。別の環境で直らなかった、記述が違う、という場合は 報告してください(対象と検証イメージは件名・本文に入ります)。