以下の内容はhttps://let.blog.jp/tag/ESMより取得しました。


CJS を ESM に書き換え
関連
🔗 ESM のみのパッケージが不便
🔗 CJS を ESM に置き換えるのは難しい場合もあった

昔ながらのプロジェクトは相変わらず CJS ですが そろそろ ESM にしようと思って一部書き換えてます
CJS だと ESM のみのパッケージを使うときに 動的 import にするしかなくて非同期処理にせざるを得ないです
自分で Rollup で個別に変換してたときもありましたが 依存パッケージに ESM のみが増えていくとやってられないですし
要望が多くて CJS から同期処理でインポートする手段が提供されるかと思ったりもしてましたが 結局そういうのは入らなそうですし

ESM にしても関連のところに書いたような問題は出てくるのですが CJS から ESM パッケージをインポートするのよりはマシかなというところです

ブロックスコープ内でのエクスポート問題ですが これはひとつのモジュールに色々まとめ過ぎということもあったので ブロックごとに別モジュールにします
モジュールが少ないものだと 5 個が 10 個になるのは倍なので抵抗があったりもしましたが 全体が大きくなって数百もあれば 10 や 20 増えてもたいして気になりませんし 数行しかなくても別モジュールにわけて index.js 的な部分でまとめるようにします

ブロックが if 文なのは条件によってはエクスポートが undefined ということなので宣言的になるよう export const で条件演算子で分岐する形にします

動的な require が import になることで非同期になる問題があります
config ファイルを env の名前を使って読み込むときなどは名前が静的でないので動的 import にせざるを得ません
ですが今はトップレベル await があるので 実質同期的なように初期化できます

読み込み順が変わるので場合によっては問題になることもありますが ほとんどの場合は無視できます
詳しく書くとこういうケースです

/// index.js
import a from "./a.js"
import b from "./b.js"
console.log("I")

/// a.js
console.log("A")
await import("./a2.js")
export default "A"

/// a2.js
console.log("A2")
export default "A2"

/// b.js
console.log("B")
export default "B"

結果はこうなります

A
B
A2
I

これが require だと同期処理なので a.js を最初に読み込み a2.js も同期的に読み込まれます
そのあとに b.js が読み込まれるので順番は A → A2 → B → I です
import だと a.js と b.js は並列して取得してから順番に a と b を実行しますが a.js で非同期処理が入ると b.js に進みます
index.js のインポート部分が全部終わらないと index.js の本体の処理は始まりませんが index.js がインポートするモジュールは同時に処理されます

モジュール内で完結してるなら問題ないのですが トップレベルでグローバルに影響する処理をしたり 別モジュールの関数呼び出したりしていると 実行順で期待通りに動かないこともあります
トップレベルではできるかぎり関数等を定義するだけにして処理は行わないようにして 初期化処理が必要なら使う側で init みたいな関数を呼び出してもらい実行するようしたほうがいいかもですね

残る問題はたまにしか使わない機能なので動的に import する場合です
動的インポートなのでその関数が非同期になってしまいます
完全には避けられないので ロードする処理とモジュールを使う処理を分けて 後者の処理は同期処理に保つくらいしかできないです
ただ いつ最初に使うかわからないので 結局チェックしてロードする処理を挟む可能性が常にあって あまり意味がないです
その機能を呼び出す前の段階でその機能を有効にするようなフェーズがあるのなら そこでインポートしておくという使い方はできそうです

あとは 少し面倒な点で CJS のみ対応のライブラリのインポート時にプロパティを直接 named export とみなせません

/// module.cjs
module.exports = { foo: "bar" }

/// index.js
const { foo } from "./module.cjs"

これができません

const module from "./module.cjs"
const { foo } = module

という一手間が必要です
Node.js の組み込みモジュールはソースコード上は CJS なのにプロパティを直接参照できるのでなにか方法があるのかと思ったのですが なさそうでした
組み込みモジュールだからこそ特別な対応がされているのでしょうか
ESM のみのパッケージが不便
最近では ESM 版しか用意せず CJS 版がないパッケージもでてきているようです
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 のみパッケージは変数に保持されているので非同期処理が不要になります
多少のコードの修正は必要ですが 非同期対応に比べれば遥かに簡単な修正で済みます



以上の内容はhttps://let.blog.jp/tag/ESMより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14