Files
NewSzxcn-Email/apps/web/scripts/check-shadcn.mjs
LanQin 3ef8caa319 feat(email): 初始化自建邮箱 MVP
- 新增 Go + SQLite 后端,支持登录、域名、邮箱、别名、联系人、规则、统计与邮件收发。
- 新增 Webmail 前端,支持多邮箱切换、邮件列表/阅读/写信、附件、搜索、主题切换与个人中心。
- 新增 Docker Compose 部署骨架,补充 Postfix、Dovecot、OpenDKIM、Nginx 配置与部署说明。
2026-06-15 00:38:50 +08:00

92 lines
4.0 KiB
JavaScript

import fs from "node:fs"
import path from "node:path"
const root = process.cwd()
const srcDir = path.join(root, "src")
const ignoredSegments = [
`${path.sep}components${path.sep}ui${path.sep}`,
`${path.sep}dist${path.sep}`,
]
// Business code must use shadcn/ui wrappers for visible UI primitives.
// Semantic/layout tags such as div, span, form, header, main, section and a are allowed.
const forbidden = [
{ tag: "button", replacement: "@/components/ui/button Button" },
{ tag: "input", replacement: "@/components/ui/input Input" },
{ tag: "textarea", replacement: "@/components/ui/textarea Textarea" },
{ tag: "select", replacement: "@/components/ui/select Select" },
{ tag: "table", replacement: "@/components/ui/table Table" },
{ tag: "thead", replacement: "@/components/ui/table TableHeader" },
{ tag: "tbody", replacement: "@/components/ui/table TableBody" },
{ tag: "tr", replacement: "@/components/ui/table TableRow" },
{ tag: "th", replacement: "@/components/ui/table TableHead" },
{ tag: "td", replacement: "@/components/ui/table TableCell" },
{ tag: "dialog", replacement: "@/components/ui/dialog Dialog" },
{ tag: "aside", replacement: "@/components/ui/sidebar Sidebar" },
]
// Product UI rule: do not add small explanatory subtitle text under titles.
// Keep screens clean: use titles, labels, badges and actionable controls only.
const forbiddenComponents = [
{ name: "CardDescription", reason: "不要在卡片标题下添加说明性小字" },
{ name: "DialogDescription", reason: "不要在弹窗标题下添加说明性小字" },
{ name: "SheetDescription", reason: "不要在抽屉标题下添加说明性小字" },
]
const forbiddenClassPatterns = [
{ pattern: /\bbg-gradient-[^\s"`'}]+/, reason: "不要使用渐变背景,保持 shadcn neutral 极简风格" },
{ pattern: /\b(?:from|via|to)-blue-[^\s"`'}]+/, reason: "不要使用蓝色渐变色阶,保持 neutral/black 视觉" },
{ pattern: /\b(?:bg|text|border|ring)-blue-[^\s"`'}]+/, reason: "不要使用蓝色品牌色,保持 neutral/black 视觉" },
{ pattern: /\bshadow-(?:lg|xl|2xl|primary[^\s"`'}]*)/, reason: "不要使用重阴影或营销感阴影,保持 shadcn 默认克制风格" },
]
function walk(dir) {
const out = []
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name)
if (entry.isDirectory()) out.push(...walk(full))
else if (/\.(tsx|jsx)$/.test(entry.name)) out.push(full)
}
return out
}
const violations = []
for (const file of walk(srcDir)) {
if (ignoredSegments.some((segment) => file.includes(segment))) continue
const rel = path.relative(root, file)
const content = fs.readFileSync(file, "utf8")
const lines = content.split(/\r?\n/)
lines.forEach((line, index) => {
for (const rule of forbidden) {
const re = new RegExp(`<${rule.tag}(\\s|>|/)`)
if (re.test(line)) {
violations.push({ file: rel, line: index + 1, tag: rule.tag, replacement: rule.replacement, code: line.trim() })
}
}
for (const rule of forbiddenComponents) {
const re = new RegExp(`\\b${rule.name}\\b`)
if (re.test(line)) {
violations.push({ file: rel, line: index + 1, tag: rule.name, replacement: rule.reason, code: line.trim() })
}
}
for (const rule of forbiddenClassPatterns) {
if (rule.pattern.test(line)) {
violations.push({ file: rel, line: index + 1, tag: "visual-style", replacement: rule.reason, code: line.trim() })
}
}
})
}
if (violations.length > 0) {
console.error("\nshadcn/ui rule failed: business TSX must not use native UI primitives.\n")
for (const v of violations) {
console.error(`${v.file}:${v.line} <${v.tag}> -> use ${v.replacement}`)
console.error(` ${v.code}`)
}
console.error("\nAllowed: native semantic/layout tags like div, span, form, header, main, section, a. Official shadcn components under src/components/ui are exempt.\n")
process.exit(1)
}
console.log("shadcn/ui rule passed: no native UI primitives found in business TSX.")