One declaration does three jobs: renders the panel, carries the defaults, names
the values that come back. A module writes no DOM and reads no DOM.
Validation refuses rather than repairs -- unknown type, a renderer its type does
not have, a label that is not text, a default outside its own bounds, a when
naming a key the module never declared. A module that is wrong does not get a
best-effort panel; it does not load, and says why once.
Ordering is by declared relation and stage, never priority numbers, and a cycle
drops only the modules in it. Compatibility is by trait, never model name.
The shell hands a module { composer, values, on, shell } and nothing else: no
node, no document, no kit internals. It cannot style anything because it is
never given anything that could.
catalogue/hide.html puts one good module and five broken ones through the real
shell and shows the result: one panel, five absent, no placeholders. The lint
fixtures make the scanner prove it catches each rule, so a clean scan means
something.
First slice: modules/system/appearance. Two settings, an optional ui.js, and
clicking Light actually turns the app light.
52 lines
1.6 KiB
JavaScript
52 lines
1.6 KiB
JavaScript
// A source scanner for the rule tests.
|
|
//
|
|
// Deliberately dumb: it reads text and matches patterns. Something cleverer
|
|
// would need a parser, and a parser is a thing that can be wrong in ways nobody
|
|
// notices. A false positive here is a comment away from being fixed; a false
|
|
// negative is a rule that quietly stopped being enforced.
|
|
|
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
const SKIP = new Set(["node_modules", ".git", "__pycache__", "tests"]);
|
|
|
|
export function jsFilesIn(root) {
|
|
const out = [];
|
|
let entries;
|
|
try { entries = readdirSync(root); } catch { return out; }
|
|
for (const name of entries) {
|
|
if (SKIP.has(name) || name.startsWith(".")) continue;
|
|
const path = join(root, name);
|
|
if (statSync(path).isDirectory()) out.push(...jsFilesIn(path));
|
|
else if (name.endsWith(".js") && !name.endsWith(".test.js")) out.push(path);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Strip comments so a rule quoted in prose is not a violation. */
|
|
export function code(source) {
|
|
return source
|
|
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
.replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
}
|
|
|
|
export function scanSource(source, rules) {
|
|
const body = code(source);
|
|
const found = [];
|
|
for (const rule of rules) {
|
|
const line = body.split("\n").findIndex((l) => rule.test.test(l));
|
|
if (line !== -1) found.push({ rule: rule.name, why: rule.why, line: line + 1 });
|
|
}
|
|
return found;
|
|
}
|
|
|
|
export function scan(root, rules) {
|
|
const violations = [];
|
|
for (const file of jsFilesIn(root)) {
|
|
for (const found of scanSource(readFileSync(file, "utf8"), rules)) {
|
|
violations.push({ file, ...found });
|
|
}
|
|
}
|
|
return violations;
|
|
}
|