The build that only worked on my machine
Vercel moved to Node 24 and the deploy broke — not on Node, but on three fonts this repo never declared and was borrowing from its parent.
The build that only worked on my machine
Why Care?
Vercel moved its default runtime to Node 24, so we went to bump ours. The deploy was already failing — and not because of Node:
[vite]: Rolldown failed to resolve import "@fontsource-variable/bodoni-moda"
from "/vercel/path0/src/layouts/BaseLayout.astro"That import has been in BaseLayout.astro since the typography landed. It has
never been in package.json. It built fine locally every single time, because
Node resolution walks up the directory tree and found the package in
astro-knots/node_modules — the parent pseudomonorepo's install, one level
above this repo.
Vercel clones the site repo alone. There is no parent. There never was.
This is the failure mode the whole astro-knots independence rule exists to prevent: a site must build from its own directory, with nothing above it. The rule was written down. Nothing was checking it.
What's New?
Node 24.
enginesmoves to>=24.0.0and a.nvmrcpins24, verified against a real local Node 24.19.0 rather than declared and hoped for.Three fonts and a syntax highlighter, finally declared.
@fontsource-variable/bodoni-moda,figtree,jetbrains-monoat 5.3.0, andshikiat 4.4.3.Astro 7.2.0 → 7.2.4.
An audit that would have caught this, run over every bare import in
src/.
How it Works
Phantom dependencies, and why local builds lie
A phantom dependency is a package your code imports and your manifest never mentions. It works right up until something changes the resolution context — a different install layout, a hoisting flag, or in this case a machine that doesn't have the parent directory at all.
shamefully-hoist=true in this repo's .npmrc makes it likelier, but it wasn't
the cause. The fonts weren't in this repo's node_modules or its lockfile:
bodoni-moda MISSING
figtree MISSING
jetbrains-mono MISSING
lockfile NOT IN LOCKFILE — resolving by accidentThey were at astro-knots/node_modules/@fontsource-variable/, and every local
build had been quietly reaching a level up to get them.
shiki was the same story with a different sponsor — src/lib/highlighter.ts
imports it directly, and it survived only because Astro happens to bundle it as
a transitive dependency. Astro dropping or relocating it would have broken the
highlighter with no change on our side.
The check that should have existed
Rather than fix three imports and move on, we now walk every .astro, .ts and
.mjs file under src/, collect every bare specifier, and diff against the
manifest:
UNDECLARED: mdast, shiki, Michael P. StatonThree results, three verdicts. shiki is real — declared. mdast is a
type-only import satisfied by @types/mdast, which is declared, and erased
before it reaches the bundler. Michael P. Staton is an author line in a
changelog entry that the regex mistook for an import, which is a decent reminder
that a scanner is not a parser.
Under the Hood
TypeScript 7 is blocked, and we reverted it
TypeScript 7.0.2 installs and the site builds, but astro check dies:
The TypeScript module loaded (found 7.0.2) does not expose the programmatic API that
astro checkrelies on. TypeScript's native compiler (7.0 and later) does not ship this API yet.
The native Go compiler doesn't implement the API @astrojs/language-server
depends on. Astro tracks it in [roadmap discussion #1321][1].
We reverted to 6.0.3. Shipping TypeScript 7 would have meant a repo with no working type checker in exchange for nothing a reader of this site could see — a bad trade. It goes back on the list when Astro ships support.
Node 24 was the easy part
Zero code changes. Astro 7.2.4, Vite 8, and Rolldown all run clean on 24.19.0 —
483 pages, astro check at 0 errors. The runtime bump that prompted all of this
turned out to be the only part of it that just worked.
Files Touched
package.json—enginesto>=24.0.0; four new dependencies; astro 7.2.4.nvmrc— new, pins 24 so Vercel doesn't decide for us againpnpm-lock.yaml— the fonts and shiki now actually recorded
What's Next
The import audit is a script that ran once in a shell. It belongs in the repo as
pnpm check:deps, and in CI, or the next phantom lands the same way.Every other astro-knots site has the same
.npmrcand the same parent directory. This repo is unlikely to be the only borrower.TypeScript 7 when
astro checksupports it.
References
Sources
- [1]
