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>
This commit is contained in:
1 parent
c9bb9c75d1
commit
a1f7eef28a
4 files changed
+78
-1
No files matched your search
@@ -0,0 +1,58 @@
|
||||
#!/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);
|
||||
Reference in new issue
Block a user