chore(guard): add no-mock regression checks
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
"format:check": "prettier \"**/*.{ts,tsx,js,jsx,mjs,cjs,json,css,yml,yaml}\" --check",
|
"format:check": "prettier \"**/*.{ts,tsx,js,jsx,mjs,cjs,json,css,yml,yaml}\" --check",
|
||||||
"check": "pnpm lint && pnpm typecheck && pnpm format:check",
|
"check": "pnpm lint && pnpm typecheck && pnpm format:check",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
|
"guard:no-mock": "node scripts/guard-no-mock.mjs",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest"
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user