#!/usr/bin/env tsx import { execSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; interface WhitelistConfig { files?: string[]; patterns?: string[]; } const WHITELIST_PATH = '.console-log-whitelist.json'; /** * Load whitelist configuration */ const loadWhitelist = (): WhitelistConfig => { try { const content = readFileSync(WHITELIST_PATH, 'utf8'); return JSON.parse(content); } catch { return { files: [], patterns: [] }; } }; /** * Check if a file is whitelisted */ const isWhitelisted = (filePath: string, whitelist: WhitelistConfig): boolean => { const normalizedPath = filePath.replaceAll('\\', '/'); // Check exact file matches if (whitelist.files?.some((f) => normalizedPath.includes(f.replaceAll('\\', '/')))) { return true; } // Check pattern matches (simple glob-like patterns) if (whitelist.patterns) { for (const pattern of whitelist.patterns) { // Escape dots and replace glob patterns // Use a placeholder for ** to avoid conflicts with single * let regexPattern = pattern .replaceAll('.', '\\.') .replaceAll('**', '\u0000DOUBLESTAR\u0000') .replaceAll('*', '[^/]*') .replaceAll('\u0000DOUBLESTAR\u0000', '.*'); // If pattern ends with /**, match everything under that directory // If pattern ends with **, just match everything from that point const regex = new RegExp(`^${regexPattern}`); if (regex.test(normalizedPath)) { return true; } } } return false; }; /** * Main check function */ const checkConsoleLogs = () => { const whitelist = loadWhitelist(); console.log('šŸ” Checking for console.log statements...\n'); try { // Search for console.log in TypeScript and JavaScript files const output = execSync( `git grep -n "console\\.log" -- "*.ts" "*.tsx" "*.js" "*.jsx" "*.mts" "*.cts" || true`, { encoding: 'utf8' }, ); if (!output.trim()) { console.log('āœ… No console.log statements found!'); return; } const lines = output.trim().split('\n'); const violations: Array<{ content: string; file: string; line: string }> = []; for (const line of lines) { // Parse git grep output: filename:lineNumber:content const match = line.match(/^([^:]+):(\d+):(.+)$/); if (!match) continue; const [, filePath, lineNumber, content] = match; // Skip if whitelisted if (isWhitelisted(filePath, whitelist)) { continue; } // Skip comments const trimmedContent = content.trim(); if (trimmedContent.startsWith('//') || trimmedContent.startsWith('*')) { continue; } violations.push({ content: content.trim(), file: filePath, line: lineNumber, }); } if (violations.length === 0) { console.log( 'āœ… No console.log violations found (all matches are whitelisted or in comments)!', ); return; } // Report violations as warnings console.log('āš ļø Found console.log statements in the following files:\n'); // Use GitHub Actions annotation format for better visibility in CI const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true'; for (const violation of violations) { if (isCI) { // GitHub Actions warning annotation format console.log( `::warning file=${violation.file},line=${violation.line}::console.log found: ${violation.content}`, ); } else { console.log(` ${violation.file}:${violation.line}`); console.log(` ${violation.content}\n`); } } console.log(`\nšŸ’” Total violations: ${violations.length}`); console.log(`\nšŸ“ To whitelist files, add them to ${WHITELIST_PATH} in the following format:`); console.log(`{ "files": ["path/to/file.ts"], "patterns": ["scripts/**/*.mts", "**/*.test.ts"] }\n`); // Exit with 0 to not block CI, but warnings will still be visible process.exit(0); } catch (error: unknown) { if (error instanceof Error && 'status' in error && error.status !== 0) { console.error('āŒ Error running git grep:', error.message); process.exit(1); } throw error; } }; checkConsoleLogs();