mirror of
https://github.com/hanzoai/store.git
synced 2026-10-30 14:54:46 +00:00
`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]>
58 lines
2.6 KiB
JavaScript
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)
|