Commands and checks
The four commands CI runs, what each one catches that the others cannot, and how to measure the editor's JavaScript for yourself.
The gate
pnpm lint && pnpm check && pnpm test && pnpm build
All four, in that order, which is what CI runs.
| Command | Tool | Catches |
|---|---|---|
pnpm lint |
Biome | style and correctness, frontmatter only in .astro files |
pnpm check |
astro check |
types across .astro and .ts, including the template below --- |
pnpm test |
Vitest | around 2,000 cases, including the git layer against scratch repositories |
pnpm build |
Astro | the half no test replaces |
pnpm build is the real check. The two rendering rules are build-time integration hooks, so a route on the wrong side of the static/on-demand line is caught there or nowhere. Content-schema errors and config typos surface here too.
If you took the decision to delete the copied tests, steps two and three still apply — pnpm check simply stops reading them.
Two tooling rules worth knowing
TypeScript is pinned to 6.x. The TS 7 native compiler does not expose the programmatic API the Astro language server drives, and astro check refuses it outright.
Never run biome check --unsafe. Its noNonNullAssertion fix rewrites ! to ?., which is a different program rather than a safer spelling — it changes return types.
Tests
They live in __tests__/ beside the module they cover, and pnpm test discovers them. There is no list to update.
Two of them are the ones you will actually care about as an adopter:
pnpm vitest run src/admin/js/__tests__/boundary.test.ts
Fails if anything inside src/admin/ imports something outside the allowed set, and prints the list of files an adopting site has to touch. That list is the adoption cost, and it is a test output rather than a claim in a document.
pnpm vitest run src/admin/js/__tests__/accountsCli.test.ts
Runs the first-account CLI as a real subprocess, under plain Node. It exists because v1.0.0 shipped one aliased import in that file and locked every fresh install out of its own CMS — and no static scan would have caught it, since a .astro import or a Vite-only feature breaks it identically.
Dev
pnpm dev
The admin is at /admin/. Two things behave differently here from a deployed server, both deliberately:
pushAfterCommitdefaults off, so a save on your laptop cannot reach the real remote by surprise.- The composer’s live draft writes to disk, because there is a working tree to write to. Deployed, the canvas renders the model instead, since an uncommitted write would diverge the checkout from
HEAD.
Restart the dev server after adding a route or changing astro.config.mjs. Astro reads its config once at startup, and the admin’s route table is scanned at config load rather than per request.
Measuring the editor’s JavaScript
The recorded figures are 1,738 bytes raw / 829 gzipped for the shell, and 201,390 / 61,812 for the editing engine on top of it. Most of that 189 kB is the markdown round trip, which is not optional — serializing has to run in the browser, because that is where the spans and the original bytes are.
Re-measure by walking the route’s entry chunk. There is no dist/**/index.html to read <script src> out of, because every admin route renders on demand:
npx astro build && node -e '
const fs=require("fs"),path=require("path"),zlib=require("zlib");
const dir="dist/client/_astro",seen=new Set();
const walk=f=>{if(seen.has(f))return;seen.add(f);
const s=fs.readFileSync(path.join(dir,f),"utf8");
for(const m of s.matchAll(/(?:from|import)\s*"\.\/([^"]+)"/g))walk(m[1]);};
walk(fs.readdirSync(dir).find(f=>f.startsWith("_slug_.astro")));
const all=[...seen].map(f=>fs.readFileSync(path.join(dir,f)));
const raw=all.reduce((a,b)=>a+b.length,0);
console.log([...seen].join("\n"));
console.log(`${raw} bytes raw, ${zlib.gzipSync(Buffer.concat(all),{level:9}).length} gzipped`);'
None of it reaches your public pages — Astro bundles per page. The one thing that is shared is CSS, because Tailwind scans all sources.
Verifying a deploy
Grep dist/ for what your change must emit. After a build, the artifacts worth checking are valid JSON-LD in the head, robots.txt and llms.txt with absolute URLs, sitemap-0.xml listing only indexable routes, and <meta name="build-rev"> on the page that answers /.
Creating an account
node --experimental-strip-types src/admin/js/accounts.cli.ts [email protected] "Your Name" [role]
Role is admin, editor or none, defaulting to admin. The password is read from stdin. See Your first account for what the roles mean and where the store lands. Running it again for an existing address replaces that account’s password, name and role.