技術 約7分で読めます

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 dateIgnored build scripts:の警告が出ていて、pnpm 10への切り替えや依存パッケージのbuild script制限が影響しているように見えた。
package.jsonpnpm.onlyBuiltDependenciesを追加し、Vercelのビルドキャッシュも切ったが、これだけでは直らなかった。

その次に疑ったのが.astro<script>内に残っていたTypeScript構文だった。
ocr-tesseractanimation-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 HTMLInputElementPromise<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/scriptspublic/vendorの静的ファイルとして読み込むと安定した、と書かれていた。

ここでやっと見立てが変わった。
Astro 6.1.2に落とすかどうかを考える前に、まずは壊れている変換パイプラインを避ける方が筋がいい。

誤解しやすい点

public/scriptspublic/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.astro
  • src/pages/lab/animation-editor.astro

やったことは以下。

  1. .astro内のscriptを<script is:inline type="module">に変更
  2. npmのbare specifier、つまり@ffmpeg/ffmpegのようなパッケージ名直書きimportをやめる
  3. ブラウザから直接読めるファイルをpublic/vendorへ置く
  4. そこを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経由に逃がすと止まるか

途中生成チャンク側で落ちているなら、自分のソースコードを延々と睨んでも時間を溶かしやすい。
「どのページが壊れているか」より先に、「どの変換経路を通って壊れているか」を見る方が早かった。