100 lines
2.4 KiB
JavaScript
100 lines
2.4 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.
|
|
*/
|
|
|
|
import { execSync } from "node:child_process";
|
|
import { readFileSync } from "node:fs";
|
|
import { relative } from "node:path";
|
|
|
|
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);
|
|
|
|
let errors = 0;
|
|
|
|
const propsRegex = /^(?:export\s+)?interface\s+(\w+Props)\s*\{/;
|
|
|
|
for (const file of files) {
|
|
const content = readFileSync(file, "utf-8");
|
|
const lines = content.split("\n");
|
|
|
|
let inInterface = false;
|
|
let interfaceName = "";
|
|
let braceDepth = 0;
|
|
let parenDepth = 0;
|
|
let propCount = 0;
|
|
let startLine = 0;
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
|
|
if (!inInterface) {
|
|
const match = propsRegex.exec(line);
|
|
if (match) {
|
|
inInterface = true;
|
|
interfaceName = match[1];
|
|
braceDepth = 0;
|
|
parenDepth = 0;
|
|
propCount = 0;
|
|
startLine = i + 1;
|
|
}
|
|
}
|
|
|
|
if (inInterface) {
|
|
for (const ch of line) {
|
|
if (ch === "{") braceDepth++;
|
|
if (ch === "}") braceDepth--;
|
|
if (ch === "(") parenDepth++;
|
|
if (ch === ")") parenDepth--;
|
|
}
|
|
|
|
// Count prop lines at brace depth 1 and not inside function params:
|
|
// Matches " propName?: type" and " readonly propName: type"
|
|
if (
|
|
braceDepth === 1 &&
|
|
parenDepth === 0 &&
|
|
/^\s+(?:readonly\s+)?\w+\??\s*:/.test(line)
|
|
) {
|
|
propCount++;
|
|
}
|
|
|
|
if (braceDepth === 0) {
|
|
if (propCount > MAX_PROPS) {
|
|
const rel = relative(process.cwd(), file);
|
|
console.error(
|
|
`${rel}:${startLine}: ${interfaceName} has ${propCount} props (max ${MAX_PROPS})`,
|
|
);
|
|
errors++;
|
|
}
|
|
inInterface = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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`,
|
|
);
|
|
}
|