FifeRouter

3 September 2026 · mistakes builds

The dependency that was never declared

Every typecheck in the project's history had passed against a node_modules that happened to contain something the lockfile did not.

Moving the site build into a container, the first attempt failed:

vite.config.ts(2,30): error TS2307: Cannot find module 'node:fs'
vite.config.ts(5,14): error TS2304: Cannot find name '__dirname'
vite.config.ts(43,13): error TS2580: Cannot find name 'process'

vite.config.ts has always used Node APIs. tsconfig.json has always had it in include. And npx tsc --noEmit had always passed.

@types/node was not in package.json and not in the lockfile.

How it had been working

Somebody, at some point, installed something that pulled @types/node in transitively. It has sat in node_modules ever since, and every typecheck on that machine resolved against it.

npm ci installs exactly the lockfile. In a fresh container there is no accumulated node_modules, so the types were not there, and three years of "this has always worked" ended in eight seconds.

The deploy script ran the same check

This is the part worth dwelling on. The old deploy path did:

( cd site/web && npm ci --silent && npx tsc --noEmit && npm run build )

Same command. It ran npm ci too. And it kept passing — because it ran on the same laptop, where npm ci prunes to the lockfile but the directory it prunes had already been the source of truth for what "installed" meant, and the developer's global state had long since made the question moot.

The check was correct. The environment it ran in was not clean, and had not been clean for long enough that nobody remembered it being otherwise.

What made it visible

Nothing changed about the code. What changed was where it built.

A container build starts from a base image and a lockfile, so it is the first place the question "what does this project actually depend on" gets asked honestly. The answer had been wrong for a long time and nothing had needed to know.

That is the specific value of building in a container, distinct from the usual argument about reproducibility. It is not that the build is more repeatable — it is that the build is interrogated, once, by something with no memory of how the project got to where it is.

The fix

npm install --save-dev @types/node

Declared, locked, and the typecheck passes from clean. One line, and the bug it closes had been latent since before anyone on the project could remember.

What it says about the old pipeline

The tarball deploy shipped source and built it on the box, and this is the class of failure that arrangement hides. The box had its own long-lived node_modules; so did the laptop; and both had drifted the same way, so they agreed.

Two environments agreeing is not the same as either being right. It is often just two copies of the same history.

The pipeline now builds images in CI from a lockfile and ships the artifact. That change was made for other reasons — knowing which build is in production, mainly — and this was the first thing it caught, before it had deployed anything at all.


← All posts