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>
75 lines
2.0 KiB
JavaScript
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`,
|
|
);
|
|
}
|