Error [ERR_UNSUPPORTED_DIR_IMPORT]

ERR_UNSUPPORTED_DIR_IMPORT: ESM でディレクトリを import したときの直し方(Node.js)

FIX SUMMARY verified
Applies when
node:20-alpinedirectory import in ESM

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

ESM のプロジェクトで、ディレクトリを指す相対 import を実行したら、次のエラーで落ちることがあります。

Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import '/app/greet' is not supported resolving ES modules imported from /app/index.js
Did you mean to import "./greet/index.js"?

原因は、ES Module(ESM)では「ディレクトリを import して中の index.js を自動で探す」ことができないのに、import { x } from './greet' のようにディレクトリを指していることです。2行目のとおり、Node 自身が Did you mean to import "./greet/index.js"? と、書くべきパスまで教えてくれます。CommonJS の頃は require('./greet')./greet/index.js を自動で探してくれたので、type: "module" を入れた(または .mjs にした)タイミングで、それまで動いていた import が落ちます。

なぜ「ディレクトリ指定」が通らないのか(CJS と ESM の解決規則の差)

同じ ./greet でも、そのファイルが CommonJS 扱いか ESM 扱いかで、Node のモジュール解決の仕方が変わります。

書き方CommonJS(requireESM(import
'./greet'(ディレクトリ)./greet/index.js を自動で探す探さない → ERR_UNSUPPORTED_DIR_IMPORT
'./util'(拡張子なしのファイル名)./util.js を自動で補う補わない → ERR_MODULE_NOT_FOUND(別記事)
'./greet/index.js'(ファイルまで)通る通る

CommonJS は「拡張子の補完」「ディレクトリの index.js 探索」を解決の一部として行っていました。ESM はこれをやりません。インポート指定子は URL のように厳密に扱われ、書いたとおりのパスだけを探します。だから手元では CJS で通っていたディレクトリ import が、type: "module" にした環境や .mjs にしたファイルでだけ落ちる、という形になります。

再現(最小構成)

package.json を ESM 指定にします。

{
  "type": "module"
}

greet/index.js を置き、ディレクトリ名で import します。

// greet/index.js
export const greeting = 'hello';
// index.js(type:module 下なので ESM 扱い)
import { greeting } from './greet';

console.log(greeting);

node index.js を実行すると ERR_UNSUPPORTED_DIR_IMPORT が出て、終了コードは 1 になります。greet/index.js は存在するのに、ディレクトリ ./greet から index.js を自動で見つけないためです。

解決:index.js までパスを書く

ESM では、ディレクトリではなくファイルまで指定します。

// index.js(ESM のまま。ファイルまで書いた)
import { greeting } from './greet/index.js';

console.log(greeting);

これで終了コード 0 で動きます。エラーの2行目のヒント(Did you mean to import "./greet/index.js"?)がそのまま答えです。このヒントは実行時のカレントディレクトリ(cwd)を基準に候補を探すので、ファイルのある場所以外から実行する(たとえば親ディレクトリから node reproduce/index.js)とヒント自体が出ないことがあります。表示の形も版で変わり、新しい版は "./greet/index.js" と引用符付き、古い Node 18 は引用符が無く候補パスの基準もずれます。ヒントの有無や形にかかわらず、対処は「ディレクトリではなくファイルまで書く」で同じです。

なお、greet/ の中に package.json を置いて exportsmain を書いても、相対パスのディレクトリ import(./greet)はそれを見ませんexportsmain が効くのは node_modules 経由などでパッケージ名として import するときだけです。ローカルの相対 import では、./greet/index.js とファイルまで書きます。

仕組み:ESM の指定子は「URL」

相対パスやローカルファイルの import は、ESM では最終的に file: URL として解決されます。URL に「フォルダの既定ファイルを推測する」機能が無いのと同じで、ESM も no folder mains(ディレクトリの index.js を自動では探さない)・no default extensions(拡張子を自動で補わない)という規則で、書かれたパスだけを見ます。URL がフォルダを指しても既定ファイルを勝手に選ばないのと同じ発想で、読み込むファイルを一意に決めるための規則です。

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

  • ディレクトリではなく拡張子なしのファイルで出たimport x from './util'./util.js が存在)のようにファイルの拡張子省略なら、エラーコードは ERR_MODULE_NOT_FOUND になります。対処は ERR_MODULE_NOT_FOUND(相対 import の拡張子) を参照してください。
  • npm パッケージの import で出た:相対 import ではなくパッケージ名(import x from 'pkg')で似た症状が出る場合は、そのパッケージ側の exportsmain の設定の問題です。原因が別なので、この記事の「ファイルまで書く」対処では直りません。
  • 同じ ESM 化で別のエラーも出ているtype: "module" にした途端に、require is not definedrequire is not defined in ES module scope)や __dirname is not defined__dirname is not defined in ES module scope)も一緒に出ることがあります。どれも原因は同じ(ESM 扱い)です。
  • そもそも ESM にしたくなかったpackage.json"type": "module" を外すか、そのファイルを .cjs にすれば CommonJS 扱いに戻り、ディレクトリ import が復活します。ESM 扱いになる条件は require is not defined in ES module scope の表も参照してください。

検証(machine-verified)

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

verify — run-case.mjs
$ node run-case.mjs node/err-unsupported-dir-import
● reproduce ERR_UNSUPPORTED_DIR_IMPORT present ✓
● apply fix exit 0
● re-run ERR_UNSUPPORTED_DIR_IMPORT gone ✓
PASS verified · node:20-alpine · signature gone