import { readFileSync, readdirSync, statSync } from 'node:fs'; import { dirname, join } from 'node:path'; const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8'); const pages = [ 'index.html', 'full-guide/index.html', 'summary/index.html', 'models/index.html', 'agents/index.html', 'skills/index.html', 'rules/index.html', 'skills-review/index.html', 'hands-on/starter/index.html', 'hands-on/rules/index.html', ]; const walk = (path) => readdirSync(path).flatMap((name) => { const child = join(path, name); return statSync(child).isDirectory() ? walk(child) : [child]; }); const definitions = (css) => new Set([...css.matchAll(/(--[\w-]+)\s*:/g)].map(([, name]) => name)); const uses = (css) => new Set([...css.matchAll(/var\((--[\w-]+)/g)].map(([, name]) => name)); const builtCss = walk('dist/_astro') .filter((path) => path.endsWith('.css')) .map((path) => readFileSync(path, 'utf8')) .join('\n'); // A declaration nothing references cannot style anything. Dropping the dead ones // before the value scan is what stops a lost value from being "restored" with an // unreferenced token that exists only to put the string back into the sheet. const referenced = uses(builtCss); const liveCss = builtCss.replace(/(--[\w-]+)\s*:[^;}]*[;}]/g, (declaration, name) => referenced.has(name) ? declaration : '', ); const colorPattern = /#[0-9a-fA-F]{3,8}\b|(?:rgba?|hsla?)\([^)]*\)/g; const sizePattern = /\b\d+(?:\.\d+)?(?:px|rem|em|vw|vh)\b/g; // Only the media prelude, so a breakpoint has to be an actual query condition. const mediaPreludes = (builtCss.match(/@media[^{]+/g) || []).join('\n'); const builtValues = { colors: new Set(liveCss.match(colorPattern) || []), sizes: new Set(liveCss.match(sizePattern) || []), breakpoints: new Set(mediaPreludes.match(sizePattern) || []), }; if (!builtCss) throw new Error('Astro build emitted no CSS assets to audit'); // Captured from origin/main after its Astro build. This catches a value that is // silently changed to a nearby token: source token checks cannot see that drift. const baselineValues = JSON.parse(read('.agents/snapshots/built-css-values.json')); for (const page of pages) { const html = read(page); if (!html.includes('name="viewport"')) throw new Error(`${page} lacks a viewport declaration`); if (html.match(/<(script|link)[^>]+(src|href)="https?:[^\"]+"/i)) throw new Error(`${page} has an external runtime dependency`); } for (const file of walk('dist').filter((path) => path.endsWith('.css'))) { const css = readFileSync(file, 'utf8').replace(/\/\*[\s\S]*?\*\//g, ''); if (/@import|src:\s*url\(['"]?https?:|url\(['"]?https?:/i.test(css)) throw new Error(`${file} has an external CSS dependency`); } for (const [kind, values] of Object.entries(baselineValues)) { const missing = values.filter((value) => !builtValues[kind].has(value)); if (missing.length) throw new Error(`built CSS lost ${kind}: ${missing.join(', ')}`); } // Legacy fixtures still ship directly. Resolve every var() from the stylesheets // that page actually links; Astro's tokens cannot mask a broken standalone page. for (const page of pages) { const html = read(page); const linked = [...html.matchAll(/]+rel="stylesheet"[^>]+href="([^"]+)"/gi)].map( ([, href]) => href, ); const styles = linked .filter((href) => !/^https?:/i.test(href)) .map((href) => read(join(dirname(page), href))); const defined = new Set(styles.flatMap((css) => [...definitions(css)])); const unresolved = new Set( styles.flatMap((css) => [...uses(css)].filter((name) => name !== '--score' && !defined.has(name)), ), ); if (unresolved.size) throw new Error(`${page} has unresolved CSS variables: ${[...unresolved].join(', ')}`); } console.log('responsive UI audit passed');