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, including the booking endpoint |
pnpm check |
Type-check .astro and .ts via astro check |
pnpm lint |
ESLint |
pnpm format |
eslint --fix, then Prettier |
pnpm test |
Every *.test.ts self-check under src/ |
pnpm wiki:lint |
Verify wiki/ citations and links |
The chain
pnpm lint && pnpm check && pnpm build && pnpm test && pnpm wiki:lint
Run it before every commit. The order is deliberate — lint and type errors are cheaper to fix than a failed build, and the build is what surfaces content-schema and config mistakes.
pnpm build is the real check. A clean astro dev proves less than you would like: the booking endpoint’s dead-code-elimination bug worked perfectly in development and returned 500 to every request in production, and only reading the built output would have caught it.
pnpm dev
Starts at http://localhost:4321 with every route available, including /book/ and the dev-only /examples/ui catalog.
The booking form renders and validates without any secrets; only the final send needs them. With a .env carrying the three keys, pnpm dev sends real mail.
pnpm build
Because one route is on-demand, the output splits: dist/client/ holds all 52 prerendered pages and dist/server/ holds the endpoint. A static host receives dist/client.
It reads SITE_URL, and on a production deploy it throws rather than build on the https://example.com placeholder. Local builds and deploy previews build freely. See Deployment.
pnpm preview
Serves the built output with a real server, so the booking endpoint actually runs. This is the only way to exercise the full POST path locally — the origin check, the validator, the Resend call and both response shapes.
Under @astrojs/node it is a plain server. Under the Cloudflare adapter it runs through wrangler’s local runtime.
pnpm check
astro check type-checks both .astro and .ts under astro/tsconfigs/strict. It is what catches a siteImages key with no file, a config file missing a field its interface requires, and an <Icon name> that is not in the registry.
pnpm lint and pnpm format
ESLint with eslint-plugin-astro, eslint-plugin-jsx-a11y and eslint-plugin-simple-import-sort. The a11y plugin is the one worth knowing about — a missing alt on a raw <img> is a lint error rather than a review comment.
pnpm format runs eslint --fix first, then Prettier with prettier-plugin-astro and prettier-plugin-tailwindcss, which sorts utility classes into Tailwind’s canonical order. Run it before reviewing a diff and the diff gets much smaller.
pnpm test
scripts/test.mjs walks src/ for *.test.ts and runs each through node --experimental-strip-types. No framework, no config, no fixtures.
Two properties are deliberate. Discovery rather than registration means a check written beside the code it covers runs without being listed anywhere. And zero checks is a failure, not a pass — the runner exits non-zero if it finds none, because the whole point of discovery is that it cannot quietly stop finding the checks it is supposed to run.
Twenty checks ship, in three groups: the config files (nine, asserting that files which must agree still do), the pure helpers in src/js/ (booking, schema, textUtils, numbers, tv), and the odd ones out — the icon registry merge, the listing predicates, the booking estimate, the password meter and the CSS tokens.
The constraint that shapes the codebase’s file layout is here: a module importing astro:content, astro:env/server or the DOM cannot be loaded by plain Node. That is why src/js/booking.ts has no imports at all, why _estimate.ts is split from _booking.ts, why _listing.ts is split from _listingFilter.ts, and why src/js/blog.ts is split from blogData.json.ts. The pure half is the checkable half.
To run one check directly:
node --experimental-strip-types src/js/booking.test.ts
pnpm wiki:lint
scripts/wiki-lint.mjs resolves every path:line citation in wiki/ and checks the cited line still contains the symbol the prose names, plus that every [[wikilink]] resolves.
It exists because the wiki/ directory documents subsystems in prose with line references, and prose drifts out from under code silently. This makes that drift a failing command.
If you delete wiki/, drop this from the chain.
tokens.test.ts
Worth calling out because it is the check most likely to surprise you. It fails the build if a .dark block reappears in the stylesheet, and if the focus:outline-hidden / focus-visible:outline-2 pairing comes back.
Both guard against regressions that are invisible in review. A .dark block with no script to set the class is dead CSS shipped to every visitor. And in Tailwind v4, outline-hidden compiles to --tw-outline-style: none while outline-2 reads that variable — so pairing them lets the :focus rule set the value the :focus-visible rule reads, and the focus ring resolves to nothing. It looked correct in the source and drew nothing in the browser.
Adding a check
Write something.test.ts beside the code it covers, using node:assert. It runs on the next pnpm test with nothing to register.
The bar the house rules set: non-trivial logic leaves one runnable check behind — the smallest thing that fails if the logic breaks. Trivial one-liners need none.