/** * 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`, ); }