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>
59 lines
2.6 KiB
JavaScript
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);
|