Error [Named export not found]

does not provide an export named / Named export not found の直し方(Node.js)

FIX SUMMARY verified
Applies when
node:20-alpinenamed import from CommonJS

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

ES Module(ESM)から名前付き import をしたら、次のどちらかのエラーで止まることがあります。

SyntaxError: The requested module './x.js' does not provide an export named 'foo'
SyntaxError: Named export 'foo' not found. The requested module './x.cjs' is a CommonJS module, which may not support all module.exports as named exports.

前者は「その名前のエクスポートが無い」、後者は「相手が CommonJS で、その名前を名前付きエクスポートとして扱えない」という表示です。名前のタイプミスでなければ、原因はたいてい CommonJS モジュールを ESM から名前付き import していることにあります。

なぜ CommonJS では名前付き import が通らないことがあるのか

CommonJS モジュールは module.exports という 1 つのオブジェクトをエクスポートする仕組みです。ESM の名前付きエクスポートとは形が違います。それでも多くの場合は名前付き import が通ります。Node が CommonJS のコードを静的に解析して、module.exports.foo = ... のように直接たどれる代入を名前付きエクスポートとして拾ってくれるからです(この解析は Node 20 / 22 のドキュメントでは cjs-module-lexer と説明されています)。

問題は、この解析で拾えない書き方のときです。たとえばキーを計算して代入する形は解析できません。

// 解析できる(import { foo } が通ることが多い)
module.exports.foo = () => {};

// 解析できない(import { foo } は "not found" になる)
const name = 'foo';
module.exports[name] = () => {};

だから 「あるパッケージでは名前付き import できるのに、別の CommonJS パッケージだと落ちる」 という差が出ます。相手のエクスポートの書き方しだいで、静的解析が名前を拾えたり拾えなかったりするためです。自分で書き分けているわけではないので、原因が分かりにくいエラーです。

再現(最小構成)

静的解析で拾えない形でエクスポートする CommonJS モジュールを用意します。

// cjs-dep.cjs(.cjs は常に CommonJS)
const name = 'greet';
module.exports[name] = function () {
  return 'hi from CJS';
};

これを ESM から名前付き import します。

// index.js(package.json に "type": "module"。ESM 扱い)
import { greet } from './cjs-dep.cjs';
console.log(greet());

実行すると Named export 'greet' not found ... が出て、終了コードは 1 になります。

解決:既定 import で受け取ってから取り出す

CommonJS モジュールは、既定 importmodule.exports を丸ごと受け取り、そこから必要なものを取り出します。エラーメッセージ自身もこの形を案内しています。

// index.js(既定 import で受けて分割代入)
import pkg from './cjs-dep.cjs';
const { greet } = pkg;
console.log(greet());

これで終了コード 0 で動きます。pkgmodule.exports そのものなので、静的解析に頼らず、実行時に module.exports から取り出せます。取り出した値が undefined になるなら、CommonJS 側にそのプロパティが実際にあるか(名前のタイプミスが無いか)を確認してください。

仕組み:名前付き import は「読み込み前」に解決される

ESM の名前付き import は、コードを実行する前の段階で「その名前が本当にあるか」を照合します。相手が ESM なら export 文を見れば分かりますが、相手が CommonJS だと export 文が無いので、Node は静的解析で名前付きエクスポートを推定します。推定できなかった名前は「無い」と判定され、実行前に SyntaxError になります。

既定 import はこの名前ごとの照合をしません。module.exports オブジェクトを 1 つ受け取るだけなので、プロパティが実行時にどう作られていても取り出せます。

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

  • 相手が自分の ESM ファイルなのに出るdoes not provide an export named 'foo' の場合、その ESM に本当に export があるか、名前(大文字小文字・default か名前付きか)が合っているかを確認します。export defaultimport foo from ...、名前付きは import { foo } from ... です。
  • 既定 import した pkg が関数そのものだったmodule.exports = function () {} の形の CommonJS では、pkg 自体が関数です。pkg.foo ではなく pkg() として使います。
  • ESM のファイルで require が無いと言われた:症状が違うケースです。require is not defined in ES module scope を参照してください。
  • CommonJS を require() したら別のエラーになった:ESM から CJS を読むのではなく、CJS から ESM を require() して落ちたなら require() of ES Module not supported(ERR_REQUIRE_ESM) を参照してください。

検証(machine-verified)

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

verify — run-case.mjs
$ node run-case.mjs node/named-export-not-found
● reproduce Named export 'greet' not found present ✓
● apply fix exit 0
● re-run Named export 'greet' not found gone ✓
PASS verified · node:20-alpine · signature gone