Commands & Testing
8-BitQuest has nine pnpm scripts and no hidden tooling. The interesting one is test, which runs a discovery-based runner with no framework at all; the rest are the standard Astro lifecycle plus the lint/format pair.
The scripts
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"start": "node ./dist/server/entry.mjs",
"astro": "astro",
"check": "astro check",
"lint": "eslint .",
"format": "eslint . --fix && prettier -w \"**/*\" --ignore-unknown --cache",
"test": "node ./scripts/test.mjs"
pnpm dev— the dev server atlocalhost:4321.pnpm build— the production build. Because the contact route is server-rendered, the output splits intodist/client/(prerendered pages) anddist/server/entry.mjs(the one server route).pnpm preview— serves the built output for a quick look.pnpm start— runs the built Node server, honouringHOST/PORT. This is what you run in production. See Deployment.pnpm check—astro check, a full type-check across.astroand.ts.pnpm lint— ESLint over the whole project.pnpm format—eslint --fixand then Prettier, with a cache.pnpm test— the check runner, below.
The verify chain
The one command to run before you commit, and the exact chain CI runs on every push and pull request:
pnpm lint && pnpm check && pnpm build && pnpm test
The build is the real check — content-schema errors, config typos and broken references all surface there rather than in the browser. CI (.github/workflows/ci.yml) runs this same chain on the placeholder SITE_URL, on the Node version that matches the engines floor, so a green CI run means the code builds; it does not mean your production domain is set.
The test runner
scripts/test.mjs is the whole of pnpm test, and it is deliberately tiny — no Jest, no Vitest, no config, nothing to register:
- It discovers every
*.test.tsfile undersrc/and runs each one undernode --experimental-strip-types. The explicit flag is what lets it work on the Node 22.13 floor, rather than only on 23.6+ where type-stripping is on by default. - Zero checks found is a failure, not a pass. The point of discovery is that it cannot quietly stop finding the checks it is meant to run — a boilerplate that ships a green-but-empty test command teaches the opposite of the rule it enforces.
There are eleven checks today: the contact validation and spam gates (contact.test.ts), the Resend send boundary (resend.test.ts), the JSON-LD builders (schema.test.ts), the RSS renderer (rss.test.ts), reading time (readingTime.test.ts), the nav active-link logic (nav.test.ts), the post and project card mappings (postCards.test.ts, projectCards.test.ts), the social-URL resolver (social.test.ts), the listbox kernel (_listbox.test.ts) and the password strength scorer (strength.test.ts).
Why the tested helpers are pure
The checks run under Node’s type-stripping, which erases type-only imports but cannot resolve Astro’s virtual modules (astro:content) as runtime values. That is why the theme keeps its logic pure: the card mappings (postCards.ts, projectCards.ts) import types only and are tested, while the data-access siblings that read astro:content at runtime (blogData.ts, projectData.ts) are intentionally untested — the split is what keeps the whole suite runnable without a bundler. When you add logic worth testing, keep it in a pure module with type-only imports and drop a *.test.ts beside it.
The tooling rules
A few conventions are worth knowing before they surprise you:
- Class ordering is Prettier’s job.
prettier-plugin-tailwindcsssorts utility classes; do not hand-order them. Runpnpm formatand let it sort. - Import ordering is ESLint’s job.
simple-import-sortorders imports (side-effect first, then alphabetised), so runpnpm formatrather than sorting by hand. - The one lint waiver. The primitive contract exports a
tv()config from.astrofrontmatter, which collides withastro/no-exports-from-components. A scoped override disables just that rule forsrc/components/ui/**/*.astroandsrc/components/svg/**/*.astro, documented ineslint.config.mjs. Everything else in those files lints normally. scripts/is linted like shipped code. There is no blanket ignore; the one script left lints clean and is held to the same bar.