/** * Regenerate the Boneyard bone files. * * Boneyard measures a running page, so this needs `pnpm dev` up. It captures exactly * two routes and does not crawl: * * - a real mint page, for the reviews panel's bones * - a /mint/ URL with no prerendered page, for the prerender miss skeleton * * The mint host is looked up from the API rather than hardcoded, so this keeps * working as mints come and go. Both islands render fixture content when the CLI * sets `window.__BONEYARD_BUILD`, so neither capture depends on a relay answering. * * Re-run after any layout change to a review card or to the prerender miss summary. */ import { spawn } from 'node:child_process'; import { rmSync } from 'node:fs'; const WEB_PORT = process.env['WEB_PORT'] ?? '4321'; const WEB_URL = process.env['BONES_URL'] ?? `http://localhost:${WEB_PORT}`; const API_URL = process.env['API_URL'] ?? 'http://127.0.0.1:8787'; /** A host that is never prerendered, so Astro serves the 404 fallback for it. */ const MISS_HOST = 'not-prerendered.boneyard.invalid'; function fail(message) { console.error(`\n bones: ${message}\n`); process.exit(1); } async function reachable(url) { try { const res = await fetch(url, { signal: AbortSignal.timeout(4000) }); return res.ok; } catch { return false; } } if (!(await reachable(WEB_URL))) { fail(`no dev server at ${WEB_URL}. Start one with "pnpm dev", then run this again.`); } /** * The dev server proxies /api to the API in same origin mode, so try it first and * fall back to the API's own address for setups that set PUBLIC_API_URL. */ async function firstMintHost() { for (const base of [WEB_URL, API_URL]) { try { const res = await fetch(`${base}/api/mints`, { signal: AbortSignal.timeout(8000) }); if (!res.ok) continue; const mints = await res.json(); if (Array.isArray(mints) && mints[0]?.host) return mints[0].host; } catch { // Try the next base. } } return null; } const host = await firstMintHost(); if (!host) { fail(`no mints from the API (tried ${WEB_URL} and ${API_URL}). Start it with "pnpm dev:api".`); } const targets = [`${WEB_URL}/mint/${host}`, `${WEB_URL}/mint/${MISS_HOST}`]; console.log(`\n bones: measuring\n ${targets.join('\n ')}\n`); // Passing explicit non-root paths puts the CLI in single page mode: it visits only // these URLs, with no link following and no filesystem route scan. // // --force because Boneyard decides a skeleton is unchanged by hashing the captured // element's innerHTML. Bone geometry depends just as much on CSS, so a styling change // alone would silently reuse stale bones. There are only two skeletons here, so // always recapturing costs a few seconds and removes the trap. const child = spawn('boneyard-js', ['build', '--force', ...targets], { stdio: 'inherit', shell: false }); child.on('error', (error) => fail(`could not run boneyard-js (${error.message}).`)); child.on('exit', (code) => { // Boneyard also writes a registry that calls registerBones() through a framework // adapter, defaulting to boneyard-js/react. We have no React and no // components: the islands import the .bones.json files directly. Leaving an // unresolvable import in src/ would only break typechecking. rmSync(new URL('../src/bones/registry.ts', import.meta.url), { force: true }); rmSync(new URL('../src/bones/registry.js', import.meta.url), { force: true }); process.exit(code ?? 1); });