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

Cloud Run上で、Path alias 設定がエラーになるのは、なぜか?
まとめ (最初に結論)
tsconfig.json
に書いた baseUrl
/ paths
は TypeScript が型を解決するためのヒントであって、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. なぜランタイムで解決できないのか
tsc
は import 文を書き換えない
1.1 公式ドキュメントにある通り、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
にできます。
tsc-alias
で直す最短パス
3. 例: // 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. よくあるハマりどころチェックリスト
-
strictNullChecks
がcompilerOptions
外にある
→ これは無視されるがビルドは通る。気持ち悪ければ中へ移動。 -
エイリアスの前に
@/
を書き、拡張子を付け忘れている
→ ESM モード (module:"NodeNext"
) では.js
が無いと実行時に落ちる (Node.js)。 -
Dockerfile で
pnpm dev
のまま
→ 本番はビルド済み JS を実行するよう CMD を切替える。 -
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 上でも @/
エイリアスを安全に使えるはずです。試してみてください!