Files
PIG-Demo/scripts/_lib.mjs
T
karti-ai b601511e7f Wordle module, cross-language tests, and the deploy path
The browser engine is a port of the Python one and CI proves it: all 21.2M
(guess, answer) pairs hashed on both sides to the same SHA-256. Six TS tests,
including the duplicate-letter table and the twelve pinned seed vectors that
keep ?seed= permalinks pointing at the same word the recording used.

Word lists are split by how they are used. answers.json is inlined because the
board needs it before first paint to turn a seed into a word, and a fetch there
means a visibly empty board on a cold cache. guesses.json is fetched, because it
is three times larger and only needed the first time somebody presses Enter;
until it lands, validation falls back to the answer list, which accepts strictly
fewer words. The failure mode is 'your real word was briefly rejected', not 'a
non-word was accepted' — the right way round.

The solver runs in a worker constructed from a same-origin module URL, never
Vite's ?worker&inline: that yields a blob:, and production CSP has no
worker-src, so it falls back to default-src 'self' and the worker is blocked
with no console error. It would fail in production only.

deploy.sh smoke-tests the real public hostname from the deploying machine and
fails on a body under 1 kB, because the bind bug's signature is a valid
certificate over an empty 200 and a local --resolve check passes anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
2026-08-28 15:53:25 -07:00

832 lines
30 KiB
JavaScript

