CJS からでも import() を使えば ESM モジュールを読み込めますが 非同期処理になります
CJS ではトップレベルの await ができないので ESM のみのパッケージが入るだけであちこちで非同期処理が発生してとても使いづらいです
中にはこれまで CJS にしていたのにアップデートで ESM のみになるパッケージもあります
あるパッケージの最新の機能を使いたいのでアップデートしたら とても困ったことになりました
パッケージの読み込み等はすべて同期処理を前提として作っているものだったので まともに対応するなら大幅な作り直しが必要になります
まともに対応なんてやってられないので別の方法で回避することにしました
一つの方法は CJS 化すること
これまで CJS だったのですから こっちで CJS に変換してから使います
Rollup などを使います
ただこれには問題があって 対象が依存パッケージにも及びます
「MyPackage → A → B」 のような依存関係になっているとき A が ESM のみだと B も ESM のみの可能性があります
一つのプロジェクトでパッケージを分割してるような場合だと A の依存関係にも ESM のみパッケージが含まれるケースが多いです
自分のパッケージからの直接の依存関係である A だけを CJS 化しても A から B をインポートするときに require だとインポートできないというエラーになります
ESM パッケージすべての変換が必要ですが 多くなってくると 1 つ 1 つを CJS 化するのは大変です
手抜きをするなら A の CJS 変換時に依存関係も解決してバンドルすることです
A の変換済み CJS モジュールには A も B も含みます
楽にはなりますが B のパッケージが複数の A からインポートされる場合に A ごとに異なる B が存在することになります
同じパッケージを別パッケージとして複数回読み込むことになるのでムダが多いです
さらにキャッシュなどで状態を持つモジュールがあると 別モジュールとみなされることで動作に影響する場合もあります
class なら instanceof を使った判定を行うケースもあり 別モジュールだと実体が違うので別物とみなされ意図した動作にならない場合もあります
この点ではパッケージ 1 つ 1 つを CJS 化した方が良いです
別の方法では CJS 化せずそのまま使います
問題は import() によって読み込みが非同期化することなので 事前にすべての読み込みを終えてからメインの処理を実行することで非同期読み込みを不要にします
エントリポイントのスクリプトが index.js なら boot.js を追加してエントリポイントをこっちに変更します
boot.js はこんな感じにします
const esm_modules = require("./esm_modules.js")
!async function() {
const modules = [
"mod1",
"mod2",
// ...
]
const loaded = await Promise.all(modules.map(name => import(name)))
for (const [idx, name] of modules.entries()) {
esm_modules[name] = loaded[idx]
}
require("./index.js")
}()
esm_modules という空のモジュールを用意して ここを import 済み ESM モジュール置き場にします
ESM のパッケージをすべて ここで import() します
読み込みが終われば esm_modules のプロパティに保持します
これらの処理が終わってから require で本来のエントリポイントの index.js を実行します
その他モジュールでは ESM のみパッケージを require するところを esm_modules の require に置き換えます
const { mod1 } = require("./esm_modules.js")
index.js 以降の処理では ESM のみパッケージは変数に保持されているので非同期処理が不要になります
多少のコードの修正は必要ですが 非同期対応に比べれば遥かに簡単な修正で済みます