Files
panel/controller/.test/gate-parse.mjs
T
2026-07-14 19:19:43 -07:00

125 lines
5.2 KiB
JavaScript

#!/usr/bin/env node
// gate-parse.mjs — static JS parse gate for the panel front-ends.
//
// This codifies the 2026-05-08 blank-page deploy gate: a single syntax error
// anywhere in the giant inline <script> block of new.html/index.html renders
// the whole panel as a blank page, and `go build` cannot catch it because the
// HTML is embedded as opaque bytes (//go:embed). This script extracts every
// <script> block from the served HTML files and parses each one.
//
// Zero dependencies: each block is written to a temp file and checked with
// `node --check` (falls back to acorn if a local install of it exists).
//
// Usage: node gate-parse.mjs
// Exit 0 = every script block in every file parses. Non-zero = at least one
// syntax error (printed with file, block index, and node's error output).
import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join, dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
const staticDir = resolve(here, '../cmd/controller/static');
// new.html + index.html are the two panel UIs the root route can serve;
// login.html is the unauthenticated landing page. All three brick on a
// syntax error, so all three are gated.
const files = ['new.html', 'index.html', 'login.html'];
// Extract inline <script> blocks. Blocks with a src= attribute have no inline
// body worth parsing; blocks with a non-JS type (application/json templates
// etc.) are skipped.
function extractScripts(html) {
const out = [];
const re = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
let m;
while ((m = re.exec(html)) !== null) {
const attrs = m[1];
const body = m[2];
// External local bundles (e.g. /rcc.js?v=hash) are part of the deploy:
// resolve them from static/ and gate their contents too.
const srcMatch = attrs.match(/\bsrc\s*=\s*["']?([^"'\s>]+)/i);
if (srcMatch) {
const src = srcMatch[1];
if (/^(https?:)?\/\//i.test(src)) continue; // never hotlink; skip if ever present
const local = src.replace(/^\//, '').split('?')[0];
try {
const extBody = readFileSync(join(staticDir, local), 'utf8');
const line = html.slice(0, m.index).split('\n').length;
out.push({ body: extBody, line, module: /\btype\s*=\s*["']?module/i.test(attrs), external: local });
} catch (e) {
out.push({ body: `throw new SyntaxError(${JSON.stringify(`missing external script ${local}: ${e.message}`)});(`, line: 0, module: false, external: local });
}
continue;
}
const typeMatch = attrs.match(/\btype\s*=\s*["']?([^"'\s>]+)/i);
const type = typeMatch ? typeMatch[1].toLowerCase() : 'text/javascript';
if (!['text/javascript', 'application/javascript', 'module'].includes(type)) continue;
// Line offset of the block start, for error reporting.
const line = html.slice(0, m.index).split('\n').length;
out.push({ body, line, module: type === 'module' });
}
return out;
}
// Prefer acorn if it happens to be installed next to this suite; otherwise
// use `node --check` on a temp file (always available, needs no npm install).
let acorn = null;
try { acorn = await import('acorn'); } catch { /* fine — node --check path */ }
const tmp = mkdtempSync(join(tmpdir(), 'panel-gate-'));
let failures = 0;
let checkedFiles = 0;
for (const name of files) {
let html;
try {
html = readFileSync(join(staticDir, name), 'utf8');
} catch (e) {
console.error(`FAIL ${name}: cannot read (${e.message})`);
failures++;
continue;
}
checkedFiles++;
const blocks = extractScripts(html);
let fileOk = true;
blocks.forEach((b, i) => {
const label = `${name} script-block ${i + 1}/${blocks.length}${b.external ? ` [external ${b.external}]` : ''} (starts html line ${b.line}, ${b.body.length} chars)`;
try {
if (acorn) {
acorn.parse(b.body, {
ecmaVersion: 'latest',
sourceType: b.module ? 'module' : 'script',
});
} else {
const tf = join(tmp, `${name.replace(/\W/g, '_')}_${i}${b.module ? '.mjs' : '.js'}`);
writeFileSync(tf, b.body);
execFileSync(process.execPath, ['--check', tf], { stdio: ['ignore', 'pipe', 'pipe'] });
}
console.log(`PASS ${label}`);
} catch (e) {
fileOk = false;
failures++;
const detail = (e.stderr ? e.stderr.toString() : e.message).trim();
console.error(`FAIL ${label}\n${detail.split('\n').map(l => ' ' + l).join('\n')}`);
}
});
if (blocks.length === 0) {
// A panel HTML with zero script blocks is itself a broken deploy.
console.error(`FAIL ${name}: 0 inline script blocks found — extraction or file is broken`);
failures++;
fileOk = false;
}
console.log(`${fileOk ? 'OK ' : 'BAD '} ${name}: ${blocks.length} inline script block(s)`);
}
rmSync(tmp, { recursive: true, force: true });
if (failures > 0) {
console.error(`\ngate-parse: ${failures} failure(s) across ${checkedFiles} file(s) — DO NOT DEPLOY`);
process.exit(1);
}
console.log(`\ngate-parse: all script blocks parse (${checkedFiles} files, parser: ${acorn ? 'acorn' : 'node --check'})`);