- Create validate-handlers.js script to check for undefined event handlers - Add npm run validate:handlers command - Add prebuild hook to run validation before builds - Add ESLint no-undef rule to catch undefined references - Add documentation in scripts/README-validation.md Prevents issues like 'ReferenceError: handleSaveEditGuestbook is not defined' by validating all onClick/onChange/onSubmit handlers are defined before use. The script: - Scans all React components for event handlers - Verifies functions are defined in component scope - Excludes props and imported functions - Runs automatically before builds - Can be run manually: npm run validate:handlers
149 lines
4.4 KiB
JavaScript
Executable File
149 lines
4.4 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Validation script to check for undefined handler functions
|
|
* This script scans React components for onClick handlers and verifies
|
|
* that the referenced functions are defined in the component scope.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { execSync } = require('child_process');
|
|
|
|
const COMPONENT_DIR = path.join(__dirname, '../src/components');
|
|
const API_DIR = path.join(__dirname, '../src/app');
|
|
|
|
// Patterns to match handler references
|
|
const HANDLER_PATTERNS = [
|
|
/onClick\s*=\s*\{?\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\}?/g,
|
|
/onChange\s*=\s*\{?\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\}?/g,
|
|
/onSubmit\s*=\s*\{?\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\}?/g,
|
|
/onClick\s*=\s*\([^)]*\)\s*=>\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g,
|
|
];
|
|
|
|
// Patterns to match function definitions
|
|
const FUNCTION_PATTERNS = [
|
|
/const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:\([^)]*\)\s*=>|function)/g,
|
|
/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g,
|
|
/const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*useCallback\s*\(/g,
|
|
/const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*useMemo\s*\(/g,
|
|
];
|
|
|
|
function findFiles(dir, extensions = ['.tsx', '.ts', '.jsx', '.js']) {
|
|
const files = [];
|
|
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
|
|
for (const item of items) {
|
|
const fullPath = path.join(dir, item.name);
|
|
|
|
if (item.isDirectory() && !item.name.startsWith('.') && item.name !== 'node_modules') {
|
|
files.push(...findFiles(fullPath, extensions));
|
|
} else if (item.isFile() && extensions.some(ext => item.name.endsWith(ext))) {
|
|
files.push(fullPath);
|
|
}
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
function extractHandlers(content) {
|
|
const handlers = new Set();
|
|
|
|
for (const pattern of HANDLER_PATTERNS) {
|
|
let match;
|
|
while ((match = pattern.exec(content)) !== null) {
|
|
const handlerName = match[1];
|
|
// Skip common built-ins and React hooks
|
|
if (!['setState', 'useState', 'useEffect', 'useCallback', 'useMemo', 'prev', 'e', 'event', 'error'].includes(handlerName)) {
|
|
handlers.add(handlerName);
|
|
}
|
|
}
|
|
}
|
|
|
|
return handlers;
|
|
}
|
|
|
|
function extractFunctions(content) {
|
|
const functions = new Set();
|
|
|
|
for (const pattern of FUNCTION_PATTERNS) {
|
|
let match;
|
|
while ((match = pattern.exec(content)) !== null) {
|
|
functions.add(match[1]);
|
|
}
|
|
}
|
|
|
|
return functions;
|
|
}
|
|
|
|
function validateFile(filePath) {
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
const handlers = extractHandlers(content);
|
|
const functions = extractFunctions(content);
|
|
|
|
const undefinedHandlers = [];
|
|
|
|
for (const handler of handlers) {
|
|
// Check if handler is defined as a function
|
|
if (!functions.has(handler)) {
|
|
// Check if it's a prop (passed from parent)
|
|
const isProp = new RegExp(`(?:props|\\{[^}]*)\\s*:\\s*\\{[^}]*${handler}[^}]*:`, 's').test(content) ||
|
|
new RegExp(`(?:interface|type)\\s+\\w+Props[^}]*${handler}[^}]*:`, 's').test(content);
|
|
|
|
// Check if it's from a hook or external import
|
|
const isImported = new RegExp(`(?:import|from).*${handler}`, 's').test(content);
|
|
|
|
if (!isProp && !isImported) {
|
|
undefinedHandlers.push(handler);
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefinedHandlers;
|
|
}
|
|
|
|
function main() {
|
|
console.log('🔍 Validating handler functions...\n');
|
|
|
|
const componentFiles = findFiles(COMPONENT_DIR);
|
|
const apiFiles = findFiles(API_DIR);
|
|
const allFiles = [...componentFiles, ...apiFiles];
|
|
|
|
let hasErrors = false;
|
|
const errors = [];
|
|
|
|
for (const file of allFiles) {
|
|
const undefinedHandlers = validateFile(file);
|
|
|
|
if (undefinedHandlers.length > 0) {
|
|
hasErrors = true;
|
|
const relativePath = path.relative(process.cwd(), file);
|
|
errors.push({
|
|
file: relativePath,
|
|
handlers: undefinedHandlers,
|
|
});
|
|
|
|
console.error(`❌ ${relativePath}`);
|
|
for (const handler of undefinedHandlers) {
|
|
console.error(` ⚠️ Handler "${handler}" is referenced but not defined`);
|
|
}
|
|
console.error('');
|
|
}
|
|
}
|
|
|
|
if (hasErrors) {
|
|
console.error('\n❌ Validation failed: Found undefined handler functions\n');
|
|
console.error('Please ensure all handler functions are defined before use.\n');
|
|
process.exit(1);
|
|
} else {
|
|
console.log('✅ All handler functions are properly defined!\n');
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
}
|
|
|
|
module.exports = { validateFile, extractHandlers, extractFunctions };
|