Open1

【Google Cloud/GCP】Cloud Run上で Path alias (@) 設定がエラーになるのは、なぜか?

まさぴょん🐱まさぴょん🐱

Cloud Run上で、Path alias 設定がエラーになるのは、なぜか?

まとめ (最初に結論)

tsconfig.json に書いた baseUrl / pathsTypeScript が型を解決するためのヒントであって、tsc はコンパイル後の JavaScript の import 文を 変更しません (TypeScript)。
ローカル開発では ts-node -r tsconfig-paths/register などがランタイムでエイリアス解決を肩代わりしますが、Cloud Run のコンテナでは最終的に node dist/...js が素の Node.js として起動するため、 @/xxx のようなエイリアスを パッケージ名「@」 と解釈してしまい Cannot find module で落ちます (Stack Overflow, GitHub)。これが Cloud Run 上だけでエラーになる根本原因です。


1. なぜランタイムで解決できないのか

1.1 tsc は import 文を書き換えない

公式ドキュメントにある通り、paths は “他のツールが実際に書き換える/バンドルする前提で使うもの” (TypeScript)。従って tsc の出力には @/… がそのまま残ります。

1.2 Node.js 側の仕様

  • ESM では拡張子必須 — 相対・絶対パス指定のときは .js 等が無いと解決しません (Node.js)
  • ベア・スペシファイア扱い@/utils はパッケージ解決に回り、node_modules/@ を探しに行くため失敗します (Stack Overflow)
  • NODE_PATH は ESM 解決に使われません (Node.js)

2. Cloud Run で動かす 4 つの代表的な解決策

手段 仕組み メリット デメリット
① tsc-alias で後処理 tsc && tsc-alias で出力 JS の import を相対パスに書き換え 導入が最小限 post-build が必須
② バンドラ (esbuild/tsup/Vite) でバンドル エイリアスをバンドラ側で解決して 1 ファイル or ツリーを生成 依存解決・縮小も一括 設定が増える
③ module-alias を runtime で require import 'module-alias/register' が Node の Module._resolveFilename をパッチ (Medium) 既存 import を変更しない ランタイムオーバーヘッド
④ Node 20+ “imports” フィールド package.json に<pre>"imports": { "#/": "./dist/" }</pre>を宣言し、import "#/foo.js" 形式で呼ぶ (Node.js) 標準機能のみ #/ 記法に書き換えが必要

最も手軽: tsc-alias かバンドラ。両者ともビルド済み JS を確実に書き換え、Cloud Run の起動コマンドを単純な node dist/index.js にできます。


3. 例: tsc-alias で直す最短パス

// package.json
{
  "scripts": {
    "build": "tsc && tsc-alias",   // ★追加
    "start": "node dist/index.js"
  },
  "devDependencies": {
    "typescript": "^5.4.0",
    "tsc-alias": "^1.9.0"
  }
}
# server/Dockerfile(抜粋)
RUN pnpm run build          # ← dev ではなく build を実行
CMD ["node", "dist/index.js"]

tsc-alias はビルド後に @/foo../../foo.js 等へ一括置換します (npm)。


4. ローカル開発と同等にする場合

ローカルで Hono の dev サーバpnpm dev などで起動している場合、
ts-node -r tsconfig-paths/register src/index.ts が走っているはずです (Stack Overflow)。
Cloud Run コンテナでも同等に動かしたいなら 本番も ts-node を使う という手もありますが、

  • コールドスタートが遅い
  • ts-node 用のネイティブ依存とメモリが増える

という理由で推奨されません。ビルドしてから起動する構成の方がコンテナ 起動時間=ユーザ待ち時間 を短くできます。


5. よくあるハマりどころチェックリスト

  1. strictNullCheckscompilerOptions 外にある
    → これは無視されるがビルドは通る。気持ち悪ければ中へ移動。
  2. エイリアスの前に @/ を書き、拡張子を付け忘れている
    → ESM モード (module:"NodeNext") では .js が無いと実行時に落ちる (Node.js)。
  3. Dockerfile で pnpm dev のまま
    → 本番はビルド済み JS を実行するよう CMD を切替える。
  4. Cloud Run のデプロイ コマンド
    gcloud run deploy --source . を使う場合は Cloud Build が自動で npm build を呼ばない。Dockerfile 内に RUN pnpm run build を必ず書く。

6. 参考資料

  • Stack Overflow – Typescript path aliases not resolved correctly at runtime (Stack Overflow)
  • Stack Overflow – baseUrl / paths は型解決用のみ (GitHub)
  • TSConfig docs – paths は emit しない旨 (TypeScript)
  • Node.js Docs – file 拡張子必須 (ESM) (Node.js)
  • Node.js Docs – package.json "imports" フィールド (Node.js)
  • Medium – module-alias でランタイム解決 (Medium)
  • tsc-alias パッケージ (npm)
  • Reddit – ビルド後に alias が解決されず落ちる例 (Reddit)
  • Reddit – tsc-alias で解決した報告 (Reddit)
  • Node.js Docs – NODE_PATH は ESM に効かない (Node.js)

これで Cloud Run 上でも @/ エイリアスを安全に使えるはずです。試してみてください!