package.json is the one file in a project that nobody audits, because every line in it has been run at least once by somebody. I went through all sixteen scripts in ours and sorted them by what they actually touch. Two of them cannot run at all, and the broken pair turned out to be the oldest thing in the file.
The sixteen
"test": "vitest run",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"format": "prettier --write .",
"format:check": "prettier --check .",
"stripe:setup": "tsx --env-file=.env stripeSetup.ts",
"stripe:seed": "tsx --env-file=.env.local stripeSeed.ts",
"stripe:listen": "stripe listen --forward-to localhost:3000/api/webhook/stripe",
"db:seed": "tsx --env-file=.env.local utils/db/seed.ts",
"indexnow": "tsx --env-file=.env.local scripts/indexnow.ts"
Of those sixteen, production runs exactly one. Vercel runs build. It never runs start, because the output goes into Vercel's own runtime rather than a Node server we operate. Everything else in the list is a local tool.
The two that cannot run
"stripe:setup": "tsx --env-file=.env stripeSetup.ts",
"stripe:seed": "tsx --env-file=.env.local stripeSeed.ts",
Neither stripeSetup.ts nor stripeSeed.ts is in the repository. stripe:setup also asks for --env-file=.env, and there is no .env either: this project has a committed .env.example and a local .env.local, and nothing in between. So that one line names two files that do not exist.
I did not run them to see the error, because a script with "stripe" and "setup" in its name is not something to fire at a live account on the strength of a guess about what it would fail on. git log was more informative anyway.
stripeSetup.ts did exist once. It was added in the repository's first commit and deleted in the second, one hour and thirty-nine minutes later, on 2 June. It was 168 lines of SaaS starter template:
const CURRENCY = 'usd';
const plans = [
{ name: IS_PRODUCTION ? 'Basic' : 'Basic-Test', price: 1000,
description: 'Perfect for small teams and individuals',
features: ['Up to 10 users', 'Up to 1000 records', 'Up to 1000 API calls'] },
It created Stripe Products and Prices from that array and registered a webhook endpoint. Deleting it was right, and not only because a pub quiz app does not sell API calls: this app now has no Products and no Prices in its Stripe account at all. Every checkout line item is built in code from a table of rates, which is how one £30 plan is quoted in 24 currencies. That deleted script is the exact thing we decided not to have, and its npm entry has been sitting in package.json for nearly five months.
stripeSeed.ts is a better story: it has never existed in any commit on any branch. The script was added in the same second commit that deleted its sibling. It has been broken for its entire life.
The one Stripe script that works is stripe:listen, and it is also the only script in the file that somebody remembered to update during a routing change: it used to forward to localhost:3000/webhook/stripe, and now forwards to localhost:3000/api/webhook/stripe, because the old path was deleted. The /webhook deletion took six edits in four files, and that script line was a seventh in a fifth.
The three ways this project loads environment variables
This is the part of the audit I did not expect to find interesting.
-
dev,buildandstartneed no env flag, because Next loads.env.localitself. - Four scripts pass
--env-file=totsx: one for.env, three for.env.local. - Three files call
dotenvdirectly:utils/db/seed.ts,drizzle.config.tsand__tests__/setup.ts.
Which means db:seed loads .env.local twice, once through the tsx flag and once inside the file:
import { config } from 'dotenv';
config({ path: '.env.local' });
Harmless, duplicated, and the kind of thing that only becomes visible when you line all sixteen scripts up next to each other.
The four db: scripts pass no env flag at all, and do not need one, because drizzle.config.ts does it for them in the order Next would:
// Load .env.local first (takes precedence), then fall back to .env for any
// missing vars. This mirrors how Next.js resolves env files at runtime.
config({ path: '.env.local' })
config({ path: '.env' })
The ones that write to a real database
db:generate writes a migration file and touches nothing else. The other three all talk to whatever DATABASE_URL resolves to:
-
db:migrateapplies the 14 SQL files inutils/db/migrations. -
db:pushskips those files entirely and diffs the schema straight into the database. -
db:studioopens a UI onto it. -
db:seedinserts the built-in question packs.
db:push is the one I would argue about. This project's deployment policy is migration files, deliberately and after an argument I have already written about, and db:push is the escape hatch that makes the migration folder optional. It stays because it is useful against a scratch database, but it sits one typo away from db:migrate in a list sorted alphabetically, which is not where you want your "apply unreviewed schema changes directly" button.
The ones that are supposed to fail, and the one that does
$ pnpm run lint
$ next lint
Invalid project directory provided, no such directory: /Users/.../pub-trivia/lint
next lint was removed in Next 16, and the error is a giveaway: lint is being read as a directory argument. That one I have written about before, along with the three other configs in this repo that nothing reads.
$ pnpm run format:check
[warn] Code style issues found in 335 files.
335 of the 358 files Prettier looks at differ from what Prettier would write. When I measured this a fortnight ago it was 310, so the gap is growing, and it is growing for an interesting reason: the handful of compliant files were the newest ones, written in the formatter's own style, and then the next round of work edited exactly those files. The 23 that still pass today are almost all the currency and pricing modules, plus package.json itself. There is no .github directory, so no CI runs this. The only thing standing between the repository and prettier --write . is that a 335-file diff makes every git blame useless, which is a cost I keep deciding not to pay.
And test runs vitest over 16 files and 219 it() calls. Fifteen of them are pure: no network, no database, mocks where Stripe would be. One is an integration test that connects to a real database, creates rows and removes them again in afterAll. That is why pnpm test is not something I run absent-mindedly while anything else is pointed at that database, and why a script called test is in the "knows what a database is" category rather than the safe one.
What a reader can check
The scripts are local, but their results are not. You can see all of them from outside:
- what
buildproduces: pub-trivia.app, 72 URLs, most of them prerendered - what the deleted
stripeSetup.tswould have configured, built in code instead: the pricing page, which quotes in your own currency if you arrive through a VPN - what
indexnowsubmits: the sitemap - what
db:seedfills in, as a reader rather than a host: the question packs page
Two dead scripts in sixteen is not a crisis. What I took from the audit is the sort order: a package.json scripts block is usually a historical record in the order things were needed, and sorting it by blast radius instead (local, writes to a database, talks to a third party) makes the two or three lines you should be careful with visible at a glance. Ours is now annotated that way in the notes, if not yet in the file.
Top comments (0)