Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Node.js+TypeScriptにおけるCJS/ESM相互運用の最新ポイント

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

 Node.js+TypeScriptにおけるCJS/ESM相互運用の最新ポイント

TSKaigi 2026 Day2での発表内容となります。
※ 一部Jestの項の記述に誤りがあったため当日の内容より修正しています

Avatar for Naoki Kiryu

Naoki Kiryu

May 23, 2026

Other Decks in Technology

Transcript

  1. CommonJS (CJS) // モジュールの読み込み const someLib = require('some-lib') // モジュールの公開

    module.exports = { myFunc } ES Module (ESM) // モジュールの読み込み import someLib from 'some-lib' // モジュールの公開 export function myFunc() { ... } 今回は CJSからESMをrequireする際に生じていた問題についてお話しします CJS/ESMってなんだっけ? 3
  2. 前提: アプリケーションがCJSの場合… TypeScriptでは、 import はトランスパイル時に require になる // TypeScript import

    { GraphQLClient } from "graphql-request"; ↓ トランスパイル // JavaScript (CJS) const { GraphQLClient } = require("graphql-request"); ライブラリもCJSで書かれているうちはこれで問題ないが… 事象の振り返り 5
  3. ライブラリのバージョンをあげると・・・ ライブラリがESM-onlyとなり、無慈悲にもエラーに(CJSからESMをrequireできないため) Error [ERR_REQUIRE_ESM]: require() of ES Module /.../node_modules/graphql-request/build/entrypoints/main.js Instead

    change the require of main.js in /.../req.cjs to a dynamic import() which is available in all CommonJS modules. at Object.<anonymous> (/.../req.cjs:1:13) { code: 'ERR_REQUIRE_ESM' } ほならdynamic import()に変えるか・・・ 事象の振り返り 6
  4. dynamic importきついぜ・・・ 全部非同期で汚染される 実体はdynamic import()、型はimport typeでimportしないといけない import type ... withという謎の構文を知る

    なんでこんなに苦労しなくちゃいけないんだ・・・ import type { GraphQLResponse } from "graphql-request" with { "resolution-mode": "import" }; (async () => { const { GraphQLClient } = await import("graphql-request"); // ... }); 事象の振り返り 7
  5. 基本的にはTop-Level Awaitのせい ESM は モジュールのトップレベルに await を書ける 一方、CJSの require は同期実行

    ESMに await が含まれていたら詰む このため、Node.jsチームはESMの require を一律で禁止にした 以降、ESMをrequireできるようにする試みもあったが、ESMの非同期性を理由に断念してい る 原因について 8
  6. ちなみに、パッチを作成された方のブログにて、より細かな話が色々と書いてあり参考になり ます。 require(esm) in Node.js | Joyee Cheung's Blog require(esm)

    in Node.js: implementer's tales | Joyee Cheung's Blog require(esm) in Node.js: from experiment to stability | Joyee Cheung's Blog require(esm)への道のり 11
  7. TS 5.8以降にアップデートし module=nodenext を指定すればOK さらにTS5.9では module=node20 も追加(挙動は同じ) module=node16 (TS 5.7以前の

    nodenext ) では依然として型エラー error TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("graphql-request")' call instead. To convert this file to an ECMAScript module, change its file extension to '.mts' or create a local package.json file with `{ "type": "module" }`. 1 import { GraphQLClient, ClientError } from "graphql-request"; ~~~~~~~~~~~~~~~~~ TypeScript側の対応 12
  8. およそ5年の時を経て、ついに相互運用の悩みから解放 日付 出来事 2019-11-21 Node.js 13.2 で --experimental-modules がデフォルト有効 2022-05-24

    TypeScript 4.7 で module=node16 / nodenext 追加 2024-12-03 Node.js 22.12 (および 20.19) で require(esm) がデフォルト有効 2025-02-28 TypeScript 5.8 で module=nodenext が require(esm) 相当の挙動に 年表で振り返ると 13
  9. パッケージで以下のように CommonJS 環境用にCJSフォールバックが設けられている場合: { "name": "some-lib", "exports": { ".": {

    "import": "./dist/index.mjs", "require": "./dist/index.cjs" } } } require('some-lib') と import * as somelib from 'some-lib' は別インスタンスとなる (同じクラスのinstanceofがfalseになるなど) 推移的な依存(CJSのライブラリから間接的にsome-libがrequireされるなど)の場合に問題にな る dual-package hazardとは 16
  10. この挙動は require(esm) に対応したNode.jsでも発生する import は require(esm) の対象とならない この問題を解決するには module-sync を追加すると良い

    同期的なESMであることを明示 これは require(esm) からも利用される { "name": "some-lib", "exports": { ".": { "module-sync": "./dist/index.mjs", "import": "./dist/index.mjs", "require": "./dist/index.cjs" } } } dual-package hazardとは 17
  11. Jest: 直接は無関係 そもそもネイティブの require を利用しておらず影響がない Node.js 24.9 + Jest 30.4.0

    にてJest独自の require(esm) を利用可能 --experimental-vm-modules が必要 それ以前のJestではCJSテストファイルからのESMの require は不可 ts-jestのtransform対象に含めればCommonJSにトランスパイルされるので動く transformIgnorePatternsに /node_modules/(?!(esm-pkg)/) のように指定 Vitest: 効果あり (そもそもCommonJSの文脈でVitestを使うことがあまりないかもしれませんが) node_modulesはrequireで読み込まれるので、 require(esm) の効果あり テストランナーでのESMサポート 18
  12. まとめ 基本は Node.js 20 以降 + TS 5.8 以降を使えばOK dual-package

    hazardの可能性を考慮に入れておく テストランナーでの互換性問題は、Vitestは解決するがJestは影響を受けない まとめ 19