Files
SravanandClaude Opus 4.8 a1f7eef28a build: pre-deploy HTML inline-script syntax guard
A missing brace in a home.html inline <script> caused an app-wide login
outage (the whole block failed to parse, so boot never ran). Nothing in the
deploy path checked HTML syntax. Add tools/check-syntax.js: parses (never
runs) every inline <script> in server/public/*.html with vm and exits non-zero
on any syntax error, reporting the file + line.

Wired in two places:
- Dockerfile RUN after COPY server/ ./ — a syntax error now FAILS the build,
  so it can't deploy.
- redeploy.sh pre-flight — fast local feedback before triggering the remote
  build.

Verified: passes on current pages (16 blocks / 9 files); fails with the exact
location when a brace is removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-11-03 21:33:01 +05:30

59 lines
2.6 KiB
JavaScript

#!/usr/bin/env node
// Pre-deploy syntax guard for the served HTML pages.
//
// WHY: our pages carry large INLINE <script> blocks. A single JavaScript syntax error in one block makes the
// WHOLE block fail to parse — every function/handler it defines becomes undefined. That is exactly what caused
// an app-wide login outage (home.html's `callConnected` listener was missing one `}`: the boot code never ran,
// so after login the splash spun forever). The browser catches it instantly; our deploy did not. This script
// turns that class of bug into a FAILED BUILD (exit 1) instead of a production outage.
//
// It parses — but never executes — every inline <script> in server/public/*.html with Node's `vm`, so it only
// checks SYNTAX (browser globals like `document` are irrelevant to parsing). Runs in the Docker build (see
// Dockerfile, right after `COPY server/ ./`) and locally from redeploy.sh for fast feedback.
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const dir = path.join(__dirname, '..', 'public');
let files;
try {
files = fs.readdirSync(dir).filter((f) => f.toLowerCase().endsWith('.html'));
} catch (e) {
console.error('[check-syntax] cannot read ' + dir + ': ' + e.message);
process.exit(1);
}
// Inline <script> … </script> (skip ones with a src= attribute — those are external files, nothing inline).
const re = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi;
let errors = 0;
let blocks = 0;
for (const f of files) {
const html = fs.readFileSync(path.join(dir, f), 'utf8');
re.lastIndex = 0;
let m;
while ((m = re.exec(html))) {
if (/\bsrc\s*=/i.test(m[1] || '')) continue; // external script
blocks++;
const code = m[2];
// File line where this block's body starts, so the reported line is file-relative (not block-relative).
const openTagEnd = m.index + m[0].indexOf('>') + 1;
const startLine = html.slice(0, openTagEnd).split('\n').length - 1; // 0-indexed offset
try {
new vm.Script(code, { filename: f, lineOffset: startLine });
} catch (e) {
errors++;
const loc = (String(e.stack || '').split('\n')[0] || '').trim(); // e.g. "home.html:4398"
console.error('[check-syntax] ' + f + ' — ' + (e.message || 'syntax error') +
(loc && /:\d+$/.test(loc) ? (' @ ' + loc) : (' (inline <script> near line ' + (startLine + 1) + ')')));
}
}
}
if (errors) {
console.error('[check-syntax] FAILED: ' + errors + ' inline <script> block(s) with syntax errors. Aborting deploy.');
process.exit(1);
}
console.log('[check-syntax] OK: ' + blocks + ' inline <script> block(s) parsed across ' + files.length + ' HTML file(s).');
process.exit(0);