chore(guard): add no-mock regression checks

This commit is contained in:
zetaloop
2026-03-01 22:21:49 +08:00
parent e2671638e6
commit d4e2c13a03
2 changed files with 157 additions and 0 deletions
+1
View File
@@ -12,6 +12,7 @@
"format:check": "prettier \"**/*.{ts,tsx,js,jsx,mjs,cjs,json,css,yml,yaml}\" --check",
"check": "pnpm lint && pnpm typecheck && pnpm format:check",
"typecheck": "tsc --noEmit",
"guard:no-mock": "node scripts/guard-no-mock.mjs",
"test": "vitest run",
"test:watch": "vitest"
},
+156
View File
@@ -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)
})