#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.json の imports フィールドで宣言する必要があり、その宣言が無い(またはパターンに一致しない)**ことです。#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 は、まさに「その #foo を imports に定義していない」という意味です。
この内部 import は、深い相対パス(../../../src/foo.js)を安定した別名に置き換えるための機能です。そのため、ファイルシステム上のパスとして自動で辿られることはなく、imports での宣言が前提になっています。
なぜ imports が無いと出るのか
境界は package.json の設定で、Node の版でもコードの中身でもありません。同じ import ... from "#foo" でも、imports の宣言があるかどうかで通るか止まるかが決まります。
importsを持つプロジェクトからコードだけ持ってきた:#fooを使うファイルをコピーしても、コピー先のpackage.jsonに対応するimportsが無ければ解決できません。#の別名は各パッケージのpackage.jsonにひも付くので、ファイル単位では動きません。- TypeScript の
pathsで#fooを解決していた:tsconfig.jsonのpathsは tsc(と一部のバンドラ)の型解決の設定で、出力する import 文は書き換えず、実行時の Node も見ません。Node はpackage.jsonのimportsを見るので、importsが無ければ実行時に落ちます。旧moduleResolution: "node"(node10)などでpathsだけを頼りに型解決していた構成では「tsc は通るのに Node で落ちる」という食い違いになります(一方moduleResolution: "node16"/"nodenext"/"bundler"は tsc もpackage.jsonのimportsを解決に使うので、importsが無ければ tsc 側でも型エラーになります)。いずれにせよ実行時に要るのはimportsなので、まずimportsを宣言します。 importsはあるが、パターンが一致していない:"#internal/*": "./src/internal/*.js"のようなワイルドカード宣言で、import 側の形(拡張子の有無など)がパターンに合わないと、宣言があっても未定義として弾かれます。
まず package.json に imports があるか、その中に該当する # の名前(またはそれに一致するパターン)があるかを確認します。
直し方:imports に宣言を足す
package.json の imports に、# の名前とその解決先を書きます。
{
"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_EXPORTED(Package 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 で落ちる:
tsconfigのpathsは実行時の Node の解決には効きません。実行時に要るのはpackage.jsonのimportsなので、そちらに宣言します。moduleResolutionがnode16/nodenext/bundlerなら tsc もimportsを解決に使うので、importsを書けば tsc 側も解決できます(この構成ではpathsの重ね書きは必須ではありません)。 #fooは相対パスではない:単に近くのファイルを読みたいだけなら./foo.jsと書けばimportsは要りません。内部 import(#)を使うのは、深い相対パスを安定した別名にしたいときです。
検証環境
node:22.23.1-alpine、ネットワーク不要- 再現:
package.jsonにimportsが無い状態で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.json の imports ブロックだけです。この停止は Node の版ではなくパッケージの設定で決まり、imports を宣言しない #foo は Node 18.20.8 / 20.20.2 / 22.23.1 のいずれでも同じ ERR_PACKAGE_IMPORT_NOT_DEFINED(終了コード 1)になることを実測して verification/probes.txt に記録しています。