Files
zeekayandHanzo Dev 5bf85a4faf The CSS check was passing on an empty shell
`postbuild: gui-css-check` read the exported HTML. This site fetches store.json
in the browser, so that HTML is a shell: the check found 29 classes, covered all
29, and reported 100%. Rendering the same three pages finds 227 — and nine of
them had no rule in any sheet the page delivers. A check that passes by not
looking is worse than no check, because it is quoted as evidence.

gui-css-check has --render for exactly this, and rendering needs the export
served, so scripts/css-check.mjs serves out/ on an ephemeral port and renders
one page of each kind — grid, app record, long-form document. Every route is one
of those three; rendering all 202 app pages proves nothing the first did not.

(spawn, not spawnSync: the checker fetches those urls from the same process that
serves them, and a synchronous child blocks the event loop the server answers
on. It waits out its own 60s timeout against a server that can never reply.)

The nine misses are btn-default/sm/lg/outline/ghost and badge + its three
variants — semantic handles @hanzo/ui stamps on a control alongside the
@hanzo/gui atomics that actually paint it. @hanzo/ui ships no rules for them on
purpose; they exist for a host to hook. Verified inert rather than assumed:
stripping every btn-*/badge-* class off a rendered element leaves its computed
background, padding and radius unmoved. So they are allowed in
gui-css-check.json with that note, instead of being given rules that would
restate what gui already does. The store hooks exactly one handle, `.btn`, and
that one is defined in globals.css.

Confirmed the gate actually fails: empty the allow list and it exits 1; restore
it and it exits 0. 227/227 covered.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 18:22:59 -07:00

58 lines
2.6 KiB
JavaScript

/**
* Fail the build when the rendered markup uses a class no delivered sheet defines.
*
* `gui-css-check out/` on its own is a FALSE GREEN here. The store fetches
* store.json in the browser, so the exported HTML is a shell: checking it found
* 29 classes and passed, while the page a user actually gets carries 228 — and
* nine of those had no rule. The check has to look at a rendered page, which is
* what its own `--render` flag is for, and rendering needs the export served.
*
* So: serve out/ on an ephemeral port, render one page of each KIND (grid, app
* record, long-form document — every route is one of those three), and hand the
* urls to gui-css-check. No dev server, no fixtures; this is the artifact that
* ships, measured the way a browser sees it.
*/
import { createServer } from 'node:http'
import { spawn } from 'node:child_process'
import { readFile } from 'node:fs/promises'
import { extname, join, normalize } from 'node:path'
const ROOT = new URL('../out/', import.meta.url).pathname
const TYPES = { '.html': 'text/html', '.css': 'text/css', '.js': 'text/javascript',
'.json': 'application/json', '.woff2': 'font/woff2', '.svg': 'image/svg+xml', '.ico': 'image/x-icon' }
const server = createServer(async (req, res) => {
// Strip the query, refuse to climb out of out/.
const rel = normalize(decodeURIComponent(req.url.split('?')[0])).replace(/^(\.\.[/\\])+/, '')
const file = join(ROOT, rel.endsWith('/') ? rel + 'index.html' : rel)
try {
const body = await readFile(file)
res.writeHead(200, { 'content-type': TYPES[extname(file)] ?? 'application/octet-stream' })
res.end(body)
} catch {
res.writeHead(404).end('not found')
}
})
await new Promise((r) => server.listen(0, '127.0.0.1', r))
const base = `http://127.0.0.1:${server.address().port}`
// One route per kind. `/apps/audio-insight` stands for all 202 — they are the
// same component with different data, and rendering 202 of them proves nothing
// the first one did not.
// spawn, never spawnSync: the checker fetches these urls from THIS process, and
// a synchronous child blocks the event loop the server answers on — it would
// wait out its own timeout against a server that can never reply.
const routes = ['/index.html', '/apps/audio-insight.html', '/terms.html']
const status = await new Promise((resolve) => {
const child = spawn('gui-css-check', routes.flatMap((r) => ['--render', base + r]), {
stdio: 'inherit',
env: { ...process.env, PATH: `${new URL('../node_modules/.bin', import.meta.url).pathname}:${process.env.PATH}` },
})
child.on('close', resolve)
child.on('error', () => resolve(1))
})
server.close()
process.exit(status ?? 1)