Node.js 26 に上げたら node_modules/yargs/yargs で require is not defined in ES module scope になる

require is not defined in ES module scope が node_modules の中で出る(Node.js 26 と yargs)

FIX SUMMARY verified
Applies when
node@sha256:4ebb5ace66f15a24c14c492e01a8beeed4fddf970a856109f5126e703e5fe503yargs 17.7.2 (exports["./yargs"] points its require condition at the extensionless ./yargs; fixed in 17.7.3 by adding yargs.cjs and repointing the condition)upgrading Node.js 24/25 to 26 with yargs 17.7.2 or older pinned in the lockfile — require('yargs/yargs') resolves an extensionless file inside a "type": "module" package, which Node 26.0.0 no longer treats as CommonJS

Verified: reproduced in node@sha256:4ebb5ace66f15a24c14c492e01a8beeed4fddf970a856109f5126e703e5fe503, then the ReferenceError: require is not defined in ES module scope, you can use import instead signature was gone after the fix (exit 0).

Node.js 26 に上げたあと、自分では触っていない node_modules の中のファイルでこのエラーが出て、CLI やテストが起動しなくなることがあります。

file:///app/node_modules/yargs/yargs:3
const {applyExtends, cjsPlatformShim, Parser, Yargs, processArgv} = require('./build/index.cjs')
                                                                    ^

ReferenceError: require is not defined in ES module scope, you can use import instead
    at file:///app/node_modules/yargs/yargs:3:69
    at ModuleJobSync.runSync (node:internal/modules/esm/module_job:673:37)
    at ModuleLoader.importSyncForRequire (node:internal/modules/esm/loader:350:47)
    at loadESMFromCJS (node:internal/modules/cjs/loader:1752:24)
    at Module._compile (node:internal/modules/cjs/loader:1916:5)

止まっているのは node_modules/yargs/yargs で、依存パッケージの中のファイルです。自分の package.json に "type": "module" は無く、自分のコードは CommonJS のまま、という構成でも出ます。

