Nuxt 3 アプリを Cloudflare Pages にデプロイしようとしたとき、以下のようなビルドエラーに遭遇することがあります。
⠙ Vite server warmed up in 2376ms
Error: ENOTDIR: not a directory, scandir '/opt/buildhome/repo/node_modules/whatwg-url/dist/URL.js'
at Object.readdirSync (node:fs:1522:3)
at createResolver (file:///opt/buildhome/repo/.nuxt/nitro/index.mjs:32:27)
...
このエラーは whatwg-url や tr46, webidl-conversions といったパッケージがビルド時に正しく解決されず、Cloudflare Pages のビルド環境(Wrangler + Workerd)で ENOTDIR として失敗することが原因です。
本記事では、この問題の背景、再現条件、原因分析、そして複数の回避策を紹介します。
1. エラーの発生条件と背景
発生しやすいケース
- Nuxt 3(3.8〜3.13系)を使用
nitro.presetにcloudflare-pagesまたはcloudflareを指定whatwg-urlを直接ではなく、node-fetch,undici,@auth/coreなどを通じて間接的に利用している
Cloudflare Pages のビルド環境は、標準的な Node.js サーバーではなく Workerd(Cloudflare Workers runtime) に最適化された Nitro 出力を生成します。その際、Nitro が依存パッケージをバンドル・解析する過程で whatwg-url の package.json と dist/ ディレクトリ構造が想定外になり、ENOTDIR が発生することがあります。
2. 原因の分析
whatwg-url パッケージは Node.js で URL API の polyfill として広く使われていますが、バージョンによっては dist/URL.js を ESM として提供していない場合があります。
Nitro は依存関係をスキャンし、バンドル対象を解決する際に readdirSync でパスを辿りますが、Cloudflare Pages のビルド環境では一部パスが正しく展開されず ファイルをディレクトリとして扱おうとして失敗する、というのが典型的な原因です。
3. 再現例
例えば server/api/hello.ts で node-fetch を使ったとします。
import fetch from 'node-fetch'
export default defineEventHandler(async () => {
const res = await fetch('https://example.com')
return res.json()
})
これを Cloudflare Pages にデプロイすると、ビルド時に以下のログが出ます。
[vite] building for production...
Nitro build error:
Error [Error]: ENOTDIR: not a directory, scandir '/opt/buildhome/repo/node_modules/whatwg-url/dist/URL.js'
ローカルでは npx nuxi build に成功しても、Cloudflare Pages のビルドステップでは失敗する、というのが特徴的です。
4. 解決策・回避策
4-1. whatwg-url のバージョンを固定
package.json に明示的に whatwg-url の安定バージョンを追加します。
"dependencies": {
"whatwg-url": "13.0.0"
}
その後、rm -rf node_modules && npm install で再インストールし、ビルドを再実行します。
4-2. Nitro の externals 設定を調整
nuxt.config.ts に以下を追記して、Nitro に特定パッケージを外部化させず、バンドルに含めるようにします。
export default defineNuxtConfig({
nitro: {
externals: {
inline: ['whatwg-url', 'tr46', 'webidl-conversions']
}
}
})
これにより Cloudflare Pages 側での解決ミスが起きにくくなります。
4-3. Node.js 依存を undici に切り替える
Nuxt 3 では globalThis.$fetch がデフォルトで提供されており、node-fetch は不要です。サーバーサイドで HTTP リクエストを行う場合は $fetch を使い、依存を減らすとエラーを回避できることが多いです。
export default defineEventHandler(async () => {
return $fetch('https://example.com')
})
4-4. Cloudflare Pages の環境変数を確認
NITRO_PRESET=cloudflare-pages が設定されているかを確認します。手動で設定していない場合、cloudflare プリセットが選ばれてビルドが異なる挙動をすることがあります。
5. CI/CD での再現と防止策
GitHub Actions など CI/CD 環境で wrangler pages dev を使ったテストを追加することで、本番デプロイ前に問題を検出できます。
- name: Build Nuxt for Cloudflare Pages
run: NITRO_PRESET=cloudflare-pages npx nuxi build
また、pnpm dedupe や npm dedupe を使い、依存パッケージの重複を解消しておくとエラー発生率を下げられます。
6. まとめ
ENOTDIRエラーは Cloudflare Pages のビルド環境で依存パッケージ解決が失敗することで発生whatwg-urlやtr46のバージョン固定、Nitro externals 設定で回避可能- そもそも
node-fetchを使わず$fetchに移行するのが最もシンプルな解決策
Cloudflare Pages は軽量で高速なホスティング環境ですが、Node.js の挙動と完全互換ではないため、依存パッケージやバンドル設定には注意が必要です。問題が発生した場合は、まずローカルで NITRO_PRESET=cloudflare-pages を付けてビルドし、同じエラーが出るかを確認するとデバッグがスムーズになります。


