Commands
| Command | Action |
|---|---|
pnpm install |
Install dependencies |
pnpm dev |
Dev server at localhost:4321 |
pnpm build |
Production build to dist/ |
pnpm preview |
Serve the production build locally |
pnpm check |
astro check — type .astro and .ts |
pnpm lint |
ESLint |
pnpm format |
eslint --fix, then Prettier |
pnpm test |
Every *.test.ts self-check under src/ |
pnpm wiki:lint |
Verify the wiki’s citations and links |
The chain
pnpm lint && pnpm check && pnpm build && pnpm test && pnpm wiki:lint
Five commands, and they catch different things, which is why the order matters.
pnpm lint catches style and the accessibility rules — eslint-plugin-jsx-a11y runs over .astro markup, so a missing label or an aria attribute that cannot apply is a lint error rather than something you find with a screen reader.
pnpm check is the type pass over both .ts and .astro. It is where an illegal <Icon name>, a SiteHref missing its trailing slash, or a section prop that does not match its data shows up.
pnpm build is the real check. Content-collection schemas, config shapes and the SITE_URL gate all resolve at build time, and a Zod failure names the entry and the field. A clean build is the closest thing to a guarantee this project offers.
pnpm test runs the self-checks. pnpm wiki:lint does the same job for the prose.
The twelve self-checks
scripts/test.mjs walks src/, finds every *.test.ts, and runs each one with Node’s type stripping — no framework, no config, no fixtures. Discovery means a check written next to the code it covers runs without being registered anywhere. And zero checks is a failure, not a pass:
if (tests.length === 0) {
console.error("No checks found under src/ — expected at least one *.test.ts file.");
process.exit(1);
}
That single condition is what turns the house rule — non-trivial logic leaves one runnable check behind — from a convention into something the tooling enforces. There are twelve at the moment, and they run in about fifty milliseconds:
| Check | What it pins |
|---|---|
config/carsData.test.ts |
Enough records for the pager, unique refs, every rail value matching a record, featured-extras integrity |
config/hubsData.test.ts |
The same invariants for the new and electric datasets, plus refs unique across all three |
config/footerLinks.test.ts |
Every footer and legal href resolves to a real page or an infoData key |
config/homeData.test.ts |
The section-layout assumptions: photo arrays zipped by index, the dossier’s two rows of three, the {count} token actually replaced |
config/reviewsData.test.ts |
The featured verdict and the shelf are real, non-draft, "Review"-category posts; four icon-backed steps; the make columns sum to their headline |
config/searchData.test.ts |
Every result ref resolves, matches all three query fields the page claims, and is ordered nearest-first — the page’s own printed rule |
config/valueMyCarData.test.ts |
The hero specimen resolves to a record with a histogram; the band has four steps; the grids are the shapes the sections render |
js/favorites.test.ts |
The store’s parse/toggle/sort/median/city-count logic, and the position tiles against the real config data |
js/schema.test.ts |
The JSON-LD builders, the @graph composition, and the < escaping |
js/toc.test.ts |
Only H2s with explicit ids are extracted; an H3 is never mistaken for one |
ui/password/strength.test.ts |
scorePassword across six inputs from empty to strong |
ui/reveal/loadIn.test.ts |
The exact class string of the house entrance, and stagger’s delays |
The pattern they share is worth naming: each one checks a cross-file assumption that no type can express. A rail option’s value matching a record’s field, a hub naming a post that exists, a section’s photo array being the same length as its config list — these are the joins that break silently, and each is three lines of assert.
Name a check <thing>.test.ts next to the code it covers and it runs. Name it anything else and nothing runs it.
The wiki linter
pnpm wiki:lint (scripts/wiki-lint.mjs) treats the project’s own wiki/ as code. It resolves every path:line citation in the prose and fails unless the cited line still contains a symbol the surrounding sentence names — which is the check that catches a citation that slid when a file grew above it. It also enforces the wiki’s bidirectional [[links]].
It currently checks 185 citations across 27 pages. If you edit a wiki page, run it.
pnpm format
eslint --fix first, then Prettier over everything with --ignore-unknown and a cache. The order matters: Prettier’s Tailwind plugin sorts class lists, so running it last means the class order in a file ESLint just rewrote is still sorted.