npc-sheet.hbs line 43 read
{{#if (lookup ../skillsByCategory key).length}}
and Handlebars cannot read a property off a subexpression. That is a PARSE
error, so the template never compiled, so every NPC sheet in the system threw
on click — Natives & Threats, the starter's enemy roster, and the whole LAST
ADMISSION cast. #if already treats an empty array as falsy, so the .length was
never doing anything in the first place.
It was invisible to all four existing guards, and the reason is worth writing
down: check-rules reads .mjs, check-behaviour says in its own header that it
cannot reach sheets, and every play-through in this project has driven the
system through its API rather than by clicking. Nobody had ever opened an NPC
sheet, including me, in any of the sessions that "played through" scenarios
full of NPCs.
So: check-templates.mjs, a fifth guard, compiling every .hbs in about a tenth
of a second, wired into the build ahead of check-behaviour. It also names the
specific shape — a property read off a subexpression — because the raw parse
error says only where. Verified it bites by reintroducing the bug. Handlebars
is a devDependency now.
Opening the sheet then showed a second thing nobody had seen: the NPC skill
rows carry four children and were using the character sheet's seven-column
grid, so the name landed in a 13px column and the whole list overlapped
itself. The NPC row now has its own four-column template.
Verified by opening all six LAST ADMISSION cast sheets with an error hook
attached: six rendered, no errors, and Old Marrow reads HP 20/20, major wound
10, armour 2, Brawl 60%, Melee Weapon (Polearm) 60%.
Version 1.0.1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
74 lines
2.9 KiB
JavaScript
74 lines
2.9 KiB
JavaScript
/**
|
|
* TEMPLATE GUARD.
|
|
*
|
|
* Every .hbs in templates/ must compile. This exists because it did not, and shipped:
|
|
* npc-sheet.hbs line 43 read
|
|
*
|
|
* {{#if (lookup ../skillsByCategory key).length}}
|
|
*
|
|
* and Handlebars cannot read a property off a subexpression. That is a PARSE error, so
|
|
* the template never compiled, so every NPC sheet in the system threw on open — the
|
|
* Natives & Threats compendium, the starter's enemy roster, the whole LAST ADMISSION
|
|
* cast. It was invisible to every other guard: check-rules reads .mjs, check-behaviour
|
|
* says in its own header that it cannot reach sheets, and the play-throughs drove the
|
|
* system through its API rather than by clicking, so no NPC sheet was ever opened.
|
|
*
|
|
* The lesson is narrow and cheap to hold: a template is code, and code that is never
|
|
* compiled by the build is code nobody has checked. This compiles all of them in about
|
|
* a tenth of a second.
|
|
*
|
|
* It cannot tell you a template is CORRECT — only that Handlebars will accept it. What
|
|
* a sheet looks like once it has data in it is still verified by opening one.
|
|
*/
|
|
|
|
import Handlebars from "handlebars";
|
|
import { readFile, readdir } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const DIR = path.join(ROOT, "templates");
|
|
|
|
async function hbsFiles(dir) {
|
|
const out = [];
|
|
for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
const full = path.join(dir, e.name);
|
|
if (e.isDirectory()) out.push(...await hbsFiles(full));
|
|
else if (e.name.endsWith(".hbs")) out.push(full);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const files = (await hbsFiles(DIR)).sort();
|
|
const failures = [];
|
|
|
|
for (const file of files) {
|
|
const src = await readFile(file, "utf8");
|
|
const rel = path.relative(ROOT, file);
|
|
try {
|
|
Handlebars.precompile(src);
|
|
} catch (err) {
|
|
// Handlebars reports the line in its message; keep it, it is the useful part.
|
|
failures.push(` ${rel}\n ${String(err.message).split("\n")[0]}`);
|
|
continue;
|
|
}
|
|
// The specific shape that caused this file to exist. Handlebars' own parser catches
|
|
// it, but naming it makes the failure say WHY rather than just where. Comments are
|
|
// blanked first, keeping line numbers, or this file's own explanation of the bug
|
|
// would report the bug.
|
|
const scan = src
|
|
.replace(/\{\{!--[\s\S]*?--\}\}|\{\{![\s\S]*?\}\}/g, m => m.replace(/[^\n]/g, " "));
|
|
const bad = scan.split("\n").findIndex(l => /\)\s*\.[a-zA-Z_]/.test(l));
|
|
if (bad >= 0) {
|
|
failures.push(` ${rel}:${bad + 1}\n property read off a subexpression — `
|
|
+ `Handlebars cannot do (helper x).prop`);
|
|
}
|
|
}
|
|
|
|
if (failures.length) {
|
|
console.error(`check-templates: ${failures.length} template(s) will not compile\n`
|
|
+ failures.join("\n"));
|
|
process.exit(1);
|
|
}
|
|
console.log(`check-templates: OK — ${files.length} templates compile`);
|