Astro 6のVercelビルドが Expected ':' but found ')' で落ちた
目次
このブログをAstro 6系で動かしていて、Vercelのビルドが急に通らなくなった。
ローカルではpnpm run buildが通るのに、Vercelだけがesbuildのparse errorで落ちる。
最終的には直ったが、原因は「TypeScriptの書き方がまずい」よりも、Astro 6がビルド途中で生成するscript用チャンクとVercelの組み合わせ側にあった。
解決前に疑ったこと
最初に気づいたのは、AdSense広告を追加するコミットをpushした直後だった。
なので最初は広告差分を疑った。
広告コードを外し、前に通っていた状態をVercelで再デプロイしても同じエラーで落ちた。
この時点で「直前の広告変更だけが原因ではなさそうだ」とわかった。
次に疑ったのはビルド環境だった。
VercelのログにはUsing pnpm@10.x based on project creation dateやIgnored build scripts:の警告が出ていて、pnpm 10への切り替えや依存パッケージのbuild script制限が影響しているように見えた。
package.jsonにpnpm.onlyBuiltDependenciesを追加し、Vercelのビルドキャッシュも切ったが、これだけでは直らなかった。
その次に疑ったのが.astroの<script>内に残っていたTypeScript構文だった。
ocr-tesseractやanimation-editorから型注釈やDOM要素のキャストを剥がしていくと、一部のエラーは動いた。
animation-editorでは<script is:inline>を付けるとそのファイルのエラーはいったん消えた。
ただし今度は次のファイル(ocr-tesseract)で同じ種類のエラーが出た。
ここでやっと見えたのは、個々のファイルの書き方よりも「そのscriptがどの変換経路を通るか」の方が本質だということだった。
広告差分、pnpm 10、TypeScript構文は全部もっともらしい仮説だったが、どれも単独では止血できなかった。
本当に効いたのは、次のセクション以降で書く通り、Astroが途中生成するscriptチャンクの経路そのものを避けることだった。
出ていたエラーはこれ。
[ERROR] [vite] ✗ Build failed
Expected ":" but found ")"
Location:
_astro/ocr-tesseract.astro_astro_type_script_index_0_lang.DupqxaF4.js:69:159
ファイル名を見るとわかる通り、壊れているのは自分が直接書いたsrc/pages/lab/ocr-tesseract.astroではない。
Astroが生成した_astro/*astro_type_script*チャンクが壊れている。
何が起きていたのか
このブログはoutput: "static"で、Vercelアダプタを使っている。
今回落ちたのは、ラボの中でもページ内<script>で重い依存を動的importしているページだった。
src/pages/lab/ocr-tesseract.astro
src/pages/lab/animation-editor.astro
両方とも.astroのページ内<script>にawait import(...)があり、そこで重い依存を必要なページだけ読み込む構成にしていた。
やりたいこと自体は普通で、全ページに無駄なJavaScriptを配らないための設計でもある。
問題は、その<script>がAstro側で...astro_type_script...という中間チャンクに変換され、Vercelビルド中のesbuildがそこでExpected ":" but found ")"を吐くことだった。
この中間チャンクは、Astroがビルド途中で一時的に作るJavaScriptファイルだと思えばいい。
flowchart TD
A[".astro のページ内 script"] --> B["Astro が generated chunk を作る"]
B --> C["_astro/*astro_type_script*.js"]
C --> D["Vercel build 中の esbuild"]
D --> E["Expected ':' but found ')'"]
これ、最初は自分のコードの構文ミスに見える。
でも実際には、ソースの該当行を見てもその位置に対応する壊れた)なんて存在しない。
Astroが途中生成したチャンク側で何かが崩れている。
最初にやった修正は効かなかった
最初に疑ったのは、.astroの<script>内に残っていたTypeScript構文だった。
DOM要素のas HTMLInputElement、Promise<void>、File | null、型注釈つきの引数や戻り値などを剥がした。
実際、animation-editorのような長いscriptではTypeScript構文がかなり残っていたので、これは無意味な作業ではなかった。
ただし、それだけでは止血できなかった。
ocr-tesseractはTypeScript構文を削ったあとでも、Vercelでは同じエラーで落ちた。
つまり問題は「TypeScriptを.astroに書いたから壊れた」ではなく、「そのscriptがAstroの途中生成チャンク経路を通ると壊れる」だった。
この時点で、TypeScript -> JavaScript変換だけでは足りないとわかった。
既知のissueがほぼ同じ症状だった
調べると、2026年4月8日にAstro本家でほぼ同じissueが上がっていた。
issueの報告内容はかなり近い。
Astro 6.1.4@astrojs/vercel 10.0.4- Node 22 on Vercel
_astro/*astro_type_script*で落ちるExpected ":" but found ")"
しかも、そのissueで一番重要だったのは「何をやったら安定したか」の部分。
そこでは、.astroのcomponent scriptを普通に通すのをやめて、public/scriptsやpublic/vendorの静的ファイルとして読み込むと安定した、と書かれていた。
ここでやっと見立てが変わった。
Astro 6.1.2に落とすかどうかを考える前に、まずは壊れている変換パイプラインを避ける方が筋がいい。
誤解しやすい点
public/scriptsやpublic/vendorに逃がすと言うと、「それだと全ページでそのスクリプトが読まれるだろ」と思いやすい。
自分もそこはかなり気にした。
でも、実際には別の話。
全ページで読まれるのは、共通レイアウトやヘッダーにscriptを入れた場合だけ。
ページ側でだけ読み込めば、そのページを開いた時だけ取得される。
<script type="module" src="/scripts/ocr-tesseract.js"></script>
あるいは、今回のようにページ内script自体をis:inline type="module"にして、その中から/vendor/...をimportしてもいい。
重要なのは「全ページ共通にすること」ではなく、「Astroの途中生成チャンク経路を通さないこと」だった。
この違いを整理するとこうなる。
| 方法 | 全ページで読むか | Astroの途中生成チャンクを通るか |
|---|---|---|
| 共通layoutにscriptを置く | 通る | 通らない場合もある |
ページ内<script>を普通に書く | 通らない | 通る |
ページ限定でpublic/vendorをimport | 通らない | 通らない |
今回欲しかったのは3番目。
実際にやった回避策
最終的には、壊れていた2ページだけを対象にした。
src/pages/lab/ocr-tesseract.astrosrc/pages/lab/animation-editor.astro
やったことは以下。
.astro内のscriptを<script is:inline type="module">に変更- npmのbare specifier、つまり
@ffmpeg/ffmpegのようなパッケージ名直書きimportをやめる - ブラウザから直接読めるファイルを
public/vendorへ置く - そこを
import('/vendor/...')で読む
ocr-tesseract側はこう変えた。
const { createWorker } = await import('/vendor/tesseract.js/6.0.1/tesseract.esm.min.js');
const worker = await createWorker(lang, 1, {
workerPath: '/vendor/tesseract.js/6.0.1/worker.min.js',
logger: (m) => {
// ...
}
});
animation-editor側も同じで、@ffmpeg/ffmpegと@ffmpeg/utilのdynamic importを、/vendor/ffmpeg/...のbrowser ESMへ差し替えた。
const { FFmpeg } = await import('/vendor/ffmpeg/ffmpeg/0.12.15/index.js');
const { fetchFile } = await import('/vendor/ffmpeg/util/0.12.2/index.js');
ついでにanimation-editorはscript内にTypeScript構文が大量に残っていたので、そこもJavaScriptへ落とした。
ただし重要なのは、そこまでやってもまだAstroの途中生成チャンク経路を通していたら再発しうる、という点。
なぜAstro 6.1.2へのダウングレードを先にやらなかったか
途中で「Astro 6.1.2に下げた方が早かったのでは」という考えも出た。
でも、その時点で本家issueはAstro 6.1.4でも再現していて、修正版リリースへの紐付けも見えていなかった。
つまり、6.1.3 -> 6.1.2のrollbackは賭けになる。
当たれば早いが、外れたら依存周りだけ大きく動かして何も解決しない。
今回みたいに「Astroの途中生成チャンクが壊れている」という状況では、まず壊れている経路を避ける方が再現性が高い。
Astro本体のfixが出たあとで戻すかどうかを考えればいい。
結果
この回避を入れたあと、ローカルのpnpm run buildは最後まで通った。
その後mainへpushしたところ、Vercelのデプロイも通った。
ローカルでは最終的に4079 page(s) builtまで進み、問題のExpected ":" but found ")"は再現しなかった。
つまり今回の本体は、アプリケーションロジックのバグではなく、Astro 6系のscript変換パイプラインとVercel buildの相性問題だったと見ていい。
同じ症状に当たったら、まず見るべきポイントはここ。
- エラー位置が
_astro/*astro_type_script*になっていないか - 落ちているページが
.astro内<script>でdynamic importしていないか - TypeScriptを剥がすだけで直ったか
public/vendor経由に逃がすと止まるか
途中生成チャンク側で落ちているなら、自分のソースコードを延々と睨んでも時間を溶かしやすい。
「どのページが壊れているか」より先に、「どの変換経路を通って壊れているか」を見る方が早かった。