#!/usr/bin/env node // generate.mjs reads webui/dist/ (the output of `vite build`) and writes // ../admin_assets_gen.go — the baked-in static assets GET /admin and // GET /admin/assets/{hashedname} serve (admin.go's serveAdminPage / // serveAdminAsset). Run via `make admin-ui`, never by hand against a stale // dist/. // // Encoding choice (documented once, here, rather than re-argued at every // call site): every asset body is emitted as a base64 string literal, // decoded once via base64.StdEncoding at package-var initialization // (encoding/base64 — stdlib, Yaegi-safe). This was picked over emitting // each asset as a Go raw-string (backtick) literal because a Vite // production build's minified JS routinely CONTAINS backtick characters // (template literals) and other bytes (control characters in source maps, // non-ASCII in string/comment content) that would need per-byte escaping // to embed safely as Go source text. Base64's alphabet is a fixed, // printable 65-character set — every input byte round-trips through it // with no escaping logic to get wrong, at the well-known cost of ~33% // size inflation. At this bundle's size (a few hundred KB), that // inflation is a non-issue; see the Makefile's admin-ui target for the // measured before/after byte count this trades against. import { createHash } from 'node:crypto' import { readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs' import { dirname, extname, join, relative } from 'node:path' import { fileURLToPath } from 'node:url' const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) const DIST_DIR = join(SCRIPT_DIR, 'dist') const OUT_PATH = join(SCRIPT_DIR, '..', 'admin_assets_gen.go') const ASSETS_SUBDIR = 'assets' /** Content-Type per file extension — every type webui/'s build actually emits, plus common source-map/font fallbacks for a future dependency change. */ const CONTENT_TYPES = { '.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.html': 'text/html; charset=utf-8', '.json': 'application/json; charset=utf-8', '.map': 'application/json; charset=utf-8', '.svg': 'image/svg+xml', '.woff': 'font/woff', '.woff2': 'font/woff2', } function contentTypeFor(filePath) { return CONTENT_TYPES[extname(filePath).toLowerCase()] ?? 'application/octet-stream' } /** walk lists every regular file under dir, recursively, as paths relative to dir (forward-slash separated). */ function walk(dir, base = dir) { const out = [] for (const entry of readdirSync(dir)) { const full = join(dir, entry) if (statSync(full).isDirectory()) { out.push(...walk(full, base)) } else { out.push(relative(base, full).split('\\').join('/')) } } return out } /** * assertNoInlineAssets fails the build if html contains an inline * `