Config rots differently from code. Code that stops working throws. Config that
stops working usually keeps sitting in the repo looking exactly as authoritative
as the day it was correct, because nothing reads it and nothing complains.
I went through PubTrivia's tooling files one at a time asking a single question
of each: if I deleted this, what would break? Four of them answered "nothing",
and the first one answered it in a way I did not expect.
1. The lint script lints a directory that does not exist
"lint": "next lint"
That was correct for most of this project's life. Here is what it does now:
$ npx next lint
Invalid project directory provided, no such directory:
/Users/daniel/Documents/programs/pub-trivia/lint
next lint was removed. The next CLI in version 16 takes a project directory
as its positional argument, so the word lint is no longer a subcommand, it is
a path. The script does not fail with "unknown command". It fails because it went
looking for a folder named lint, which is a far better error than it sounds:
it tells you exactly which assumption expired.
So I called ESLint directly, which is the documented replacement:
$ npx eslint lib/site.ts
Oops! Something went wrong! :(
ESLint: 10.4.1
ESLint couldn't find an eslint.config.(js|mjs|cjs) file.
The repo has a .eslintrc.json:
{ "extends": "next/core-web-vitals" }
ESLint 10 does not read that file. Flat config became the default in 9 and the
eslintrc format is gone in 10, so the config is a file that only another tool's
older self could have used. Both halves of linting are broken, independently,
and the two breakages hid each other: the script failed with a message about
directories, so I never got as far as the message about config formats.
Nothing caught it because nothing runs it. There is no .github directory in
this repo, and next build in version 16 does not lint for you either. One
removed subcommand and one config format migration, and a project can go from
linted to not linted without a single error anyone sees.
2. A dependency cooldown with an exclusion list and no cooldown
This is in pnpm-workspace.yaml:
minimumReleaseAgeExclude:
- '@next/env@15.5.19'
- '@next/eslint-plugin-next@15.5.19'
- '@next/swc-darwin-arm64@15.5.19'
- '@next/swc-darwin-x64@15.5.19'
- '@next/swc-linux-arm64-gnu@15.5.19'
- '@next/swc-linux-arm64-musl@15.5.19'
- '@next/swc-linux-x64-gnu@15.5.19'
- '@next/swc-linux-x64-musl@15.5.19'
- '@next/swc-win32-arm64-msvc@15.5.19'
- '@next/swc-win32-x64-msvc@15.5.19'
- eslint-config-next@15.5.19
- next@15.5.19
minimumReleaseAge is pnpm's supply chain cooldown: refuse to install any
version published less than N minutes ago, so a compromised release has time to
be noticed before it lands in your lockfile. minimumReleaseAgeExclude is the
escape hatch for packages you need immediately, which is why twelve Next.js
artefacts are listed here. A patched Next release is exactly the thing you do not
want to wait a week for.
Two things are wrong with the block. The first:
$ pnpm config get minimumReleaseAge
undefined
There is no cooldown. The exclusion list excludes packages from a rule that is
not in effect anywhere in this project's config, so it has never changed the
outcome of an install.
The second is the version numbers. Every entry pins 15.5.19. This repo is on
Next 16:
$ node -e "console.log(require('next/package.json').version)"
16.2.7
Even if the cooldown were switched on tomorrow, the exclusions would apply to
versions nothing installs. The list is a snapshot of an urgent upgrade from a
previous major, left behind the way a changed-your-mind comment gets left behind.
The fix is not to delete it. It is to set minimumReleaseAge, then rewrite the
exclusions without version pins so they survive the next major. A cooldown with
no exclusions blocks a security patch; exclusions with no cooldown block nothing.
I had the half that does nothing.
3. A property testing library nothing imports
"fast-check": "^4.8.0"
Installed, resolved at 4.8.0 in node_modules, and imported by zero files. I
grepped every tracked .ts, .tsx, .mjs, .js and .json for the string
fast-check and the only hit is package.json itself.
I know why it is there. Property based testing is the right tool for a couple of
things in this codebase, the kind of code where I want "for any input, this
invariant holds" rather than six examples. I installed it to do that and then
wrote the example based tests instead, because examples are what you reach for at
11pm. The dependency is the fossil of an intention.
Same shape, different flavour, one line below:
"@vitest/coverage-v8": "^4.1.8"
There is no coverage script in package.json, and vitest.config.ts has no
coverage block. The provider is installed so that vitest run --coverage
works if I type it, and in four months of commits there is no evidence I typed it.
4. An .nvmrc two majors behind the types
$ cat .nvmrc
v20.18.1
$ node --version
v26.5.0
And in devDependencies:
"@types/node": "^25.9.1"
So the file that tells a new machine which Node to use names 20, the type
definitions the compiler checks against describe 25, and the Node actually
running the dev server here is 26. The WebSocket server, which is a separate
package in the workspace, declares "engines": { "node": ">=20" }, which is true
of all three and therefore informative about none of them.
This is the least dangerous of the four and the most likely to bite someone else.
Nothing enforces .nvmrc, so on my machine it is a note I ignore. On a fresh
clone with nvm use, it is a Node 20 that typechecks against Node 25 types,
which is how you get an API that the compiler swears exists and the runtime has
never heard of.
What this has to do with the product
Nothing, which is the point worth making. All four of these have been broken or
pointless for weeks and the app they belong to is fine. The
free tools still run entirely in the browser, the
pricing page still resolves a display currency
per request, and the
features index still ships as prerendered HTML.
You can sign up on the free tier with no card and run a quiz tonight, and not one
part of that experience depends on whether my linter has a config file it can
read.
That is exactly why these things rot. Every one of them is a check on the
developer, not a check on the product, so the product cannot tell you when the
check stopped running. The only way to find out is to open each config file and
ask what would break if it were deleted, and to treat "nothing" as a bug report
rather than an answer.
Order I am fixing them in: ESLint flat config first, because it is the one that
was actually catching bugs. Then minimumReleaseAge, because an unused supply
chain protection is worse than none at all: it looks like I have one.
Top comments (0)