Astro 6 on Vercel failed with Expected ':' but found ')'
Contents
This site is running on Astro 6, and one day Vercel builds suddenly stopped passing.
pnpm run build worked locally, but Vercel alone died with an esbuild parse error.
The fix turned out not to be “clean up the TypeScript syntax.” The real problem was the combination of Astro 6’s generated script chunks and Vercel’s build pipeline.
What I suspected before the actual fix
The first thing I noticed was that the failure started right after I pushed an AdSense-related change.
Naturally I blamed the ad code first.
I removed the ad code, redeployed a previously working state on Vercel, and it still failed with the same error.
At that point it was clear the most recent ad diff alone was not the whole story.
Next I suspected the build environment.
Vercel logs showed Using pnpm@10.x based on project creation date and also warnings about ignored build scripts, so pnpm 10 and dependency build-script restrictions looked suspicious.
I added pnpm.onlyBuiltDependencies to package.json and forced a redeploy without cache, but that still did not fix it.
Then I looked at the TypeScript syntax left inside .astro page scripts.
I stripped type annotations and DOM casts from ocr-tesseract and animation-editor.
That moved some things around. In animation-editor, adding <script is:inline> made that file’s error go away.
Then the exact same class of failure showed up in the next file, ocr-tesseract.
That was the turning point.
The important question was no longer “what syntax is inside this file?” but “what transformation path does this script go through?”
The ad diff, pnpm 10, and leftover TypeScript syntax were all plausible guesses, but none of them alone explained the full behavior.
What actually worked was bypassing the generated script chunk path itself.
This was the error:
[ERROR] [vite] ✗ Build failed
Expected ":" but found ")"
Location:
_astro/ocr-tesseract.astro_astro_type_script_index_0_lang.Dd3mjldp.js:69:159
The filename tells the story.
The broken file was not src/pages/lab/ocr-tesseract.astro itself.
It was an Astro-generated _astro/*astro_type_script* chunk.
What was actually happening
This site uses output: "static" with the Vercel adapter.
The pages that failed were lab pages with page-local <script> blocks doing dynamic imports of heavier dependencies.
src/pages/lab/ocr-tesseract.astro
src/pages/lab/animation-editor.astro
Both pages had await import(...) inside .astro page scripts so the heavy code only loaded on the page that actually needed it.
That design itself was fine. It avoids shipping unnecessary JavaScript to every page.
The problem was that Astro transformed those scripts into ...astro_type_script... intermediate chunks, and esbuild on Vercel then failed on the generated output with Expected ":" but found ")".
That intermediate chunk is just a temporary JavaScript file Astro creates during the build.
flowchart TD
A["script inside a .astro page"] --> B["Astro generates an intermediate chunk"]
B --> C["_astro/*astro_type_script*.js"]
C --> D["esbuild during Vercel build"]
D --> E["Expected ':' but found ')'"]
At first glance this looks like an ordinary syntax mistake in the source.
But when the error points into a generated file and the source line does not contain any matching broken ), staring at the original file is a good way to waste time.
The thing breaking is the generated chunk path, not the line you wrote.
Stripping TypeScript syntax was not enough
The first concrete fix attempt was to remove TypeScript syntax from the .astro scripts.
That meant DOM casts like as HTMLInputElement, unions like File | null, return types like Promise<void>, and typed parameters.
That was not useless work.
animation-editor in particular still had a lot of TypeScript syntax embedded in a long page script.
But it was still not enough.
Even after removing TypeScript syntax, ocr-tesseract kept failing on Vercel with the same class of error.
The real issue was not “TypeScript inside .astro is always bad.”
The real issue was that these scripts were still going through Astro’s generated chunk path.
That was the moment it became clear that TypeScript -> JavaScript conversion alone would not solve this.
The GitHub issue that matched almost exactly
Looking around turned up an Astro issue filed on April 8, 2026 that matched the symptom almost perfectly.
The reported environment was close to mine:
Astro 6.1.4@astrojs/vercel 10.0.4- Node 22 on Vercel
- failure inside
_astro/*astro_type_script* Expected ":" but found ")"
The useful part was not the stack trace.
It was the workaround.
The stable path reported there was to stop sending those scripts through the normal .astro component-script pipeline and instead load them as static files from public/scripts or public/vendor.
That changed the direction completely.
Before thinking about downgrading Astro to 6.1.2, it made more sense to stop using the path that was already known to be fragile.
The easy misunderstanding
The obvious objection is performance.
If you move code into public/scripts or public/vendor, it sounds like the whole site will load it on every page.
That is not what happens.
It only becomes site-wide if you put the script in a shared layout or global header.
If the page alone loads it, only that page fetches it.
<script type="module" src="/scripts/ocr-tesseract.js"></script>
Or, as in this case, the page script itself can be is:inline type="module" and import from /vendor/....
The important distinction is not “inline versus external.”
It is “does this go through Astro’s generated chunk path or not?”
The difference looks like this:
| Method | Loaded on every page? | Goes through Astro’s generated chunk path? |
|---|---|---|
| Put a script in a shared layout | Yes | Maybe not |
Write a normal page <script> in .astro | No | Yes |
Use page-local imports from public/vendor | No | No |
The third one was what I actually wanted.
What I changed
In the end I only touched the two broken pages:
src/pages/lab/ocr-tesseract.astrosrc/pages/lab/animation-editor.astro
The actual changes were:
- Change the
.astropage scripts to<script is:inline type="module"> - Stop using bare specifiers like
@ffmpeg/ffmpegdirectly in those scripts - Copy browser-readable files into
public/vendor - Import those files via
/vendor/...
On the ocr-tesseract page, the import became this:
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) => {
// ...
}
});
On the animation-editor page, the dynamic imports for @ffmpeg/ffmpeg and @ffmpeg/util were replaced with browser ESM files under /vendor/ffmpeg/....
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 also still had a large amount of TypeScript syntax inside the page script, so I converted that script to plain JavaScript as well.
But the important part is this: even a clean JavaScript script could still fail if it kept going through the same generated chunk path.
Why I did not start with an Astro 6.1.2 downgrade
At one point I did consider that rolling back to Astro 6.1.2 might be faster.
But the upstream issue was still reproducible on Astro 6.1.4, and there was no clear sign yet of a fixed release tied to the issue.
That makes 6.1.3 -> 6.1.2 a gamble.
Maybe it helps, maybe it just moves the dependency graph around while solving nothing.
In a case like this, where the generated chunk path itself appears to be the problem, avoiding that path is the more reliable move.
A framework downgrade is a separate decision to revisit later if needed.
Result
After switching to that approach, local pnpm run build completed successfully.
Then I pushed the fix to main, and the Vercel deployment passed as well.
Locally the final build reached 4079 page(s) built, and the Expected ":" but found ")" error did not come back.
That strongly suggests the core issue was not application logic, but the interaction between Astro 6’s script transformation path and Vercel’s build process.
If you run into the same symptom, the first things to check are these:
- Is the error location inside
_astro/*astro_type_script*? - Does the failing page use dynamic import inside a
.astropage<script>? - Did stripping TypeScript syntax fail to solve it?
- Does the problem stop once the page loads from
public/vendorinstead?
If the failure is happening in an intermediate generated chunk, reading your original source over and over is often the wrong move.
The faster question is not “which page broke?” but “which transformation path broke?”