infoalloggi-monorepo/apps/infoalloggi/check-unused-trpc.ts

125 lines
3.5 KiB
TypeScript

import * as fs from "node:fs";
import * as path from "node: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/pages"),
path.join(ROOT_DIR, "src/components"),
path.join(ROOT_DIR, "src/forms"),
path.join(ROOT_DIR, "src/hooks"),
path.join(ROOT_DIR, "src/lib"),
path.join(ROOT_DIR, "src/providers"),
];
const IGNORE_DIRS: string[] = ["node_modules", ".next", "dist"];
// Regex to find tRPC procedure definitions.
// Matches `procedureName: someProcedure` where the procedure type is one of the known base procedures.
// This handles chained .input()/.output() before .query/.mutation/.subscription.
const PROCEDURE_REGEX: RegExp =
/([a-zA-Z0-9_]+)\s*:\s*(?:adminProcedure|protectedProcedure|publicProcedure)\b/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<string>();
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();