Skip to content
AstroCraft Docs
On this theme

Commands & Testing

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

The scripts

  • pnpm dev — the dev server at localhost:4321.
  • pnpm build — a production build to dist/, plus adapter output in .netlify/.
  • pnpm preview — serve the production build locally.
  • pnpm astro — passthrough to the Astro CLI, for astro add and friends.
  • pnpm checkastro check, type-checking .astro and .ts files.
  • pnpm lint — ESLint across the project.
  • pnpm formateslint --fix first, then Prettier.
  • pnpm test — the self-check runner.

The full gate before shipping:

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

Each catches something the others do not, and the build does not run the tests. If you wire one CI step, wire all four.

pnpm build is the one that matters most. Content-schema violations, config typos, unresolvable image paths and broken collection references all surface there rather than in a browser.

The test runner

There is no framework, no config file and no fixtures. scripts/test.mjs finds every *.test.ts anywhere under src/ and runs each in its own Node process:

spawnSync(process.execPath,
  ["--experimental-strip-types", "--disable-warning=ExperimentalWarning", file],
  { cwd: root, stdio: "inherit" });

Node strips the types and runs the file directly. That is why the Node floor is 22.12.0--experimental-strip-types arrived there, and the flag is passed explicitly so the runner works on the floor rather than only on newer releases where stripping is on by default.

If discovery finds zero test files it exits with an error rather than passing. A suite that can quietly disappear is worse than no suite.

Eleven checks ship with the theme, each sitting beside the code it covers:

  • src/js/readingTime.test.ts, rss.test.ts, contact.test.ts, schema.test.ts
  • src/components/ui/_listbox.test.ts
  • src/components/ui/password/strength.test.ts
  • src/components/ui/page-transition/pageTransition.test.ts
  • src/components/ui/distort-image/pixelDistortion.test.ts
  • src/components/Sections/Global/headerScroll.test.ts, localTime.test.ts
  • src/components/Sections/Home/testimonialSlider.test.ts

Run one directly while iterating:

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

Writing a check

The convention is one runnable check per piece of non-trivial logic — the smallest thing that fails if the logic breaks. Plain node:assert, no imports beyond the module under test:

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

assert.equal(readingTime("word ".repeat(200)), 1);
assert.equal(readingTime("word ".repeat(201)), 2);
assert.equal(readingTime(""), 1, "an empty post still reads as one minute");

console.log("readingTime checks passed");

Name it <module>.test.ts, put it beside the module, and it is discovered. Nothing to register.

This shapes how logic gets factored. The header’s scroll rule, the local-time formatter, the page-transition geometry and the pixel-distortion maths all live in plain .ts files beside their components rather than inside frontmatter, specifically so they can be tested without a DOM. When you extract logic for the same reason, keep it free of browser globals and path aliases — that is what lets it run under bare Node.

Trivial one-liners need no check.

Lint and format rules that matter

ESLint runs the recommended JavaScript, TypeScript and Astro configs, plus jsx-a11y-recommended — accessibility problems are lint errors here.

Two rules are downgraded to warnings on purpose: no-explicit-any and no-unused-vars. That is a teaching relaxation for a buyer-facing template, not the bar for your own code. Keep any out and delete unused symbols.

Two carve-outs exist. .astro files turn off no-undef, because Astro’s ambient types are globals. And src/components/ui/** and src/components/svg/** turn off astro/no-exports-from-components, because the primitive contract requires exporting the tv() config from frontmatter.

simple-import-sort orders imports — side-effect imports first, then alphabetised. Run pnpm format rather than sorting by hand.

Prettier is configured at 100 columns, double quotes, two-space indentation, trailing commas everywhere. prettier-plugin-tailwindcss must stay last in the plugin list — it is what orders utility classes. Let it sort and do not fight it.

Dependencies

Ten runtime dependencies, and the list is worth reading once because it tells you what is not here:

Astro, Tailwind CSS with its Vite plugin, tailwind-variants and tailwind-merge, two Fontsource variable families, and three official Astro integrations — MDX, sitemap and the Netlify adapter.

No UI kit. No animation library. No SEO package. No icon package. No mail SDK. No test framework. Those six things are implemented inside the theme, which is why the surface you have to keep updated stays small.

tailwind-merge is a load-bearing peer of tailwind-variants — no source file imports it directly, but tv() uses it for all 94 call sites. Do not remove it because grep finds no import.

pnpm settings

pnpm-workspace.yaml is not a monorepo declaration. It exists for two policies:

allowBuilds denies post-install scripts for @parcel/watcher, esbuild and sharp. None needs to compile for this project to work, and denying them keeps installs fast and free of native toolchain requirements. A notice about ignored build scripts during install is this setting working, not a failure.

minimumReleaseAgeExclude pins the Tailwind packages to the exact version set the theme was built and tested against, including the platform-specific binaries.

Useful one-liners

# type-check without building
pnpm check

# find every use of a token before renaming it
grep -rn "text-foreground" src/

# confirm a keyframe actually shipped
pnpm build && grep -r "@keyframes fade-in-up" dist/_astro/

# run a single check
node --experimental-strip-types src/components/Sections/Global/headerScroll.test.ts

# see the primitive catalog
pnpm dev   # then open http://localhost:4321/examples/ui
NEXT STEPTroubleshooting