import * as fs from "fs"; import * as path from "path"; // --- CONFIGURATION --- const ROOT_DIR: string = process.cwd(); const ROUTERS_DIR: string = path.join(ROOT_DIR, "src/server/api/routers"); const SCAN_DIRS: string[] = [path.join(ROOT_DIR, "src")]; const IGNORE_DIRS: string[] = ["node_modules", ".next", "dist"]; // Regex to find tRPC procedure definitions const PROCEDURE_REGEX: RegExp = /([a-zA-Z0-9_]+)\s*:\s*[a-zA-Z0-9_.]+\.(?:query|mutation|subscription)\s*\(/g; interface ProcedureDefinition { name: string; file: string; line: number; } function getAllFiles(dir: string, ext: string[] = [".ts", ".tsx"]): string[] { let results: string[] = []; if (!fs.existsSync(dir)) return results; const list: string[] = fs.readdirSync(dir); list.forEach((file: string) => { const fullPath: string = path.join(dir, file); const stat = fs.statSync(fullPath); if (stat?.isDirectory()) { if (!IGNORE_DIRS.includes(file)) { results = results.concat(getAllFiles(fullPath, ext)); } } else { if (ext.some((e) => file.endsWith(e))) { results.push(fullPath); } } }); return results; } /** * Calculates the line number of a specific index in a string */ function getLineNumber(content: string, index: number): number { const linesBefore = content.substring(0, index).split("\n"); return linesBefore.length; } function run(): void { console.log("🔍 Scanning for tRPC procedures...\n"); const routerFiles: string[] = getAllFiles(ROUTERS_DIR); const procedures: ProcedureDefinition[] = []; // 1. Extract procedure names and their specific line numbers routerFiles.forEach((filePath: string) => { const content: string = fs.readFileSync(filePath, "utf8"); let match: RegExpExecArray | null; match = PROCEDURE_REGEX.exec(content); while (match !== null) { if (match[1]) { procedures.push({ name: match[1], file: filePath, line: getLineNumber(content, match.index), }); } match = PROCEDURE_REGEX.exec(content); } }); console.log(`Found ${procedures.length} procedures. Checking for usage...\n`); // 2. Get all files to scan let scanFiles: string[] = []; SCAN_DIRS.forEach((dir: string) => { scanFiles = scanFiles.concat(getAllFiles(dir)); }); // 3. Check for usage const usedNames = new Set(); scanFiles.forEach((filePath: string) => { const content: string = fs.readFileSync(filePath, "utf8"); procedures.forEach((proc) => { if (filePath === proc.file) return; // Search for name with word boundaries const usageRegex = new RegExp(`\\b${proc.name}\\b`); if (usageRegex.test(content)) { usedNames.add(proc.name); } }); }); // 4. Report results const unused = procedures.filter((p) => !usedNames.has(p.name)); if (unused.length === 0) { console.log("✅ All good! No unused tRPC procedures found."); } else { console.log( `🚨 Found ${unused.length} potentially unused tRPC procedures:\n`, ); unused.forEach((p) => { const relativePath: string = path.relative(ROOT_DIR, p.file); // Format: path/to/file.ts:line // Most IDEs (VS Code, WebStorm) make this clickable in the terminal console.log(`- ${p.name} -> ${relativePath}:${p.line}`); }); } } run();