import { readFile, readdir } from "node:fs/promises"; import { basename, join } from "node:path"; export const RULE_SET_ORDER = ["common", "python", "typescript", "react"] as const; export type RuleSetId = (typeof RULE_SET_ORDER)[number]; const IGNORED_DIRECTORIES = new Set([ ".cache", ".git", ".next", ".turbo", ".venv", "__pycache__", "build", "coverage", "dist", "node_modules", "target", "vendor", "venv", ]); const MAX_SCANNED_ENTRIES = 5_000; const PYTHON_FILENAMES = new Set([ "Pipfile", "pyproject.toml", "pyrightconfig.json", "setup.cfg", "setup.py", "uv.lock", ]); const TYPESCRIPT_FILENAMES = new Set([ "bun.lock", "bun.lockb", "deno.json", "deno.jsonc", "package-lock.json", "package.json", "pnpm-lock.yaml", "yarn.lock", ]); const PYTHON_EXTENSIONS = new Set([".py", ".pyi"]); const TYPESCRIPT_EXTENSIONS = new Set([".cjs", ".cts", ".js", ".jsx", ".mjs", ".mts", ".ts", ".tsx"]); const REACT_EXTENSIONS = new Set([".jsx", ".tsx"]); const REACT_PACKAGES = new Set(["@remix-run/react", "@types/react", "gatsby", "next", "react", "react-dom"]); interface DetectionState { hasPython: boolean; hasTypeScript: boolean; hasReact: boolean; scannedEntries: number; } function extensionOf(filename: string): string { const dotIndex = filename.lastIndexOf("."); return dotIndex < 0 ? "" : filename.slice(dotIndex).toLowerCase(); } function inspectFilename(filename: string, state: DetectionState): void { const name = basename(filename); const lowerName = name.toLowerCase(); const extension = extensionOf(name); if ( PYTHON_FILENAMES.has(name) || lowerName === "requirements.txt" || (lowerName.startsWith("requirements-") && lowerName.endsWith(".txt")) || PYTHON_EXTENSIONS.has(extension) ) { state.hasPython = true; } if ( TYPESCRIPT_FILENAMES.has(name) || lowerName.startsWith("tsconfig") && lowerName.endsWith(".json") || TYPESCRIPT_EXTENSIONS.has(extension) ) { state.hasTypeScript = true; } if (REACT_EXTENSIONS.has(extension)) { state.hasReact = true; state.hasTypeScript = true; } } function readDependencyNames(value: unknown): string[] { if (!value || typeof value !== "object" || Array.isArray(value)) return []; return Object.keys(value); } async function inspectPackageJson(projectRoot: string, state: DetectionState): Promise { try { const raw = await readFile(join(projectRoot, "package.json"), "utf8"); const parsed: unknown = JSON.parse(raw); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return; const packageJson = parsed as Record; const dependencyNames = [ ...readDependencyNames(packageJson.dependencies), ...readDependencyNames(packageJson.devDependencies), ...readDependencyNames(packageJson.peerDependencies), ...readDependencyNames(packageJson.optionalDependencies), ]; if (dependencyNames.some((name) => REACT_PACKAGES.has(name))) { state.hasReact = true; state.hasTypeScript = true; } } catch (error: unknown) { if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !(error instanceof SyntaxError)) { throw error; } } } async function scanDirectory(directory: string, state: DetectionState): Promise { if (state.scannedEntries >= MAX_SCANNED_ENTRIES) return; const entries = await readdir(directory, { withFileTypes: true }); for (const entry of entries) { state.scannedEntries += 1; if (state.scannedEntries > MAX_SCANNED_ENTRIES) return; if (entry.isDirectory()) { if (!IGNORED_DIRECTORIES.has(entry.name)) { await scanDirectory(join(directory, entry.name), state); } continue; } if (entry.isFile()) inspectFilename(entry.name, state); } } export async function detectRuleSets(projectRoot: string): Promise { const state: DetectionState = { hasPython: false, hasTypeScript: false, hasReact: false, scannedEntries: 0, }; await Promise.all([scanDirectory(projectRoot, state), inspectPackageJson(projectRoot, state)]); return RULE_SET_ORDER.filter((ruleSet) => { if (ruleSet === "common") return true; if (ruleSet === "python") return state.hasPython; if (ruleSet === "typescript") return state.hasTypeScript; return state.hasReact; }); }