Files
initiative/scripts/check-component-props.mjs
Lukas 01b1bba6d6 Replace regex prop counter with TypeScript compiler API
Uses ts.createProgram to parse real AST instead of regex + brace-depth
state machine. Immune to comments, strings, and complex type syntax.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 18:11:34 +01:00

75 lines
2.0 KiB
JavaScript

/**
* Enforce a maximum number of explicitly declared props per component
* interface.
*
* Components should consume shared application state via React context
* providers, not prop drilling. Props are reserved for per-instance
* configuration (a specific data item, a layout variant, a ref).
*
* Only scans component files (not hooks, adapters, etc.) and only
* counts properties declared directly in *Props interfaces — inherited
* or extended HTML attributes are not counted.
*
* Uses the TypeScript compiler API for accurate AST-based counting,
* immune to comments, strings, and complex type syntax.
*/
import { execSync } from "node:child_process";
import { relative } from "node:path";
import ts from "typescript";
const MAX_PROPS = 8;
const files = execSync(
"git ls-files -- 'apps/web/src/components/*.tsx' 'apps/web/src/components/**/*.tsx'",
{ encoding: "utf-8" },
)
.trim()
.split("\n")
.filter(Boolean);
const program = ts.createProgram(files, {
target: ts.ScriptTarget.ESNext,
module: ts.ModuleKind.ESNext,
jsx: ts.JsxEmit.ReactJSX,
strict: true,
noEmit: true,
skipLibCheck: true,
});
let errors = 0;
for (const file of files) {
const sourceFile = program.getSourceFile(file);
if (!sourceFile) continue;
ts.forEachChild(sourceFile, (node) => {
if (!ts.isInterfaceDeclaration(node)) return;
if (!node.name.text.endsWith("Props")) return;
const propCount = node.members.filter((m) =>
ts.isPropertySignature(m),
).length;
if (propCount > MAX_PROPS) {
const rel = relative(process.cwd(), file);
const { line } = sourceFile.getLineAndCharacterOfPosition(node.name.pos);
console.error(
`${rel}:${line + 1}: ${node.name.text} has ${propCount} props (max ${MAX_PROPS})`,
);
errors++;
}
});
}
if (errors > 0) {
console.error(
`\n${errors} component(s) exceed the ${MAX_PROPS}-prop limit. Use React context to reduce props.`,
);
process.exit(1);
} else {
console.log(
`check-component-props: all component interfaces within ${MAX_PROPS}-prop limit`,
);
}