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
を付けてビルドし、同じエラーが出るかを確認するとデバッグがスムーズになります。