refactor: modularize frontend flows
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join, relative } from 'node:path'
|
||||
import { globSync } from 'glob'
|
||||
|
||||
const rootDir = new URL('..', import.meta.url).pathname
|
||||
const srcDir = join(rootDir, 'src')
|
||||
|
||||
const rules = [
|
||||
{
|
||||
name: 'views/components 不直接导入 services',
|
||||
files: ['src/views/**/*.{ts,vue}', 'src/components/**/*.{ts,vue}'],
|
||||
patterns: [/from ['"]@\/services(?:\/[^'"]*)?['"]/],
|
||||
},
|
||||
{
|
||||
name: 'views/components 不直接调用服务类',
|
||||
files: ['src/views/**/*.{ts,vue}', 'src/components/**/*.{ts,vue}'],
|
||||
patterns: [
|
||||
/\bFileService\./,
|
||||
/\bConfigService\./,
|
||||
/\bAuthService\./,
|
||||
/\bStatsService\./,
|
||||
/\bPresignUploadService\./,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'views/components/composables 不直接导入具体 service 文件',
|
||||
files: ['src/views/**/*.{ts,vue}', 'src/components/**/*.{ts,vue}', 'src/composables/**/*.ts'],
|
||||
patterns: [/from ['"]@\/services\/[^'"]+['"]/],
|
||||
},
|
||||
{
|
||||
name: '预签名上传 composable 不直接依赖 store',
|
||||
files: ['src/composables/usePresignedUpload.ts'],
|
||||
patterns: [/from ['"]@\/stores\/[^'"]+['"]/],
|
||||
},
|
||||
{
|
||||
name: '取件流程 composable 不直接依赖路由',
|
||||
files: ['src/composables/useRetrieveFlow.ts'],
|
||||
patterns: [/from ['"]vue-router['"]/],
|
||||
},
|
||||
{
|
||||
name: '发送流程 composable 不直接依赖路由',
|
||||
files: ['src/composables/useSendFlow.ts'],
|
||||
patterns: [/from ['"]vue-router['"]/],
|
||||
},
|
||||
{
|
||||
name: '登录流程 composable 不直接依赖路由',
|
||||
files: ['src/composables/useAdminLogin.ts'],
|
||||
patterns: [/from ['"]vue-router['"]/],
|
||||
},
|
||||
{
|
||||
name: '应用壳不直接调用服务,启动任务需拆到专用 composable',
|
||||
files: ['src/composables/useAppShell.ts'],
|
||||
patterns: [/\b[A-Z][A-Za-z]+Service\./, /from ['"]@\/services\/[^'"]+['"]/],
|
||||
},
|
||||
{
|
||||
name: '公共配置启动任务独立维护',
|
||||
files: ['src/composables/useAppShell.ts'],
|
||||
patterns: [/\bgetUserConfig\(/, /\bapplyRemoteConfig\(/],
|
||||
},
|
||||
{
|
||||
name: '仪表盘数据 composable 不在初始化阶段自动加载',
|
||||
files: ['src/composables/useDashboardStats.ts'],
|
||||
patterns: [/\bonMounted\(/],
|
||||
},
|
||||
{
|
||||
name: '请求客户端统一放在 services/client',
|
||||
files: ['src/**/*.{ts,vue}'],
|
||||
patterns: [/from ['"]@\/utils\/api['"]/],
|
||||
ignoreFiles: [/^src\/utils\/api\.ts$/],
|
||||
},
|
||||
{
|
||||
name: '请求客户端不直接控制浏览器跳转',
|
||||
files: ['src/services/client.ts'],
|
||||
patterns: [/\bwindow\.location\.(href|replace|assign)\b/, /\blocation\.(href|replace|assign)\b/],
|
||||
},
|
||||
{
|
||||
name: '裸 axios 只能出现在请求客户端封装内',
|
||||
files: ['src/**/*.{ts,vue}'],
|
||||
patterns: [/import\s+axios\b/],
|
||||
ignoreFiles: [/^src\/services\/client\.ts$/, /^src\/services\/upload-client\.ts$/],
|
||||
},
|
||||
{
|
||||
name: '文件打包能力统一放在 file-processing 工具',
|
||||
files: ['src/**/*.{ts,vue}'],
|
||||
patterns: [/from ['"]jszip['"]/],
|
||||
ignoreFiles: [/^src\/utils\/file-processing\.ts$/],
|
||||
},
|
||||
{
|
||||
name: '内容预览渲染统一放在 content-preview 工具',
|
||||
files: ['src/**/*.{ts,vue}'],
|
||||
patterns: [/from ['"]marked['"]/, /from ['"]dompurify['"]/i],
|
||||
ignoreFiles: [/^src\/utils\/content-preview\.ts$/],
|
||||
},
|
||||
{
|
||||
name: '下载动作统一放在 download-action 工具',
|
||||
files: ['src/**/*.{ts,vue}'],
|
||||
patterns: [/from ['"]file-saver['"]/, /\bwindow\.open\(/],
|
||||
ignoreFiles: [/^src\/utils\/download-action\.ts$/],
|
||||
},
|
||||
{
|
||||
name: 'services 统一使用 services/client 作为内部请求入口',
|
||||
files: ['src/services/**/*.ts'],
|
||||
patterns: [/from ['"]@\/services\/client['"]/],
|
||||
},
|
||||
{
|
||||
name: '非 storage helper 不直接访问 localStorage',
|
||||
files: ['src/**/*.{ts,vue}'],
|
||||
patterns: [/\blocalStorage\./],
|
||||
ignoreFiles: [
|
||||
/^src\/utils\/auth-storage\.ts$/,
|
||||
/^src\/utils\/config-storage\.ts$/,
|
||||
/^src\/utils\/preference-storage\.ts$/,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '非 URL helper 不直接读取 window.location.origin',
|
||||
files: ['src/**/*.{ts,vue}'],
|
||||
patterns: [/\bwindow\.location\.origin\b/],
|
||||
ignoreFiles: [/^src\/utils\/share-url\.ts$/],
|
||||
},
|
||||
{
|
||||
name: '非剪贴板 helper 不直接调用剪贴板 API',
|
||||
files: ['src/**/*.{ts,vue}'],
|
||||
patterns: [/\bnavigator\.clipboard\b/, /\bdocument\.execCommand\(['"]copy['"]\)/],
|
||||
ignoreFiles: [/^src\/utils\/clipboard\.ts$/],
|
||||
},
|
||||
{
|
||||
name: '页面和组件不直接导入剪贴板 helper',
|
||||
files: ['src/views/**/*.{ts,vue}', 'src/components/**/*.{ts,vue}'],
|
||||
patterns: [/from ['"]@\/utils\/clipboard['"]/],
|
||||
},
|
||||
{
|
||||
name: '工具层不直接依赖 store',
|
||||
files: ['src/utils/**/*.ts'],
|
||||
patterns: [/from ['"]@\/stores\/[^'"]+['"]/],
|
||||
},
|
||||
{
|
||||
name: '系统配置页通过 composable 动作切换二值配置',
|
||||
files: ['src/views/manage/SystemSettingsView.vue'],
|
||||
patterns: [/config\.(enableChunk|s3_proxy|openUpload)\s*=[^=]/],
|
||||
},
|
||||
{
|
||||
name: '系统配置页开关统一使用 SettingSwitch',
|
||||
files: ['src/views/manage/SystemSettingsView.vue'],
|
||||
patterns: [/\brole=['"]switch['"]/],
|
||||
},
|
||||
{
|
||||
name: '系统配置页简单数字项统一使用 SettingNumberInput',
|
||||
files: ['src/views/manage/SystemSettingsView.vue'],
|
||||
patterns: [/<input[^>]*v-model=['"]config\.(uploadMinute|uploadCount|errorMinute|errorCount)['"]/],
|
||||
},
|
||||
{
|
||||
name: '系统配置页不直接处理配置表单转换',
|
||||
files: ['src/views/manage/SystemSettingsView.vue'],
|
||||
patterns: [/from ['"]@\/utils\/config-form['"]/],
|
||||
},
|
||||
{
|
||||
name: '文件管理编辑弹窗字段统一使用 FileEditField',
|
||||
files: ['src/views/manage/FileManageView.vue'],
|
||||
patterns: [/<input[^>]*v-model=['"]editForm\.(code|prefix|suffix|expired_at|expired_count)['"]/],
|
||||
},
|
||||
{
|
||||
name: '发送页记录列表统一使用 SentRecordList',
|
||||
files: ['src/views/SendFileView.vue'],
|
||||
patterns: [/v-for=['"]record in sendRecords['"]/],
|
||||
},
|
||||
{
|
||||
name: '发送页记录详情统一使用 SentRecordDetailModal',
|
||||
files: ['src/views/SendFileView.vue'],
|
||||
patterns: [/<QRCode\b/, /\bselectedRecord\s*=\s*null\b/],
|
||||
},
|
||||
{
|
||||
name: '发送流程记录构造统一放在 send-record 工具',
|
||||
files: ['src/composables/useSendFlow.ts'],
|
||||
patterns: [/\bDate\.now\(\)/, /\bnew Date\(\)\.toISOString\(\)/],
|
||||
},
|
||||
{
|
||||
name: '发送流程过期时间格式化统一放在 send-record 工具',
|
||||
files: ['src/composables/useSendFlow.ts'],
|
||||
patterns: [/\bsetMinutes\(/, /\bsetHours\(/, /\bsetDate\(/],
|
||||
},
|
||||
{
|
||||
name: '发送记录复制和二维码动作统一放在 sent-record-actions 工具',
|
||||
files: ['src/composables/useSendFlow.ts'],
|
||||
patterns: [
|
||||
/\bcopyRetrieve(Code|Link)\(/,
|
||||
/\bcopyWgetCommand\(/,
|
||||
/\bbuildSentRecordQrValue\(/
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '发送提交策略统一放在 useSendSubmit',
|
||||
files: ['src/composables/useSendFlow.ts'],
|
||||
patterns: [
|
||||
/from ['"]@\/services['"]/,
|
||||
/from ['"]\.\/usePresignedUpload['"]/,
|
||||
/\buploadChunkedFile\(/,
|
||||
/\bFileService\.uploadText\(/,
|
||||
/\bpackFilesAsZip\(/
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '取件页不直接改流程内部状态',
|
||||
files: ['src/views/RetrievewFileView.vue'],
|
||||
patterns: [/\bselectedRecord\s*=\s*null\b/, /\bshowPreview\s*=\s*false\b/],
|
||||
},
|
||||
{
|
||||
name: '旧上传下载 composable 不回流',
|
||||
files: ['src/**/*.{ts,vue}'],
|
||||
patterns: [/\buseFileUpload\b/, /\buseFileDownload\b/],
|
||||
},
|
||||
{
|
||||
name: '页面层统一从 @/composables 导入',
|
||||
files: ['src/views/**/*.{ts,vue}', 'src/App.vue'],
|
||||
patterns: [/from ['"]@\/composables\/[^'"]+['"]/],
|
||||
},
|
||||
{
|
||||
name: 'composables 内部同层依赖使用相对导入',
|
||||
files: ['src/composables/**/*.ts'],
|
||||
patterns: [/from ['"]@\/composables\/[^'"]+['"]/],
|
||||
},
|
||||
]
|
||||
|
||||
const violations = []
|
||||
|
||||
for (const rule of rules) {
|
||||
const files = rule.files.flatMap((pattern) =>
|
||||
globSync(pattern, {
|
||||
cwd: rootDir,
|
||||
absolute: true,
|
||||
nodir: true,
|
||||
ignore: ['node_modules/**', 'dist/**'],
|
||||
})
|
||||
)
|
||||
|
||||
for (const file of files) {
|
||||
const relativeFile = relative(rootDir, file)
|
||||
if (rule.ignoreFiles?.some((pattern) => pattern.test(relativeFile))) {
|
||||
continue
|
||||
}
|
||||
|
||||
const source = readFileSync(file, 'utf8')
|
||||
const lines = source.split(/\r?\n/)
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
for (const pattern of rule.patterns) {
|
||||
if (pattern.test(line)) {
|
||||
violations.push({
|
||||
rule: rule.name,
|
||||
file: relativeFile,
|
||||
line: index + 1,
|
||||
text: line.trim(),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error('架构边界检查失败:')
|
||||
for (const violation of violations) {
|
||||
console.error(
|
||||
`- ${violation.rule}: ${violation.file}:${violation.line} ${violation.text}`
|
||||
)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`架构边界检查通过:${srcDir}`)
|
||||
Reference in New Issue
Block a user