Nuxt 3 + Cloudflare Pages で whatwg-url パッケージ等のビルドエラー

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-urltr46, webidl-conversions といったパッケージがビルド時に正しく解決されず、Cloudflare Pages のビルド環境(Wrangler + Workerd)で ENOTDIR として失敗することが原因です。

本記事では、この問題の背景、再現条件、原因分析、そして複数の回避策を紹介します。

1. エラーの発生条件と背景

発生しやすいケース

  • Nuxt 3(3.8〜3.13系)を使用
  • nitro.presetcloudflare-pages または cloudflare を指定
  • whatwg-url を直接ではなく、node-fetch, undici, @auth/core などを通じて間接的に利用している

Cloudflare Pages のビルド環境は、標準的な Node.js サーバーではなく Workerd(Cloudflare Workers runtime) に最適化された Nitro 出力を生成します。その際、Nitro が依存パッケージをバンドル・解析する過程で whatwg-urlpackage.jsondist/ ディレクトリ構造が想定外になり、ENOTDIR が発生することがあります。

2. 原因の分析

whatwg-url パッケージは Node.js で URL API の polyfill として広く使われていますが、バージョンによっては dist/URL.js を ESM として提供していない場合があります。

Nitro は依存関係をスキャンし、バンドル対象を解決する際に readdirSync でパスを辿りますが、Cloudflare Pages のビルド環境では一部パスが正しく展開されず ファイルをディレクトリとして扱おうとして失敗する、というのが典型的な原因です。

3. 再現例

例えば server/api/hello.tsnode-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 dedupenpm dedupe を使い、依存パッケージの重複を解消しておくとエラー発生率を下げられます。

6. まとめ

  • ENOTDIR エラーは Cloudflare Pages のビルド環境で依存パッケージ解決が失敗することで発生
  • whatwg-urltr46 のバージョン固定、Nitro externals 設定で回避可能
  • そもそも node-fetch を使わず $fetch に移行するのが最もシンプルな解決策

Cloudflare Pages は軽量で高速なホスティング環境ですが、Node.js の挙動と完全互換ではないため、依存パッケージやバンドル設定には注意が必要です。問題が発生した場合は、まずローカルで NITRO_PRESET=cloudflare-pages を付けてビルドし、同じエラーが出るかを確認するとデバッグがスムーズになります。

タイトルとURLをコピーしました