Skip to content
AstroCraft Docs
On this theme

Commands & Testing

Eight scripts, no test framework, and no build configuration beyond astro.config.mjs.

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-check .astro and .ts
pnpm lint ESLint
pnpm format eslint --fix then Prettier
pnpm test Run every check under src/
pnpm astro … The Astro CLI directly

Which command catches what

This matters more than the list, because the four verification commands catch different classes of mistake and none of them subsumes another.

pnpm lint catches unused variables, import ordering, and accessibility problems in markup — a missing alt, a label with no control, an anchor with an invalid href. The jsx-a11y rules are on for .astro files.

pnpm check catches type errors, and it is the only thing that catches some of this theme’s invariants. The pricing comparison matrix is keyed by plan name through a derived union, so a missing or misspelled cell is a compile error and not a build error. astro build does not type-check. Skipping this command is how a broken matrix ships.

pnpm build catches content and config mistakes: a bad frontmatter field, an author reference to a slug that does not exist, an image path that does not resolve, a missing integration logo, a route that accidentally depends on request-time behaviour. Content-schema errors surface here with the entry named.

pnpm test is the only thing that executes the self-checks. astro check type-checks them without running them; astro build ignores them entirely.

Run all four before you push:

pnpm lint && pnpm check && pnpm build && pnpm test

The check runner

scripts/test.mjs is 35 lines and the whole test infrastructure. It walks src/ recursively for files ending in .selfcheck.ts or .test.ts, sorts them, and spawns each with node --experimental-strip-types. If any exits non-zero, the run fails.

Discovery is by filename. A new check needs no registration, no config entry and no import anywhere. Name the file correctly and put it next to the code it checks.

Zero checks is a failure, not a pass. If the runner finds nothing it prints an error and exits 1. That guard exists because the suite once did quietly empty itself: the runner used to scan scripts/ rather than src/, and when some one-shot scripts were deleted they took their checks with them — leaving a green run over nothing while four real checks under src/ sat unexecuted.

Node’s type stripping is why there is no framework. --experimental-strip-types runs TypeScript directly, which is what makes a check a plain file with assert calls rather than something a runner has to compile. It is also why engines.node is >=22.12.0.

The seven checks that ship:

File Covers
src/js/schema.selfcheck.ts the JSON-LD builders, @id wiring, the < escaping
src/js/contact.selfcheck.ts the contact schema bounds, CRLF rejection, both spam gates, the escaper, the email body
src/js/rss.selfcheck.ts the feed document and its XML escaping
src/components/ui/count-up/format.selfcheck.ts number formatting
src/components/ui/password/strength.test.ts the password scoring rules
src/components/ui/_hotkey.selfcheck.ts ⌘K registration and last-wins ownership
src/components/ui/_listbox.selfcheck.ts list filtering and active-descendant index maths

Run one directly while you are working on it:

node --experimental-strip-types src/js/schema.selfcheck.ts

Why those seven and not more

The house rule is that non-trivial logic leaves one runnable check behind — the smallest thing that fails if the logic breaks. No frameworks, no fixtures, no mocks. Trivial one-liners get none.

That is why the checks cluster where they do. Everything with a check is pure logic at a trust boundary or with real edge cases: escaping, validation, scoring, index arithmetic. Nothing checks a component’s markup, because pnpm build renders every page and /examples/ui renders every primitive — a rendering failure is already loud.

Note that all three src/js/ modules with checks were written import-free on purpose. schema.ts imports nothing at all; contact.ts imports only astro/zod; rss.ts is pure. That is what lets them run under plain Node, which knows nothing about tsconfig path aliases or the Astro runtime. If you add a helper that deserves a check, keep it free of astro:* imports and put the Astro-specific part in a thin caller — that split is the pattern rss.ts versus rss.xml.ts demonstrates.

Adding a check

Create <name>.selfcheck.ts beside the module, import what you are checking with a relative path, and assert:

import assert from "node:assert/strict";
import { escapeXml } from "./rss.ts";

assert.equal(escapeXml("a & b"), "a &amp; b");
console.log("✓ rss");

Note the explicit .ts extension in the import — Node’s resolver needs it, and path aliases are unavailable. Then run pnpm test; the runner finds it.

Tooling notes

ESLint is flat config (eslint.config.mjs) with the recommended JS and TypeScript sets, the Astro plugin, jsx-a11y, and simple-import-sort. Two allowances worth knowing: astro/no-exports-from-components is off for src/components/ui/** and src/components/svg/**, because the primitive contract requires exporting the tv() config; and no-undef is off in .astro files, since Astro’s global types are ambient.

scripts/** is excluded from linting — it is tooling rather than shipped code. Mind that while editing test.mjs; it is unlinted.

Prettier runs at 100 columns with double quotes and trailing commas. prettier-plugin-tailwindcss must stay last in the plugins array — it is what orders utility classes. Let it sort; do not hand-order class lists.

pnpm format runs eslint --fix first, then Prettier over everything with --ignore-unknown and a cache.

Dev server notes

Two behaviours that look like bugs and are not:

A renamed content entry keeps 404ing. Astro’s content layer caches entries; renaming a file while the server runs can leave the old id in place. Restart.

A new utility class has no effect. In a long-running dev server, classes used only in newly created files can be missing from the generated stylesheet. Restart before debugging your markup.

And one that matters at build time: astro dev runs a server whether or not an adapter is installed. A working dev server proves nothing about whether a route survives a static build. pnpm build is the check that tells the truth.

NEXT STEPTroubleshooting