1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ローカルでは動くのに本番だけ500エラー。原因はNode.jsのESM importだった

1
Posted at

導入

最近、AIエージェントに何でも任せられるようになった、みたいな景気のいい話をよく
目にします。一方で私が最近つまずいたのは、そういう華やかな話とは真逆の、地味な
import文の書き方でした。世の中どんどん便利になっていくのに、自分は../shared/types
の後ろに.jsをつけるかどうかで丸一日悩んでいたわけです。

目的

個人開発しているニュース収集アプリLoki-Huginnには、毎日自動でRSSフィードを
取りに行って記事を整形・保存する処理があります。ユーザーがアプリを開いたときでは
なく、誰も見ていなくても毎日決まった時間に動く必要があったので、Vercel Functionsで
Node.jsのサーバーサイドコードとして書き、Vercelの定期実行(Cron)で1日1回叩く
構成にしました。集めた記事はVercel Blobに保存し、フロントエンドはそれを読みに
行くだけの薄いつくりです。

実装

RSS取得・タグ付け・重複除去のロジックを書き、ローカルで何度も動作確認しました。
型チェックもビルドも通っていたので、そのままデプロイしました。

ところが本番環境で該当の処理を呼び出すと、こういうエラーで落ちていました。

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../shared/types' imported from '.../server/collect.js'

ローカルでは再現しません。ビルドも型チェックも通っています。それなのに本番では
毎回落ちます。この時点では、何が原因か見当がつきませんでした。

環境変数の設定漏れかと思って確認、違いました。依存パッケージのバージョン差かと
思って確認、これも違いました。Node.jsのバージョンを揃えても変わりません。

手がかりはVercelのランタイムログでした。ERR_MODULE_NOT_FOUNDというエラー
コードと、見つからなかったファイルパスがそのまま書いてあります。ここで
「ビルドやテストでは検出できない、実行時特有の何かが起きている」と気づきました。

コードでは、こういう拡張子なしの相対importを書いていました。

// server/collect.ts
import { Article } from '../shared/types';

型チェックもViteの開発サーバーも、これで普通に通っていました。私はこのとき
初めて、「型チェックが通る」ことと「実際に実行できる」ことはイコールではない、
という話を実感することになりました。

tsconfig.jsonではmoduleResolution: "bundler"という設定を使っています。この
設定だと拡張子なしの相対importでも型チェックが通ります。ローカルの開発サーバーは
Viteが担っていて、Viteはバンドル時に拡張子の省略をよしなに解決してくれます。
つまり「型チェックが通る」も「ローカルで動く」も、Viteが裏で肩代わりしてくれて
いただけで、実行時のモジュール解決が正しいかどうかとは関係ありませんでした。

Vercel Functionsの実行環境は素のNode.js(ESM)です。Node.jsのESMローダーは
CommonJSのrequireと違い、相対importの拡張子省略を許していません。.js
読み込むつもりなら、ソースが.tsでも、ビルド後の.jsという拡張子を書く
必要があります。

api/server/shared/配下の相対importすべてに.js拡張子を付けました。

// 修正後
import { Article } from '../shared/types.js';

ソースが.tsなのに.jsと書くのは最初違和感がありましたが、「ビルド後に
実際に存在するファイル名」だと思えば納得できます。フロントエンド側(src/
配下)はViteがビルド・バンドルを両方担うので対象外です。影響が出るのは、
Node.jsが素のESMとして直接実行するapi/server/配下だけでした。

反省・教訓

「ローカルで動く」「型チェックが通る」を、本番でもそのまま動く保証だと思い
込んでいたのが今回の反省点です。ビルドツールとランタイムでモジュール解決の
ルールが違う、というだけの地味な話でしたが、似たようなことは他にもありそうだ
なと思っています。

あとはやっぱり、エラーメッセージは最後まで読んだほうがいいなと。ERR_MODULE_ NOT_FOUNDという文字列とファイルパスさえ見えていれば、もう少し早く気づけた
はずです。

このルールはプロジェクトの開発ガイドラインに書いておきました。次に同じ構成の
コードを書くときの自分向けのメモです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?