はじめに
NestもNextもLintも最新がイケてる!
そう思って、全部最新版にしてMonorepoで始めたら…ESModuleの地獄がやってきました。
この記事では、実際にやってみて起きたエラーや構成の罠、そして「なぜMonorepoだと余計に爆発するのか?」を振り返ります。
✅ 前提構成
-
Monorepo構成(pnpmワークスペース)
-
apps/web
(Next.js 13+, App Router) -
apps/api
(NestJS 10+) -
packages/shared
(型共有・ユーティリティ)
-
- 全体でESLint / Prettier / Jest / tsconfigを統一
-
type: "module"
を設定し、ESModuleベースに寄せた構成
💥 そして起きたESModule系のエラーたち
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module
SyntaxError: Cannot use import statement outside a module
Cannot find module 'tsconfig-paths/register'
Parsing error: Cannot read config file .eslintrc.js: Unexpected token export
Jest: Unexpected token 'export'
💣 なぜ爆発したのか?振り返ってわかったこと
1. Monorepoは「複数のmodule形式を共存させようとする」構成になりやすい
パッケージ | 想定形式 |
---|---|
apps/web |
ESM(Nextが前提) |
apps/api |
CommonJS(Nestはまだ主にCJS) |
packages/shared |
両方からimportされる → どちらにも合わせづらい |
→ この shared
パッケージが ESMとCJSの狭間で爆発しました。
2. tsconfig の継承地獄
- ルートの
tsconfig.json
にmodule: "ESNext"
を指定 - Nest側では
ts-node
が__dirname
やrequire()
で爆死 - Next側では
.js
拡張子必須で警告が出る
→ tsconfig.base.json
を共通で使うのが逆に毒になった
3. ツールごとにESM対応がバラバラすぎる
ツール | ESM対応状況 |
---|---|
ts-node |
△ フラグつければ可(--esm ) |
jest |
❌ デフォはCJS前提(babel-jest で対応) |
eslint |
△ 設定によって壊れることも |
tsconfig-paths |
❌ ESM非対応 |
🔧 対処としてやったこと
-
apps/api
の NestJS はtype: "commonjs"
に戻した(正直諦めた) -
apps/web
は ESM維持(Nextがそうなので) -
packages/shared
はexports
フィールドを定義して両対応っぽくした(が微妙) -
tsconfig
はパッケージごとに分割し、module
とtarget
を個別管理 -
ts-node
は--esm
フラグを明示して対応 -
jest
はbabel-jest
に切り替えて一部解決
🧠 学んだこと
気付き | コメント |
---|---|
「全部ESMで揃える」はまだ無理がある | ライブラリ側の対応が不完全すぎる |
Monorepoだとmodule形式の衝突が表面化する | 単体構成だと隠れていた問題が全部出る |
最初からtsconfigを分けるのが吉 |
tsconfig.app.json , tsconfig.build.json など必須レベル |
Jestとts-nodeのCJS依存がネック | テストランナーは選定を見直した方がいいかも |
✅ 結論
Monorepo構成 × 全部最新版 × ESM移行 は理想だけど、現実ではまだ厳しい。
特に NestJS
+ ts-node
+ Jest
+ ESLint
+ Next
のように、異なるmodule形式が混在する環境では、
一気にESM化しようとすると、必ずどこかが爆発する。
🙌 最後に
Node.js界隈がESMに本気で寄せ始めてるのは間違いない。
だけど、現場で全部揃えるのはまだ修行フェーズ。
特にMonorepoでは、「混在をうまく許容する構成設計」が大事。
同じように「全部最新版で爆死しかけた」人の参考になればうれしいです。