Files
houseplan-card/scripts/render-layout-read.mjs
T
2026-09-26 09:05:20 +03:00

46 lines
1.8 KiB
JavaScript

#!/usr/bin/env node
// #654: render and 2.5D pure helpers must not force browser layout. This is an
// AST contract, not a regex anchor: formatting and comments cannot satisfy it.
import { readFileSync, readdirSync } from 'node:fs';
import ts from 'typescript';
const forbidden = new Set(['getComputedStyle', 'getBoundingClientRect']);
const source = (path) => ts.createSourceFile(
path, readFileSync(new URL(`../${path}`, import.meta.url), 'utf8'),
ts.ScriptTarget.Latest, true,
);
const callName = (node) => !ts.isCallExpression(node) ? null
: ts.isIdentifier(node.expression) ? node.expression.text
: ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text : null;
const forbiddenCalls = (node) => {
const found = [];
const visit = (child) => {
const name = callName(child);
if (name && forbidden.has(name)) found.push(name);
ts.forEachChild(child, visit);
};
visit(node);
return found;
};
const card = source('src/houseplan-card.ts');
const required = new Set(['render', '_renderBody', 'willUpdate']);
const checked = new Set();
const visit = (node) => {
if (ts.isMethodDeclaration(node) && node.name && ts.isIdentifier(node.name)
&& required.has(node.name.text)) {
checked.add(node.name.text);
const calls = forbiddenCalls(node.body);
if (calls.length) throw new Error(`${node.name.text} forces layout: ${calls.join(', ')}`);
}
ts.forEachChild(node, visit);
};
visit(card);
if ([...required].some((name) => !checked.has(name))) throw new Error('guarded lifecycle method is missing');
for (const file of readdirSync(new URL('../src', import.meta.url)).filter((name) => /^iso-.*\.ts$/.test(name))) {
const calls = forbiddenCalls(source(`src/${file}`));
if (calls.length) throw new Error(`${file} forces layout: ${calls.join(', ')}`);
}
console.log('render/layout-read AST contract: OK');