検索で打たれる形はいくつかあります。

  • ReferenceError: require is not defined in ES module scope, you can use import instead
  • at file:///.../node_modules/yargs/yargs:3:69
  • スタックトレースの loadESMFromCJS(CommonJS の require が ESM を読み込んだ経路)
  • npx で実行したツールの中で起きた場合は、パスが ~/.npm/_npx/<ハッシュ>/node_modules/yargs/yargs になる形で報告されています(artifact-registry-npm-tools#88)

原因は Node.js の解決規則の変更と、yargs 17.7.2 以前の exports の書き方の組み合わせです。この変更は 25.7.0 で一度入り、25 系の後の版では戻され、26.0.0 から残りました。26 に上げていなくても、Node.js 25.7.0 と 25.8.0 では同じエラーが出ます。

直し方

yargs を 17.7.3 以上にする

コードは変えません。yargs 17.7.3 が同じ問題を上流で直しています。

npm update yargs

npm update はロックファイルも書き換えます。反映されたことは版で確認できます。

npm ls yargs
[email protected] /app
`-- [email protected]

17.7.3 は 17 系のパッチで、require('yargs/yargs') の呼び出し方はそのまま使えます。18 系でもこのエラーは出ませんが、17 から 18 へ上げる必要はありません。

npm update が動かすのは package.json に書いた範囲の中だけです。 上の例は依存指定が ^17 の場合です。"yargs": "17.7.2" のように版を固定していると、npm update yargs を打っても 17.7.2 のまま変わらず、エラーも消えません。その場合は指定そのものを書き換えます。

"dependencies": {
  "yargs": "^17.7.3"
}

自分のコードに require('yargs/yargs') が無いとき

yargs を直接使っておらず、CLI ツールの依存として入っている場合も同じエラーになります。まず誰が引いているかを見ます。

npm ls yargs
[email protected] /app
`-- [email protected]
  `-- [email protected]

この形なら、npm update yargs で入れ替えられます。上の例では c8 の依存指定が ^17 なので、17.7.3 が条件を満たし、c8 自体を上げる必要はありません。

引いている側が版を固定していて npm update では動かせない場合は、package.json の overrides で解決先を指定します(yarn なら resolutions)。

"overrides": {
  "yargs": "17.7.3"
}

overrides は引いている側の指定に関係なく解決先を決めます。c8 が yargs を ^17.7.2 と宣言している構成に対して overrides へ 17.7.3 を書くと 17.7.3 が入り、c8 が起動します(範囲の外にある 16.2.0 を書けば 16.2.0 が入ります)。ただし自分でも yargs を直接の依存に書いている場合、overrides にそれと違う版を書くと npm が衝突として拒否し、インストール自体が失敗します(その場合は上のように直接の依存の指定を書き換えます)。

新しく npm install した環境でこのエラーが出ないのに、既存のリポジトリで出るのは、ロックファイルが 17.7.2 以前を固定しているためです。素の npm install は ^17 から 17.7.3 を選びますが、npm ci はロックファイルに書かれた版をそのまま復元します。手元で作り直した検証用プロジェクトでは再現せず、CI だけが落ちる、という形になります。

yargs の版を動かせないとき

読み込む形を変えても通ります。

// 変更前
const yargs = require('yargs/yargs');

// 変更後
const yargs = require('yargs');

require('yargs') は exports の "." を解決し、index.cjs という拡張子付きのファイルに当たるので、Node.js 26 でも CommonJS のまま読み込まれます。呼び出し側の形(yargs(hideBin(process.argv)))は変わりません。

ただし返ってくるものは同じではありません。require('yargs/yargs') は factory 関数を返し、require('yargs') は $0 や argv といったプロパティを持つ singleton のインスタンスを返します。どちらも関数として呼べるので上のコードは通りますが、同じオブジェクトではありません(=== は false)。

singleton に変えると前の呼び出しの設定が次に残るのではないか、という心配は、上の形(引数を渡して関数として呼ぶ)では起きません。require('yargs') を 2 回呼んで片方にだけ option() を足しても、その既定値がもう片方の解析結果に現れることはありませんでした。yargs のドキュメントが yargs/yargs の形を案内しているので、版を上げられるならそちらへ寄せるほうが素直ですが、この書き換えのために設定が混ざることは確認していません。

ESM 側から使っている場合は影響を受けません。import yargs from 'yargs/yargs' は exports の import 条件を通って yargs.mjs に当たります。

なぜ Node.js 26 で出るのか

Node.js は、ファイルを CommonJS として読むか ES Module として読むかを、拡張子と最も近い package.json の "type" から決めます。ここで問題になるのが拡張子の無いファイルです。

yargs 17.7.2 の package.json は、パッケージ全体を "type": "module" と宣言したうえで、yargs/yargs というサブパスの CommonJS 側を拡張子の無いファイルに向けていました。

"exports": {
  "./yargs": [{ "import": "./yargs.mjs", "require": "./yargs" }, "./yargs"]
}

require 条件の行き先 ./yargs に拡張子がありません。このファイルの中身は CommonJS です。

const {applyExtends, cjsPlatformShim, Parser, Yargs, processArgv} = require('./build/index.cjs')

もともと Node.js には、"type": "module" のパッケージの中にある拡張子の無いファイルでも、require() 経由で読み込まれたときだけ CommonJS として扱う、という例外がありました。この例外を外して拡張子の無いファイルにもパッケージの "type" を適用する変更が 25.7.0 に入り、25 系ではいったん例外が戻され、26.0.0 からは戻されていません。その結果、このファイルは ES Module として評価され、ESM スコープに存在しない require を参照した時点で ReferenceError になります。

Node.js 26 のドキュメントが「CommonJS として読む」条件に挙げる拡張子なしファイルは 2 つだけで、"type": "module" のパッケージ内という条件は含まれていません。

Files with a .js extension or without an extension, when the nearest parent package.json file contains a top-level field "type" with a value of "commonjs".

Files with a .js extension or without an extension, when the nearest parent package.json file doesn’t contain a top-level field "type" or there is no package.json in any parent folder; unless the file contains syntax that errors unless it is evaluated as an ES module.

外れたのは拡張子が無い場合だけで、同じリストの最後の項目には別の例外が残っています。

Files with an extension that is not .mjs, .cjs, .json, .node, or .js, when the nearest parent package.json file contains a top-level field "type" with a value of "module".

"type": "module" のパッケージの中でも、.mjs / .cjs / .json / .node / .js のどれでもない拡張子を持つファイルは CommonJS として読まれます。node_modules の中に置いた dep.custom は Node.js 24.19.0 でも 26.7.0 でも require() できて、拡張子の無い dep だけが 26.7.0 で ESM 扱いになります。変わったのは、拡張子を持たないファイルの扱いです。

上流の経緯も追えます。拡張子なしファイルに "type" を効かせる変更は nodejs/node#61600 で入り、yargs が壊れたという報告が #61971 に立ちました。#62083 が例外を復活させていますが、対象は v25.x だけで、26 系では元の変更が残っています。ドキュメントから例外の記述を消したのが #62176 です。

yargs 17.7.3 が直したのは、この解決先です。

"exports": {
  "./yargs": [{ "import": "./yargs.mjs", "require": "./yargs.cjs" }, "./yargs.cjs"]
}

yargs.cjs を追加し、require 条件をそこへ向けました。拡張子の無い yargs は 17.7.3 にも残っていますが、ファイル自身のコメントが用途を書いています。

// This file gets hit in node versions below node 12.17.0
// when require('yargs/yargs') from CommonJS
// because conditional exports not fully supported and treats as filename.

module.exports = require('./yargs.cjs');

つまり、ファイルが消えたのではなく、現行の Node.js が解決しなくなりました。

版の境界

require('yargs/yargs') を yargs 17.7.2 で実行した結果です。

Node.js結果
22.23.2通る
24.19.0通る
25.6.0通る
25.7.0ReferenceError
25.8.0ReferenceError
25.8.1通る
25.9.0通る
26.0.0ReferenceError
26.7.0ReferenceError

25.7.0 で落ちるようになり、25.8.1 で戻り、26.0.0 で再び落ちます。25 系での復活は 25 系のためだけのもので、26 系には入っていません。Node.js 25 の新しい版で通ったことは、26 で通る根拠になりません。

yargs 側の境界は次のとおりです(Node.js 26.7.0 で実行)。

yargs結果
16.2.0ReferenceError
16.2.2通る
17.7.2ReferenceError
17.7.3通る
18.0.0通る
18.1.0通る

Node.js 24.19.0 では 17.7.2 と 17.7.3 のどちらも通ります。

**yargs 16 系を使っている場合は 16.2.2 で直ります。**16.2.2 は 17.7.3 と同じ直し方をしていて、exports の require 条件が ./yargs.cjs を指します。17 系へ上げなくてもこのエラーは消えるので、16 から 17 への移行を別途抱える必要はありません。

説明行が出ない

同じ ReferenceError でも、.js のファイルが "type": "module" で ESM 扱いになったときは、Node.js が理由を続けて出します。

This file is being treated as an ES module because it has a '.js' file extension and '/app/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.

拡張子の無いファイルでは、この行は出ません。エラー本文だけを読むと「どこかの .js を .cjs に直せばよい」という方向に見えますが、.cjs へのリネームを提案できる相手がそもそも無く、対象は node_modules の中のファイルです。

同じ理由で、createRequire も届きません。あれは呼び出し側に require を用意するもので、読み込まれる側がどの形式として評価されるかは変えないからです。一方、自分のファイルを ESM に移せるなら、import yargs from 'yargs/yargs' は Node.js 26 でも通ります——exports の import 条件が yargs.mjs を指しているためで、これは yargs の版を動かせないときの選択肢になります。自分の呼び出し側を CommonJS のまま保つ場合の答えが、上の 17.7.3 以上への更新です。

切り分け

  • 落ちているファイルのパスを見てください。 node_modules の下なら、動かす対象は依存パッケージの版です。手元に置いた .js や .mjs のパスが出ているなら、原因は別で、require is not defined in ES module scope の直し方 が扱っている条件("type": "module" や .mjs、ESM 構文の自動検出)に当たります。
  • npm ls yargs の結果が 17.7.3 以上なのに落ちる場合、別のパッケージが同じ形を持っている可能性があります。条件は「package.json に "type": "module" があり、exports の require 条件が拡張子の無いファイルを指している」ことです。スタックトレースの 1 行目のパスが、その相手を名指ししています。
  • 拡張子の無い実行ファイル(bin/tsc や bin/terser など)は今回の変更とは別です。 エントリポイントとして実行される拡張子なしファイルの扱いは Node.js 24 と 26 で変わっておらず、terser 5.50.0 と TypeScript 5.9.3 はどちらの版でも起動します。変わったのは require() で解決したときの扱いです。
  • 自分のリポジトリではなく、実行したツールの側で起きることもあります。 marp-cli(#708)、quint(#1926)、c8(#582)のように、yargs を依存に持つツールの側へ同じ内容が報告されています。ツール側が yargs を上げた版を出していればそれに更新するのが本筋で、待てない場合の手当てが上の npm update yargs です。

検証(machine-verified)

この修正は node@sha256:4ebb5ace66f15a24c14c492e01a8beeed4fddf970a856109f5126e703e5fe503 のバージョン固定コンテナ内で再現し、修正後に ReferenceError: require is not defined in ES module scope, you can use import instead のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs node/yargs-extensionless-require-node26
● reproduce require is not defined in ES module scope present ✓
● apply fix exit 0
● re-run require is not defined in ES module scope gone ✓
PASS verified · node@sha256:4ebb5ace66f15a24c14c492e01a8beeed4fddf970a856109f5126e703e5fe503 · signature gone

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