/**
* Shared plumbing for the contract scripts.
*
* Every `scripts/check-*.mjs` is a gate, not a linter: it exits non-zero with a
* message naming the file and the numbered rule that was broken, so a failure
* in CI reads as an instruction rather than a puzzle.
*
* The demo contract lives in TypeScript and these scripts are plain Node, so
* most of what follows is a small, deliberately dumb TypeScript reader: find a
* named declaration, brace-match its object/array literal, strip the two TS-only
* forms a literal can legally carry (`as const`, `satisfies X`), and evaluate it
* in a bare `vm` context.
*
* That reader is honest about its limits. A literal that references an imported
* constant does not evaluate, and the scripts FAIL rather than skip: the whole
* point of `meta` and `reward.components` is that a human can read the numbers
* off the page beside the code, so a weight hidden behind an indirection is a
* contract problem, not a tooling problem.
*/
import fs from 'node:fs';
import path from 'node:path';
import http from 'node:http';
import zlib from 'node:zlib';
import { fileURLToPath } from 'node:url';
import vm from 'node:vm';
export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
export const abs = (...parts) => path.join(ROOT, ...parts);
export const rel = (p) => path.relative(ROOT, p) || '.';
export const exists = (p) => fs.existsSync(p);
export const read = (p) => fs.readFileSync(p, 'utf8');
export const readJson = (p) => JSON.parse(read(p));
/* ------------------------------------------------------------------ output */
const ESC = '\u001b[';
const COLOUR = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
const paint = (code, s) => (COLOUR ? `${ESC}${code}m${s}${ESC}0m` : String(s));
export const bold = (s) => paint('1', s);
export const dim = (s) => paint('2', s);
export const red = (s) => paint('31', s);
export const green = (s) => paint('32', s);
export const yellow = (s) => paint('33', s);
export const cyan = (s) => paint('36', s);
/**
* Collects failures instead of throwing on the first one.
*
* A contract check that dies on failure #1 makes the author fix and re-run
* seven times. Every rule that can still be evaluated is evaluated.
*/
export class Report {
/** @param {string} title */
constructor(title) {
this.title = title;
/** @type {{file: string, rule: string, message: string}[]} */
this.failures = [];
/** @type {string[]} */
this.warnings = [];
/** @type {string[]} */
this.staticNotes = [];
this.passed = 0;
}
/**
* @param {string} file repo-relative path the reader should open
* @param {string} rule the numbered rule, e.g. 'rule 10 (reward weights)'
* @param {string} message what is actually wrong
*/
fail(file, rule, message) {
this.failures.push({ file, rule, message });
return false;
}
/** Assert, recording either a pass or a precise failure. */
check(condition, file, rule, message) {
if (condition) {
this.passed += 1;
return true;
}
return this.fail(file, rule, message);
}
warn(message) {
this.warnings.push(message);
}
/** Record that a rule was verified by reading source, not by running it. */
staticOnly(message) {
this.staticNotes.push(message);
}
/** Prints the report and exits the process. Never returns. */
finish() {
const out = [];
if (this.staticNotes.length) {
out.push('');
out.push(dim('Checked by reading source, not by executing it:'));
for (const n of this.staticNotes) out.push(dim(` - ${n}`));
}
if (this.warnings.length) {
out.push('');
for (const w of this.warnings) out.push(`${yellow('warn')} ${w}`);
}
if (this.failures.length) {
out.push('');
for (const f of this.failures) {
out.push(`${red('FAIL')} ${bold(f.file)}`);
out.push(` ${cyan(f.rule)}: ${f.message}`);
}
out.push('');
out.push(red(`${this.title}: ${this.failures.length} failure(s), ${this.passed} check(s) passed.`));
console.error(out.join('\n'));
process.exit(1);
}
out.push('');
out.push(green(`${this.title}: ${this.passed} check(s) passed.`));
console.log(out.join('\n'));
process.exit(0);
}
}
/** Fatal error that is the script's own problem, not the repo's. */
export function die(message) {
console.error(`${red('FAIL')} ${message}`);
process.exit(1);
}
/* ------------------------------------------------------- filesystem walking */
/**
* @param {string} dir
* @param {(file: string) => boolean} [filter]
* @returns {string[]} absolute paths, depth-first, stable order
*/
export function walk(dir, filter = () => true) {
if (!exists(dir)) return [];
/** @type {string[]} */
const out = [];
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1));
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === 'node_modules' || entry.name === '__pycache__') continue;
out.push(...walk(full, filter));
} else if (entry.isFile() && filter(full)) {
out.push(full);
}
}
return out;
}
export const DEMOS_DIR = abs('src', 'demos');
/** Every directory under `src/demos`, including `_template`. */
export function allDemoDirs() {
if (!exists(DEMOS_DIR)) return [];
return fs
.readdirSync(DEMOS_DIR, { withFileTypes: true })
.filter((e) => e.isDirectory())
.map((e) => e.name)
.sort();
}
/** Shippable demos: a leading underscore marks scaffolding, not a demo. */
export function demoSlugs() {
return allDemoDirs().filter((name) => !name.startsWith('_'));
}
/* ----------------------------------------------------- the small TS reader */
/**
* Splits source into code and non-code (string literal / comment) spans.
*
* Everything below works on this rather than on raw regex, because the metas
* are full of prose: a `description` containing the words "as an executive"
* is otherwise indistinguishable from a TypeScript `as` assertion.
*
* @param {string} src
* @returns {{code: boolean, text: string}[]}
*/
export function segment(src) {
/** @type {{code: boolean, text: string}[]} */
const out = [];
let codeStart = 0;
let i = 0;
const flushCode = (end) => {
if (end > codeStart) out.push({ code: true, text: src.slice(codeStart, end) });
};
while (i < src.length) {
const c = src[i];
const next = src[i + 1];
if (c === '/' && next === '/') {
flushCode(i);
const nl = src.indexOf('\n', i);
const end = nl === -1 ? src.length : nl;
out.push({ code: false, text: src.slice(i, end) });
i = codeStart = end;
continue;
}
if (c === '/' && next === '*') {
flushCode(i);
const close = src.indexOf('*/', i + 2);
const end = close === -1 ? src.length : close + 2;
out.push({ code: false, text: src.slice(i, end) });
i = codeStart = end;
continue;
}
if (c === "'" || c === '"' || c === '`') {
flushCode(i);
const end = endOfString(src, i) + 1;
out.push({ code: false, text: src.slice(i, end) });
i = codeStart = end;
continue;
}
i += 1;
}
flushCode(src.length);
return out;
}
/** Index of the closing quote of the string literal starting at `start`. */
function endOfString(src, start) {
const quote = src[start];
for (let j = start + 1; j < src.length; j += 1) {
const c = src[j];
if (c === '\\') {
j += 1;
continue;
}
if (quote === '`' && c === '$' && src[j + 1] === '{') {
j = scanBalanced(src, j + 1) - 1;
continue;
}
if (c === quote) return j;
}
throw new Error(`unterminated string literal starting at offset ${start}`);
}
/**
* End offset (exclusive) of the bracket pair opening at `start`.
* Skips strings and comments, and recurses through `${...}` in templates.
*/
export function scanBalanced(src, start) {
const open = src[start];
const close = open === '{' ? '}' : open === '[' ? ']' : open === '(' ? ')' : null;
if (!close) throw new Error(`offset ${start} is ${JSON.stringify(open)}, not an opening bracket`);
let depth = 0;
for (let i = start; i < src.length; i += 1) {
const c = src[i];
const next = src[i + 1];
if (c === '/' && next === '/') {
const nl = src.indexOf('\n', i);
if (nl === -1) break;
i = nl;
continue;
}
if (c === '/' && next === '*') {
const end = src.indexOf('*/', i + 2);
i = end === -1 ? src.length : end + 1;
continue;
}
if (c === "'" || c === '"' || c === '`') {
i = endOfString(src, i);
continue;
}
if (c === open) depth += 1;
else if (c === close) {
depth -= 1;
if (depth === 0) return i + 1;
}
}
throw new Error(`unbalanced ${open} starting at offset ${start}`);
}
/** Offsets of every code (non-string, non-comment) span in `src`. */
function codeSpans(src) {
const spans = [];
let at = 0;
for (const s of segment(src)) {
spans.push({ code: s.code, start: at, end: at + s.text.length });
at += s.text.length;
}
return spans;
}
/**
* Finds the literal that follows `anchor` and returns its source text.
*
* @param {string} src
* @param {RegExp} anchor must match immediately before the opening bracket
* @param {'{' | '['} open
* @returns {string | null}
*/
export function literalsAfter(src, anchor, open = '{') {
const flags = anchor.flags.includes('g') ? anchor.flags : `${anchor.flags}g`;
const re = new RegExp(anchor.source, flags);
const spans = codeSpans(src);
const isCode = (offset) => {
const hit = spans.find((s) => offset >= s.start && offset < s.end);
return hit ? hit.code : true;
};
const found = [];
let match;
while ((match = re.exec(src)) !== null) {
// The anchor may legally match inside a comment or a string \u2014 a docblock
// that quotes `const meta = {`. Only a match in real code counts.
if (!isCode(match.index)) continue;
const from = src.indexOf(open, match.index + Math.max(match[0].length - 1, 0));
if (from === -1) continue;
const between = src.slice(match.index + match[0].length, from);
if (/[;=}]/.test(between)) continue;
const end = scanBalanced(src, from);
found.push({ text: src.slice(from, end), start: from, end });
// Skip past this literal so a nested anchor of the same name is not
// reported as a second, overlapping hit.
re.lastIndex = end;
}
return found;
}
/** First literal following `anchor`, or null. See {@link literalsAfter}. */
export function literalAfter(src, anchor, open = '{') {
const [first] = literalsAfter(src, anchor, open);
return first ? first.text : null;
}
/** Marker key on the stand-in an unresolvable identifier evaluates to. */
export const UNRESOLVED = '__pigUnresolvedIdentifier__';
export const isUnresolved = (v) => Boolean(v) && typeof v === 'object' && UNRESOLVED in v;
/**
* A sandbox in which every free identifier resolves to a labelled stand-in.
*
* `RewardSpec.source.code` is legitimately an identifier — the Python is
* imported with `?raw` and cannot exist in plain Node — so the reward literal
* must be readable WITHOUT its `code`. Strict mode is still the default: a
* weight that turns out to be a stand-in is a contract failure, not a skip.
*/
function lenientSandbox() {
return new Proxy(Object.create(null), {
has: () => true,
get: (_target, key) => {
if (key === Symbol.unscopables) return undefined;
if (typeof key !== 'string') return undefined;
return { [UNRESOLVED]: key };
},
});
}
/**
* Evaluates a TypeScript object/array literal as plain data.
*
* `as const` and `satisfies T` are stripped from code spans only. Anything else
* a literal might carry — an identifier, a call, a spread of an import — throws,
* and callers turn that into a contract failure with the file named. Pass
* `{lenient: true}` to get stand-ins for free identifiers instead.
*
* @param {string} text
* @param {string} label
* @param {{lenient?: boolean}} [options]
*/
export function evalLiteral(text, label, options = {}) {
const js = segment(text)
.map((s) =>
s.code
? s.text
.replace(/\bas\s+const\b/g, '')
.replace(/\bsatisfies\s+[A-Za-z_$][\w$.]*(?:<[^>]*>)?(?:\[\])*/g, '')
.replace(/\bas\s+[A-Za-z_$][\w$.]*(?:<[^>]*>)?(?:\[\])*/g, '')
: s.text,
)
.join('');
try {
const sandbox = options.lenient ? lenientSandbox() : Object.create(null);
const value = vm.runInNewContext(`(${js})`, sandbox, { timeout: 2000 });
return { ok: true, value, error: null };
} catch (error) {
return {
ok: false,
value: null,
error:
`the ${label} literal did not evaluate as plain data (${error.message}). ` +
'These literals are read by tooling and by humans reading the page beside the code, ' +
'so they must be written out, not assembled from imported constants.',
};
}
}
/** Members of a string-union type alias, e.g. `export type Vertical = 'a' | 'b'`. */
export function parseStringUnion(src, typeName) {
const match = src.match(new RegExp(`\\btype\\s+${typeName}\\s*=([^;]+);`));
if (!match || !match[1]) return null;
const members = [...match[1].matchAll(/'([^']+)'/g)].map((m) => m[1]);
return members.length ? members : null;
}
/* ------------------------------------------------------------ demo metadata */
const META_ANCHORS = [
// `export default defineMeta({...})` is the shape the template uses, and the
// one the registry's glob reads. It has to come first: a file can also carry
// a `meta:` key inside something else, and that would win on file order.
/\bdefineMeta\s*(?:<[^>]*>)?\s*\(\s*/,
/(?:export\s+)?const\s+meta\s*(?::\s*[^=]+)?=\s*/,
/(?:export\s+)?const\s+[A-Za-z_$][\w$]*Meta\s*(?::\s*[^=]+)?=\s*/,
/export\s+default\s*/,
/\bmeta\s*:\s*/,
];
const CANDIDATE_META_FILES = ['meta.ts', 'meta.tsx', 'demo.tsx', 'demo.ts', 'index.ts', 'index.tsx'];
/**
* Reads one demo's `DemoMeta` out of its source.
* @param {string} slug
* @returns {{file: string, meta: Record<string, any>} | {file: string, error: string}}
*/
export function loadMeta(slug) {
const dir = path.join(DEMOS_DIR, slug);
const tried = [];
for (const name of CANDIDATE_META_FILES) {
const file = path.join(dir, name);
if (!exists(file)) continue;
tried.push(rel(file));
const src = read(file);
for (const anchor of META_ANCHORS) {
let text;
try {
text = literalAfter(src, anchor, '{');
} catch (error) {
return { file: rel(file), error: `could not brace-match the meta literal: ${error.message}` };
}
if (!text) continue;
const result = evalLiteral(text, 'DemoMeta');
if (!result.ok) return { file: rel(file), error: result.error };
if (result.value && typeof result.value === 'object' && 'slug' in result.value) {
return { file: rel(file), meta: result.value };
}
}
}
return {
file: tried[0] ?? rel(path.join(dir, 'meta.ts')),
error:
tried.length === 0
? `no meta source found; expected one of ${CANDIDATE_META_FILES.join(', ')} in ${rel(dir)}`
: `no DemoMeta object literal with a \`slug\` key found in ${tried.join(', ')}`,
};
}
/** Every demo whose meta could be read, keyed by directory name. */
export function loadAllMetas() {
/** @type {Map<string, Record<string, any>>} */
const metas = new Map();
/** @type {{slug: string, file: string, error: string}[]} */
const errors = [];
for (const slug of demoSlugs()) {
const result = loadMeta(slug);
if ('meta' in result) metas.set(slug, result.meta);
else errors.push({ slug, file: result.file, error: result.error });
}
return { metas, errors };
}
/** Every .ts/.tsx file that belongs to one demo. */
export function demoFiles(slug) {
return walk(path.join(DEMOS_DIR, slug), (f) => /\.tsx?$/.test(f));
}
/**
* Finds one named literal anywhere inside a demo's own source.
*
* A demo is free to put `reward` in `reward.ts` or inline it in `demo.tsx`;
* the contract is about the values, not the file layout. First match in
* filename order wins, and the file it came from is returned so failures can
* name it.
*
* @param {string} slug
* @param {RegExp[]} anchors
* @param {'{' | '['} open
* @param {string} label
* @param {{lenient?: boolean}} [options]
*/
export function findInDemo(slug, anchors, open, label, options = {}) {
for (const file of demoFiles(slug)) {
const src = read(file);
for (const anchor of anchors) {
let text = null;
try {
text = literalAfter(src, anchor, open);
} catch (error) {
return { file: rel(file), error: `could not brace-match the ${label} literal: ${error.message}` };
}
if (!text) continue;
const result = evalLiteral(text, label, options);
if (!result.ok) return { file: rel(file), error: result.error, text };
return { file: rel(file), value: result.value, text };
}
}
return {
file: null,
error: `no ${label} literal found in any .ts/.tsx file under ${rel(path.join(DEMOS_DIR, slug))}`,
};
}
/* ------------------------------------------------------------------ verticals */
export const VERTICALS_FILE = abs('src', 'content', 'verticals.ts');
const VERTICAL_ANCHORS = [
/(?:export\s+)?const\s+VERTICALS\s*(?::\s*[^=]+)?=\s*/,
/(?:export\s+)?const\s+verticals\s*(?::\s*[^=]+)?=\s*/,
];
const asVertical = (id, v) => ({
id,
title: v?.title ?? v?.label ?? v?.name ?? null,
// `anxiety` is this repo's field for the one line a vertical page leads
// with, and it is the only prose in a VerticalEntry short enough to be a
// meta description.
description: v?.description ?? v?.blurb ?? v?.tagline ?? v?.summary ?? v?.anxiety ?? null,
});
/**
* The vertical records the site groups demos by.
*
* Tolerant about field names on purpose: this file belongs to another lane, and
* a prerender that hard-codes `label` then silently bakes an empty <title> when
* the author wrote `name` is worse than one that looks for both.
*/
export function loadVerticals() {
if (!exists(VERTICALS_FILE)) return { list: [], error: `${rel(VERTICALS_FILE)} does not exist` };
const src = read(VERTICALS_FILE);
for (const anchor of VERTICAL_ANCHORS) {
for (const open of ['[', '{']) {
let text = null;
try {
text = literalAfter(src, anchor, open);
} catch {
text = null;
}
if (!text) continue;
const result = evalLiteral(text, 'verticals');
if (!result.ok) return { list: [], error: result.error };
const value = result.value;
const list = Array.isArray(value)
? value.map((v) => asVertical(v?.id ?? v?.slug ?? v?.key, v))
: value && typeof value === 'object'
? Object.entries(value).map(([id, v]) => asVertical(id, v))
: [];
const clean = list.filter((v) => typeof v.id === 'string' && v.id.length > 0);
if (clean.length) return { list: clean, error: null };
}
}
return { list: [], error: `no \`verticals\` or \`VERTICALS\` literal found in ${rel(VERTICALS_FILE)}` };
}
/* --------------------------------------------------------------------- traces */
export const MANIFEST_FILE = abs('public', 'traces', 'manifest.json');
/**
* Normalises `public/traces/manifest.json` into `slug -> {runs, extras}`.
*
* Four shapes are accepted because the manifest is authored by hand and all
* four are things a reasonable person writes. Anything else fails loudly rather
* than quietly reporting zero runs, which would turn rule 7 into a no-op.
*/
export function loadManifest() {
if (!exists(MANIFEST_FILE)) return { byDemo: new Map(), error: `${rel(MANIFEST_FILE)} does not exist` };
let raw;
try {
raw = readJson(MANIFEST_FILE);
} catch (error) {
return { byDemo: new Map(), error: `${rel(MANIFEST_FILE)} is not valid JSON: ${error.message}` };
}
/** @type {Map<string, {runs: any[], extras: Record<string, any>}>} */
const byDemo = new Map();
// Unwrap the two envelopes the shell's own reader accepts. `{runs: [...]}`
// has to be unwrapped BEFORE the record branch below, or "runs" is read as a
// demo slug and every real demo reports zero runs.
let container = raw;
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
if (raw.demos && typeof raw.demos === 'object') container = raw.demos;
else if (Array.isArray(raw.runs)) container = raw.runs;
}
if (Array.isArray(container)) {
// A flat array of runs, each carrying its own `demo`/`slug`.
for (const run of container) {
const slug = run?.demo ?? run?.slug;
if (typeof slug !== 'string') continue;
if (!byDemo.has(slug)) byDemo.set(slug, { runs: [], extras: {} });
byDemo.get(slug).runs.push(run);
}
} else if (container && typeof container === 'object') {
for (const [slug, value] of Object.entries(container)) {
if (Array.isArray(value)) byDemo.set(slug, { runs: value, extras: {} });
else if (value && typeof value === 'object' && Array.isArray(value.runs)) {
const { runs, ...extras } = value;
byDemo.set(slug, { runs, extras });
}
}
}
if (byDemo.size === 0) {
return {
byDemo,
error:
`${rel(MANIFEST_FILE)} did not parse into any demo runs. Expected one of: ` +
'{"<slug>": RunRef[]}, {"<slug>": {"runs": RunRef[], ...}}, {"demos": {...}}, ' +
'or a flat RunRef[] where each run carries a "demo" field.',
};
}
return { byDemo, error: null };
}
/** Resolve a site-absolute trace path ('/traces/x.json') to a disk path. */
export function traceFile(sitePath) {
const clean = String(sitePath).split('?')[0].split('#')[0];
return abs('public', clean.replace(/^\/+/, ''));
}
/* -------------------------------------------------------------- static server */
const MIME = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.mjs': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.webp': 'image/webp',
'.avif': 'image/avif',
'.ico': 'image/x-icon',
'.woff2': 'font/woff2',
'.woff': 'font/woff',
'.ttf': 'font/ttf',
'.txt': 'text/plain; charset=utf-8',
'.map': 'application/json; charset=utf-8',
'.xml': 'application/xml; charset=utf-8',
};
/**
* Serves a directory the way the deploy host will: a real file if one exists,
* `index.html` otherwise.
*
* Prerender MUST hit the SPA fallback, so this deliberately does not 404 on an
* unknown route — but it DOES 404 on a missing file under a path with an
* extension, because serving HTML where a script was requested is how you
* prerender a blank page and never find out why.
*
* @param {string} dir
* @returns {Promise<{origin: string, close: () => Promise<void>}>}
*/
export function serveStatic(dir) {
const root = path.resolve(dir);
const server = http.createServer((req, res) => {
const url = new URL(req.url ?? '/', 'http://localhost');
const decoded = decodeURIComponent(url.pathname);
const safe = path.normalize(decoded).replace(/^(\.\.[/\\])+/, '');
let file = path.join(root, safe);
if (!file.startsWith(root)) {
res.writeHead(403).end('forbidden');
return;
}
if (exists(file) && fs.statSync(file).isDirectory()) file = path.join(file, 'index.html');
if (!exists(file)) {
if (/\.[a-z0-9]+$/i.test(safe)) {
res.writeHead(404, { 'content-type': 'text/plain' }).end(`not found: ${safe}`);
return;
}
file = path.join(root, 'index.html');
}
const body = fs.readFileSync(file);
res.writeHead(200, {
'content-type': MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream',
'content-length': body.length,
'cache-control': 'no-store',
});
res.end(body);
});
return new Promise((resolve, reject) => {
server.on('error', reject);
server.listen(0, '127.0.0.1', () => {
const address = server.address();
resolve({
origin: `http://127.0.0.1:${address.port}`,
close: () => new Promise((done) => server.close(() => done())),
});
});
});
}
/* ------------------------------------------------------------------- routing */
/** The public origin the baked tags point at. */
export const SITE_ORIGIN = (process.env.PIG_DEMO_ORIGIN ?? 'https://demo.primeintellectgrowth.com').replace(/\/+$/, '');
/**
* Every route pattern the app declares, read out of the router source.
*
* Hard-coding `['/', '/d/:slug']` here is the exact failure this function
* exists to avoid: the router moves, prerender keeps emitting the old paths,
* and every shared link previews as the homepage again — which is the bug
* prerendering was added to fix in the first place.
*/
export function discoverRoutePatterns() {
const files = walk(abs('src'), (f) => /\.tsx?$/.test(f));
/** @type {Set<string>} */
const patterns = new Set();
for (const file of files) {
const src = read(file);
for (const m of src.matchAll(/<Route\b[^>]*?\bpath\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*['"]([^'"]*)['"]\s*\})/g)) {
const value = m[1] ?? m[2] ?? m[3];
if (typeof value === 'string') patterns.add(value);
}
// `path:` is a common key name, so only trust it in a file that is
// demonstrably a router config.
if (/createBrowserRouter|createHashRouter|createMemoryRouter|RouteObject/.test(src)) {
for (const m of src.matchAll(/\bpath\s*:\s*(?:"([^"]*)"|'([^']*)')/g)) {
const value = m[1] ?? m[2];
if (typeof value === 'string') patterns.add(value);
}
}
}
return [...patterns];
}
/**
* Which set fills a pattern's one parameter.
*
* Decided by the STATIC segment in front of it, not by the parameter's name.
* This router calls both of them `:slug` — `demos/:slug` and `verticals/:slug`
* — so a name-based reader confidently prerenders `/verticals/wordle`, twelve
* 404s, and no vertical pages at all.
*/
function fillFor(pattern) {
const head = pattern.replace(/^\/+/, '').split('/')[0]?.toLowerCase() ?? '';
if (/^(demos?|d)$/.test(head)) return 'demo';
if (/^(verticals?|v|sectors?)$/.test(head)) return 'vertical';
const param = pattern.match(/:([A-Za-z0-9_]+)/)?.[1] ?? '';
if (/^(vertical|verticalId|sector|category)$/.test(param)) return 'vertical';
if (/^(slug|demo|demoSlug|id)$/.test(param)) return 'demo';
return null;
}
/**
* Expands the router's patterns against the real data into concrete paths.
*
* Child paths are taken as written. This router nests everything exactly one
* level under `/`, so a child's `path` is already the full path; a second level
* of nesting would need the parent prefix joined on, and this would silently
* emit the wrong routes rather than fail. If you nest deeper, fix this.
*
* @param {{metas: Map<string, any>, verticals: {id: string}[]}} data
* @returns {{routes: {path: string, kind: string, slug?: string, id?: string}[], errors: string[]}}
*/
export function expandRoutes(data) {
const patterns = discoverRoutePatterns();
/** @type {string[]} */
const errors = [];
/** @type {Map<string, any>} */
const routes = new Map();
const add = (p, record) => {
const normalised = p === '/' || p === '' ? '/' : `/${p.replace(/^\/+|\/+$/g, '')}`;
if (!routes.has(normalised)) routes.set(normalised, { path: normalised, ...record });
};
if (patterns.length === 0) {
errors.push(
'no route patterns found under src/. Looked for `<Route path=...>` and, in files that mention ' +
'createBrowserRouter/RouteObject, `path: ...`. Refusing to guess a route list.',
);
return { routes: [], errors };
}
const demoSlugList = [...data.metas.keys()];
const verticalIds = data.verticals.map((v) => v.id);
for (const pattern of patterns) {
if (pattern.includes('*')) continue; // the catch-all becomes 404.html, not a route
const params = [...pattern.matchAll(/:([A-Za-z0-9_]+)\??/g)].map((m) => m[1]);
if (params.length === 0) {
add(pattern, pattern === '/' || pattern === '' ? { kind: 'home' } : { kind: 'static' });
continue;
}
if (params.length > 1) {
errors.push(`route pattern "${pattern}" has more than one parameter; prerender cannot expand it.`);
continue;
}
const fill = fillFor(pattern);
const substitute = (value) => pattern.replace(/:[A-Za-z0-9_]+\??/, value);
if (fill === 'vertical') {
if (!verticalIds.length) errors.push(`route "${pattern}" needs verticals, but none were readable from ${rel(VERTICALS_FILE)}.`);
for (const id of verticalIds) add(substitute(id), { kind: 'vertical', id });
} else if (fill === 'demo') {
if (!demoSlugList.length) errors.push(`route "${pattern}" needs demos, but no demo meta was readable under src/demos/.`);
for (const slug of demoSlugList) add(substitute(slug), { kind: 'demo', slug });
} else {
errors.push(
`route pattern "${pattern}" has a parameter prerender cannot fill. Put it under /demos/ or ` +
'/verticals/, or teach fillFor() in scripts/_lib.mjs about it.',
);
}
}
if (!routes.has('/')) add('/', { kind: 'home' });
return { routes: [...routes.values()].sort((a, b) => (a.path < b.path ? -1 : 1)), errors };
}
/* ----------------------------------------------------------------- sizes */
export const gzipSize = (buf) => zlib.gzipSync(buf, { level: 9 }).length;
export const kb = (bytes) => `${(bytes / 1024).toFixed(1)} kB`;
/** Padded plain-text table. Beats taking a dependency to print eight rows. */
export function table(headers, rows) {
const all = [headers, ...rows];
const widths = headers.map((_, i) => Math.max(...all.map((r) => String(r[i] ?? '').length)));
const line = (cells, pad = ' ') =>
cells
.map((c, i) => (i === 0 ? String(c ?? '').padEnd(widths[i], pad) : String(c ?? '').padStart(widths[i], pad)))
.join(' ');
return [line(headers), line(widths.map((w) => '-'.repeat(w)), '-'), ...rows.map((r) => line(r))].join('\n');
}