import fs from "node:fs/promises" import path from "node:path" import process from "node:process" import { fileURLToPath } from "node:url" const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]) const IGNORE_DIRS = new Set(["node_modules", ".git", ".next", "dist", "build", "coverage"]) function rel(p) { return path.relative(REPO_ROOT, p) } async function statSafe(p) { try { return await fs.stat(p) } catch { return null } } async function listFilesRecursive(dir, { exts = null } = {}) { const out = [] const st = await statSafe(dir) if (!st || !st.isDirectory()) return out const entries = await fs.readdir(dir, { withFileTypes: true }) for (const ent of entries) { if (ent.isDirectory()) { if (IGNORE_DIRS.has(ent.name)) continue out.push(...(await listFilesRecursive(path.join(dir, ent.name), { exts }))) continue } if (!ent.isFile()) continue const fullPath = path.join(dir, ent.name) if (exts && !exts.has(path.extname(ent.name))) continue out.push(fullPath) } return out } async function findFilesContainingString(roots, needle) { const matches = [] for (const root of roots) { const absRoot = path.join(REPO_ROOT, root) const files = await listFilesRecursive(absRoot, { exts: SOURCE_EXTS }) for (const file of files) { const content = await fs.readFile(file, "utf8") if (content.includes(needle)) matches.push(file) } } return matches } async function findFilesMatchingPredicate(roots, predicate) { const matches = [] for (const root of roots) { const absRoot = path.join(REPO_ROOT, root) const files = await listFilesRecursive(absRoot, { exts: SOURCE_EXTS }) for (const file of files) { const content = await fs.readFile(file, "utf8") if (predicate(content)) matches.push(file) } } return matches } async function findAppApiRoutes() { const apiRoot = path.join(REPO_ROOT, "app", "api") const st = await statSafe(apiRoot) if (!st || !st.isDirectory()) return [] const files = await listFilesRecursive(apiRoot, { exts: new Set([".ts"]) }) return files.filter((p) => path.basename(p) === "route.ts") } async function main() { const violations = [] const mockImports = await findFilesContainingString( ["app", "components", "lib", "store", "tests"], "@/lib/mock", ) if (mockImports.length > 0) { violations.push({ title: "Found @/lib/mock import string in source files", files: mockImports, }) } const libMockDir = path.join(REPO_ROOT, "lib", "mock") const libMockSt = await statSafe(libMockDir) if (libMockSt?.isDirectory()) { violations.push({ title: "Found lib/mock directory", files: [libMockDir], }) } const apiRoutes = await findAppApiRoutes() if (apiRoutes.length > 0) { violations.push({ title: "Found app/api/**/route.ts files", files: apiRoutes, }) } const absoluteBackendMatches = await findFilesMatchingPredicate( ["app", "components"], (content) => content.includes("NEXT_PUBLIC_BACKEND_URL") || /https?:\/\/localhost:8080/.test(content), ) if (absoluteBackendMatches.length > 0) { violations.push({ title: "Found absolute backend origins or NEXT_PUBLIC_BACKEND_URL usage in client code (app/ or components/)", files: absoluteBackendMatches, }) } const apiDir = path.join(REPO_ROOT, "lib", "api") const apiFiles = (await listFilesRecursive(apiDir, { exts: new Set([".ts"]) })).filter( (p) => p !== path.join(apiDir, "client.ts"), ) const storeImportRe = /^\s*(?:import|export)\s+(?:type\s+)?(?:[\w*\s{},]*from\s*)?['"]@\/store\//m const apiStoreImports = [] for (const file of apiFiles) { const content = await fs.readFile(file, "utf8") if (storeImportRe.test(content)) apiStoreImports.push(file) } if (apiStoreImports.length > 0) { violations.push({ title: "Found @/store/ imports in lib/api (except lib/api/client.ts)", files: apiStoreImports, }) } if (violations.length > 0) { console.error("guard:no-mock failed") for (const v of violations) { console.error(`\n- ${v.title}`) for (const f of v.files) console.error(` - ${rel(f)}`) } process.exitCode = 1 return } console.log("guard:no-mock ok") } main().catch((err) => { console.error("guard:no-mock crashed") console.error(err) process.exit(1) })