← 回首頁

人人都在引用的那兩個 ESM/CommonJS 錯誤已經沒了,沒人提的那一個還在

現在 require() 一個 ESM 模組會成功,TypeScript 也不再抱怨。真正還會壞的是雙格式套件——而且它的症狀是 instanceof 回傳 false,不是丟出錯誤。

兩條粗管線在畫面中央天衣無縫地對接,一條是溫暖的米白色、一條是冷調的淡藍色,下方掉出兩個形狀相同、分別帶著這兩種顏色的方塊。

搜一個 ESM/CommonJS 的錯誤,你會拿到上千個講得很有把握的答案。它們大部分是對的 ——只是對的是三年前的 Node。而它們給你的那個具體做法,修的是一個你現在沒有的問題。

那個最有名的錯誤已經不會發生了

在 Node v25.9.0 上,用真的套件、真的 package.json 實測:require() 一個 ESM 模組會成功。沒有 ERR_REQUIRE_ESM。

而且你拿到的是模組的 namespace,不是 default 匯出:

keys: Token, __esModule, default, name
m[Symbol.toStringTag] === 'Module'

這個區別比大家記得的那個修法更重要。從舊世界搬過來的程式碼會去拿 .default, 而現在的 .default 是那個模組的 default 匯出、跟具名匯出並排放著的一個屬性—— 通常不是它要的東西。

TypeScript 那個也一樣

TS1479——the current file is a CommonJS module whose imports will produce ‘require’ calls; however, the referenced file is an ECMAScript module——是同一套傳說的編譯期版本。

在 TypeScript 6.0.3、module: nodenext 之下,一個 .cts 檔匯入 .mjs 檔, 完全沒有任何診斷。為了確認那個檔案真的有被檢查,我先在裡面放了一個故意的型別錯誤:

main.cts(2,14): error TS2322: Type 'number' is not assignable to type 'string'.

對照組有跳,互載錯誤沒有。

那現在真正會壞的是什麼

Node 實際上會做什麼
負責載入的那一方

實際拿到什麼

keys: Token, default, name  ·  m.default === 'i-am-a-property-called-default'

錯誤訊息,逐字

沒有錯誤。這一格是成功的。

ESM 裡的 CJS 全域變數

  • ReferenceError: __dirname is not defined → import.meta.dirname
  • ReferenceError: __filename is not defined → import.meta.filename
  • ReferenceError: require is not defined → module.createRequire(import.meta.url)
  • ReferenceError: module is not defined → 沒有直接的替代品
  • ReferenceError: exports is not defined → 沒有直接的替代品

每一格都於 2026-08-07 在 Node v25.9.0 上用真的套件實測。

上面每一格都是跑出來的,不是查來的。其中三格值得細看。

新的失敗是 top-level await

require() 一個模組圖裡含有 top-level await 的 ESM 模組,你會拿到:

require() cannot be used on an ESM graph with top-level await. Use import() instead.
To see where the top-level await comes from, use --experimental-print-required-tla.

這是一個好錯誤:它說出了原因、給了做法,還附一個能幫你找出兇手模組的旗標。 它也是你在 2026 年真的會遇到的那一句——所以這裡是逐字照抄,不是改寫。

從 CommonJS 具名匯入行不行,取決於原始碼怎麼寫

Node 是靠靜態分析原始碼找出 CommonJS 的具名匯出。一個在執行期才算出鍵名的匯出, 它看不見:

const key = ['com', 'puted'].join('');
module.exports[key] = '…';

於是 namespace 裡只剩 default 和一個字面上叫 module.exports 的鍵, 具名匯入失敗:

SyntaxError: Named export 'computed' not found. The requested module 'cjs-dynamic' is a
CommonJS module, which may not support all module.exports as named exports.

注意那個 may not。這不是一條關於 CommonJS 的規則,這是一句關於「靜態分析器在那個 特定檔案裡看得到什麼」的陳述。所以同一種匯入寫法對某個套件可行、對另一個不行—— 在你知道它到底在看什麼之前,那看起來像是隨機的。

雙格式套件的陷阱還在

一個同時提供兩個入口的套件,可能在同一個行程裡被載入兩次:

require(pkg).Token === (await import(pkg)).Token   →  false
instance instanceof otherToken                     →  false

兩個 class、同一份原始碼、不同的身分。而且不會有任何東西丟出來。 你的 instanceof 只是回傳 false,你的錯誤型別比對開始比不中, 而症狀出現的地方離原因很遠。

這一個是活下來的那個。人人引用的那兩個錯誤之所以被修掉,是因為它們很吵、 立刻擋住所有人。這一個很安靜,所以它還在。

全域變數,順便補完

在 ESM 裡,五個 CommonJS 全域變數全部丟同一種錯:

ReferenceError: __dirname is not defined

__filename、require、module、exports 也一樣。替代品是 import.meta.dirname 與 import.meta.filename(兩者都是字串),真的需要 require 時用 module.createRequire(import.meta.url)。

這篇該帶走的東西

有用的習慣不是背下哪一種組合可行,而是意識到這個主題腐壞得比你會去搜尋的幾乎任何 東西都快,以及一個被大量按讚的答案,是一份關於過去的證據。

在你因為讀到什麼而準備重構一個套件之前:拿那兩行能重現問題的程式碼, 在你真正要出貨的那個 Node 上跑一次。2026 年有一半的機率那個問題已經不存在了, 而你原本要做的那個改動,只會多疊一層什麼都沒防到的相容性墊片。

常見追問

ERR_REQUIRE_ESM 真的不見了嗎?

對一個普通的 ESM 模組來說是的——在 Node v25.9.0 上實測,require() 一個 ESM 套件 會成功。你拿到的是模組的 namespace,所以具名匯出直接掛在你收到的那個物件上, 而不是在 default 底下。但如果模組圖裡有 top-level await,它仍然會失敗, 而且是另一個錯誤。

require() 一個 ESM 模組會回傳什麼?

namespace 物件,Symbol.toStringTag 是 "Module",並且被加上一個 __esModule 標記。 照舊世界寫法去拿 .default 的程式碼,會拿到那個模組的 default 匯出—— 而那通常不是它想要的東西。

為什麼從 CommonJS 套件具名匯入有時候會失敗?

Node 是靠靜態分析原始碼來偵測 CommonJS 的具名匯出,所以一個在執行期才算出鍵名的 匯出,它看不見。錯誤訊息寫的是這個模組「may not support all module.exports as named exports」——是 may not,因為行不行取決於那個模組的原始碼怎麼寫。

什麼是雙格式套件的陷阱(dual package hazard)?

一個同時提供 CommonJS 與 ESM 入口的套件,可能在同一個行程裡被載入兩次, 兩個入口各一次。兩份副本沒有共用身分,所以其中一份的 class 與另一份的不是同一個, 拿去做 instanceof 會回傳 false。而且不會有任何東西丟出來——那個檢查只是安靜地說不。

ESM 裡用什麼取代 __dirname?

import.meta.dirname,__filename 則用 import.meta.filename。在 ESM 裡直接參照 __dirname、__filename、require、module 或 exports,會丟出一個普通的 ReferenceError, 說那個名字沒有被定義。