@brms/ai-skills 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +256 -0
- package/bin/brms-skills.mjs +411 -0
- package/package.json +30 -0
- package/skills/brms-prototype-generator/SKILL.md +129 -0
- package/skills/brms-prototype-generator/agents/openai.yaml +7 -0
- package/skills/brms-prototype-generator/examples/few-shot-examples.md +577 -0
- package/skills/brms-prototype-generator/references/01-list-query.md +444 -0
- package/skills/brms-prototype-generator/references/02-form-entry.md +129 -0
- package/skills/brms-prototype-generator/references/03-detail-display.md +125 -0
- package/skills/brms-prototype-generator/references/04-composite-page-package.md +339 -0
- package/skills/brms-prototype-generator/references/05-dialog-patterns.md +113 -0
- package/skills/brms-prototype-generator/references/06-backend-request-patterns.md +118 -0
- package/skills/brms-prototype-generator/references/resource-index.md +46 -0
- package/skills/brms-prototype-generator/references/system-prompt.md +242 -0
- package/skills/brms-prototype-generator/scripts/analyze-doc.mjs +554 -0
- package/skills/brms-prototype-generator/scripts/check-project.mjs +228 -0
- package/skills/brms-prototype-generator/scripts/discover-targets.mjs +158 -0
- package/skills/brms-prototype-generator/scripts/install-codex.mjs +74 -0
- package/skills/brms-prototype-generator/scripts/plan-pages.mjs +390 -0
- package/skills/brms-prototype-generator/scripts/validate-generated.mjs +838 -0
- package/skills/brms-prototype-generator/templates/user-input-template.md +182 -0
- package/skills/brms-vxe-plus-developer/SKILL.md +105 -0
- package/skills/brms-vxe-plus-developer/agents/openai.yaml +7 -0
- package/skills/brms-vxe-plus-developer/references/prototype-to-real.md +54 -0
- package/skills/brms-vxe-plus-developer/references/real-base-development.md +110 -0
- package/skills/brms-vxe-plus-developer/references/resource-index.md +43 -0
- package/skills/brms-vxe-plus-developer/references/review-checklist.md +49 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/01-mental-model.md +150 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/02-vxe-plus-form.md +302 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/03-vxe-plus-table.md +253 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/04-example-map.md +488 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/05-request-and-eiinfo.md +170 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/90-anti-patterns.md +137 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/README.md +43 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/README.md +21 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/A1/P0/A1P01601.vue +483 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/A1/P1/A1P11011.vue +444 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/AB/BP/ABBP0201.vue +1648 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/AM/AF/component/AMAF0601/Bidding/formConfig.ts +228 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/AM/AF/component/AMAF0601/Record/columns.ts +110 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/BM/BR/BMBR01.vue +130 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/BM/BR/component/BMBR01/columns.ts +94 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/BM/BR/component/BMBR01/formConfig.ts +108 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Change/formConfig.ts +123 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Change/index.vue +103 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Clause/columns.ts +48 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Clause/index.vue +202 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Correcte/formConfig.ts +117 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Correcte/index.vue +103 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Explain/Pay/Payment/formConfig.ts +90 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Explain/Pay/Payment/index.vue +42 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Explain/Pay/columns.ts +376 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Explain/Pay/index.vue +619 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Explain/Settle/Domestic/formConfig.ts +73 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Explain/Settle/Domestic/index.vue +47 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Explain/Settle/Foreign/formConfig.ts +141 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Explain/Settle/Foreign/index.vue +42 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Explain/Settle/columns.ts +123 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Explain/Settle/index.vue +593 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Explain/index.vue +68 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Fee/columns.ts +150 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Fee/index.vue +235 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Files/columns.ts +63 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Files/index.vue +117 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Goods/columns.ts +327 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Goods/index.vue +790 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Main/Base/Approve/formConfig.ts +341 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Main/Base/Approve/index.vue +63 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Main/Base/Approve2/formConfig.ts +232 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Main/Base/Approve2/index.vue +27 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Main/Base/Diff/columns.ts +46 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Main/Base/Diff/index.vue +92 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Main/Base/formConfig.ts +979 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Main/Base/index.vue +62 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Main/Other/formConfig.ts +179 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Main/Other/index.vue +140 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Main/Sign/formConfig.ts +118 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Main/Sign/index.vue +44 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Main/index.vue +168 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Party/Major/formConfig.ts +257 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Party/Major/index.vue +47 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Party/columns.ts +256 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Party/index.vue +738 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Price/formConfig.ts +174 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Price/index.vue +51 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/PM/PC/component/PMPC0101/Top/index.vue +924 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/SM/SW/SMSW0101.vue +567 -0
- package/skills/brms-vxe-plus-developer/references/vxe-plus-knowledge/sources/project/base/src/views/demo/index.vue +448 -0
- package/skills/brms-vxe-plus-developer/scripts/check-project.mjs +259 -0
- package/skills/brms-vxe-plus-developer/scripts/check-vxe-plus-page.mjs +137 -0
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2)
|
|
7
|
+
|
|
8
|
+
function usage() {
|
|
9
|
+
console.error('Usage: node scripts/validate-generated.mjs <generated-files-or-dirs...> [--repo-root <repo>]')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseArgs(argv) {
|
|
13
|
+
const paths = []
|
|
14
|
+
let repoRoot = process.cwd()
|
|
15
|
+
|
|
16
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
17
|
+
const arg = argv[i]
|
|
18
|
+
if (arg === '--repo-root') {
|
|
19
|
+
repoRoot = argv[i + 1]
|
|
20
|
+
i += 1
|
|
21
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
22
|
+
usage()
|
|
23
|
+
process.exit(0)
|
|
24
|
+
} else {
|
|
25
|
+
paths.push(arg)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!paths.length) {
|
|
30
|
+
usage()
|
|
31
|
+
process.exit(2)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { repoRoot: path.resolve(repoRoot), paths }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function walk(target) {
|
|
38
|
+
if (!fs.existsSync(target)) return []
|
|
39
|
+
const stat = fs.statSync(target)
|
|
40
|
+
if (stat.isFile()) return [target]
|
|
41
|
+
if (!stat.isDirectory()) return []
|
|
42
|
+
|
|
43
|
+
return fs.readdirSync(target, { withFileTypes: true }).flatMap((entry) => {
|
|
44
|
+
const child = path.join(target, entry.name)
|
|
45
|
+
if (entry.isDirectory()) return walk(child)
|
|
46
|
+
return [child]
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function lineNumber(text, index) {
|
|
51
|
+
return text.slice(0, index).split(/\r?\n/).length
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function addIssue(issues, severity, file, line, message) {
|
|
55
|
+
issues.push({ severity, file, line, message })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hasMarkerNear(text, index, markerPattern, lineWindow = 3) {
|
|
59
|
+
const beforeLines = text.slice(0, index).split(/\r?\n/)
|
|
60
|
+
const snippet = beforeLines.slice(Math.max(0, beforeLines.length - lineWindow - 1)).join('\n')
|
|
61
|
+
return markerPattern.test(snippet)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function findClosingBrace(text, openBraceIndex) {
|
|
65
|
+
let depth = 0
|
|
66
|
+
for (let i = openBraceIndex; i < text.length; i += 1) {
|
|
67
|
+
const char = text[i]
|
|
68
|
+
if (char === '{') depth += 1
|
|
69
|
+
if (char === '}') {
|
|
70
|
+
depth -= 1
|
|
71
|
+
if (depth === 0) return i
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return -1
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function findClosingPair(text, openIndex, openChar, closeChar) {
|
|
78
|
+
let depth = 0
|
|
79
|
+
let quote = ''
|
|
80
|
+
let escaped = false
|
|
81
|
+
|
|
82
|
+
for (let i = openIndex; i < text.length; i += 1) {
|
|
83
|
+
const char = text[i]
|
|
84
|
+
|
|
85
|
+
if (quote) {
|
|
86
|
+
if (escaped) {
|
|
87
|
+
escaped = false
|
|
88
|
+
} else if (char === '\\') {
|
|
89
|
+
escaped = true
|
|
90
|
+
} else if (char === quote) {
|
|
91
|
+
quote = ''
|
|
92
|
+
}
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
97
|
+
quote = char
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
if (char === openChar) depth += 1
|
|
101
|
+
if (char === closeChar) {
|
|
102
|
+
depth -= 1
|
|
103
|
+
if (depth === 0) return i
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return -1
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function findFunctionBlocks(script) {
|
|
111
|
+
const blocks = []
|
|
112
|
+
const patterns = [
|
|
113
|
+
/(?:^|\n)\s*(?:async\s+)?function\s+\w+\s*\([^)]*\)\s*\{/g,
|
|
114
|
+
/(?:^|\n)\s*(?:const|let)\s+\w+\s*=\s*(?:async\s*)?(?:\([^)]*\)|\w+)\s*=>\s*\{/g,
|
|
115
|
+
/(?:^|\n)\s*onMounted\s*\(\s*(?:async\s*)?\(\)\s*=>\s*\{/g,
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
for (const pattern of patterns) {
|
|
119
|
+
for (const match of script.matchAll(pattern)) {
|
|
120
|
+
const openBraceIndex = script.indexOf('{', match.index)
|
|
121
|
+
if (openBraceIndex === -1) continue
|
|
122
|
+
const closeBraceIndex = findClosingBrace(script, openBraceIndex)
|
|
123
|
+
if (closeBraceIndex === -1) continue
|
|
124
|
+
blocks.push({
|
|
125
|
+
index: match.index,
|
|
126
|
+
openBraceIndex,
|
|
127
|
+
closeBraceIndex,
|
|
128
|
+
text: script.slice(match.index, closeBraceIndex + 1),
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return blocks.sort((a, b) => a.index - b.index)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function extractDefaultRouteObjects(text) {
|
|
137
|
+
const defaultMatch = /export\s+default\s*\[/.exec(text)
|
|
138
|
+
if (!defaultMatch)
|
|
139
|
+
return []
|
|
140
|
+
|
|
141
|
+
const arrayOpen = text.indexOf('[', defaultMatch.index)
|
|
142
|
+
const arrayClose = findClosingPair(text, arrayOpen, '[', ']')
|
|
143
|
+
if (arrayOpen === -1 || arrayClose === -1)
|
|
144
|
+
return []
|
|
145
|
+
|
|
146
|
+
const objects = []
|
|
147
|
+
let i = arrayOpen + 1
|
|
148
|
+
while (i < arrayClose) {
|
|
149
|
+
if (/\s|,/.test(text[i])) {
|
|
150
|
+
i += 1
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
if (text[i] !== '{') {
|
|
154
|
+
i += 1
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
const objectClose = findClosingPair(text, i, '{', '}')
|
|
158
|
+
if (objectClose === -1 || objectClose > arrayClose)
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
const block = text.slice(i, objectClose + 1)
|
|
162
|
+
objects.push({
|
|
163
|
+
index: i,
|
|
164
|
+
text: block,
|
|
165
|
+
path: /\bpath\s*:\s*['"]([^'"]+)['"]/.exec(block)?.[1] || '',
|
|
166
|
+
name: /\bname\s*:\s*['"]([^'"]+)['"]/.exec(block)?.[1] || '',
|
|
167
|
+
component: /\bcomponent\s*:\s*['"]([^'"]+)['"]/.exec(block)?.[1] || '',
|
|
168
|
+
hasChildren: /\bchildren\s*:\s*\[/.test(block),
|
|
169
|
+
isHide: /\bisHide\s*:\s*true\b/.test(block),
|
|
170
|
+
})
|
|
171
|
+
i = objectClose + 1
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return objects
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function validatePageCode(code) {
|
|
178
|
+
if (!/^[A-Z0-9]{6,}$/.test(code)) {
|
|
179
|
+
return 'Page code must be 6+ uppercase letters/digits, for example DMDA01. Do not generate short codes such as KQ01.'
|
|
180
|
+
}
|
|
181
|
+
return ''
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function pageCodeFromComponent(component) {
|
|
185
|
+
const clean = component.replaceAll('\\', '/').replace(/\.vue$/i, '')
|
|
186
|
+
return clean.split('/').filter(Boolean).at(-1) || ''
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function validateViewPath(file, repoRoot, issues) {
|
|
190
|
+
const normalized = path.relative(repoRoot, file).replaceAll(path.sep, '/')
|
|
191
|
+
const match = /(?:^|\/)src\/views\/([A-Z0-9]{2})\/([A-Z0-9]{2})\/([A-Z0-9]+)\.vue$/.exec(normalized)
|
|
192
|
+
if (!match)
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
const [, moduleCode, submoduleCode, pageCode] = match
|
|
196
|
+
const pageCodeMessage = validatePageCode(pageCode)
|
|
197
|
+
if (pageCodeMessage)
|
|
198
|
+
addIssue(issues, 'error', file, 1, pageCodeMessage)
|
|
199
|
+
|
|
200
|
+
const expectedPrefix = `${moduleCode}${submoduleCode}`
|
|
201
|
+
if (!pageCode.startsWith(expectedPrefix)) {
|
|
202
|
+
addIssue(
|
|
203
|
+
issues,
|
|
204
|
+
'error',
|
|
205
|
+
file,
|
|
206
|
+
1,
|
|
207
|
+
`Generated page file path must match the page code prefix: ${pageCode}.vue should live under src/views/${pageCode.slice(0, 2)}/${pageCode.slice(2, 4)}/.`,
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getTemplateContent(text) {
|
|
213
|
+
const match = /<template\b[^>]*>([\s\S]*?)<\/template>/i.exec(text)
|
|
214
|
+
if (!match) return null
|
|
215
|
+
return { content: match[1], offset: match.index + match[0].indexOf(match[1]) }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function countTopLevelRoots(template) {
|
|
219
|
+
let depth = 0
|
|
220
|
+
let roots = 0
|
|
221
|
+
let firstRootIndex = -1
|
|
222
|
+
const tagRegex = /<!--[\s\S]*?-->|<\/?([A-Za-z][\w.-]*)(?:\s[^>]*)?>/g
|
|
223
|
+
|
|
224
|
+
for (const match of template.matchAll(tagRegex)) {
|
|
225
|
+
const raw = match[0]
|
|
226
|
+
const tag = match[1]
|
|
227
|
+
if (!tag || raw.startsWith('<!--')) continue
|
|
228
|
+
if (/^<!/.test(raw)) continue
|
|
229
|
+
const isClosing = raw.startsWith('</')
|
|
230
|
+
const isSelfClosing = /\/>$/.test(raw)
|
|
231
|
+
|
|
232
|
+
if (isClosing) {
|
|
233
|
+
depth = Math.max(0, depth - 1)
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (depth === 0) {
|
|
238
|
+
roots += 1
|
|
239
|
+
if (firstRootIndex === -1) firstRootIndex = match.index
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!isSelfClosing) depth += 1
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { roots, firstRootIndex }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function inspectMockMarkers(file, text, issues, { isVue, isDataFile }) {
|
|
249
|
+
const hasAnyMockDataMarker = /@brms-mock-data\b/.test(text)
|
|
250
|
+
const hasExplicitInterfaceMarker = /@brms-explicit-interface\b/.test(text)
|
|
251
|
+
const mockDataMarker = /@brms-mock-data\b/
|
|
252
|
+
const mockHandlerMarker = /@brms-mock-handler\b/
|
|
253
|
+
const explicitInterfaceMarker = /@brms-explicit-interface\b/
|
|
254
|
+
|
|
255
|
+
if (isVue) {
|
|
256
|
+
for (const match of text.matchAll(/<VxePlusTable\b[\s\S]*?(?:\/>|<\/VxePlusTable>)/g)) {
|
|
257
|
+
const tag = match[0]
|
|
258
|
+
if (/(?::data|v-bind:data)\s*=/.test(tag) && !/serviceConfig|service-config/.test(tag) && !hasAnyMockDataMarker) {
|
|
259
|
+
addIssue(
|
|
260
|
+
issues,
|
|
261
|
+
'error',
|
|
262
|
+
file,
|
|
263
|
+
lineNumber(text, match.index),
|
|
264
|
+
'Mock-backed VxePlusTable :data requires a searchable @brms-mock-data marker near the local mock data source.',
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const scriptMatch = isVue
|
|
271
|
+
? /<script\b[^>]*>([\s\S]*?)<\/script>/i.exec(text)
|
|
272
|
+
: { 1: text, index: 0 }
|
|
273
|
+
if (!scriptMatch)
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
const script = scriptMatch[1]
|
|
277
|
+
const scriptOffset = scriptMatch.index + (isVue ? scriptMatch[0].indexOf(script) : 0)
|
|
278
|
+
|
|
279
|
+
const mockDataPatterns = [
|
|
280
|
+
/\b(?:export\s+)?(?:const|let|var)\s+\w*mock\w*\s*(?::[^=]+)?=/gi,
|
|
281
|
+
/\b(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*ref(?:<[^>]+>)?\(\s*\w*mock\w*/gi,
|
|
282
|
+
]
|
|
283
|
+
if (isDataFile) {
|
|
284
|
+
mockDataPatterns.push(/\bexport\s+const\s+\w+\s*(?::[^=]+)?=\s*(?:\[|\{)/g)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const reportedMockDataLines = new Set()
|
|
288
|
+
for (const pattern of mockDataPatterns) {
|
|
289
|
+
for (const match of script.matchAll(pattern)) {
|
|
290
|
+
const absoluteIndex = scriptOffset + match.index
|
|
291
|
+
const line = lineNumber(text, absoluteIndex)
|
|
292
|
+
if (reportedMockDataLines.has(line))
|
|
293
|
+
continue
|
|
294
|
+
if (!hasMarkerNear(text, absoluteIndex, mockDataMarker)) {
|
|
295
|
+
reportedMockDataLines.add(line)
|
|
296
|
+
addIssue(
|
|
297
|
+
issues,
|
|
298
|
+
'error',
|
|
299
|
+
file,
|
|
300
|
+
line,
|
|
301
|
+
'Mock data declarations must have // @brms-mock-data <source-or-page>: <purpose> within the previous 3 lines.',
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const mockHandlerEvidence = /TODO:\s*接入.*接口|ElMessage\.info\s*\(\s*['"]TODO:.*接口|\.value\s*=\s*[^;\n]*(?:\.filter\s*\(|\.map\s*\()|(?:\.push|\.splice)\s*\(|\b(?:upsertLocalRow|saveLocalForm|queryLocalDetail|matchesQuery)\s*\(/s
|
|
308
|
+
for (const block of findFunctionBlocks(script)) {
|
|
309
|
+
if (!mockHandlerEvidence.test(block.text))
|
|
310
|
+
continue
|
|
311
|
+
const absoluteIndex = scriptOffset + block.index
|
|
312
|
+
const hasMockHandlerMarker = hasMarkerNear(text, absoluteIndex, mockHandlerMarker)
|
|
313
|
+
const hasExplicitNear = hasMarkerNear(text, absoluteIndex, explicitInterfaceMarker)
|
|
314
|
+
|
|
315
|
+
if (!hasMockHandlerMarker && !hasExplicitNear) {
|
|
316
|
+
addIssue(
|
|
317
|
+
issues,
|
|
318
|
+
'error',
|
|
319
|
+
file,
|
|
320
|
+
lineNumber(text, absoluteIndex),
|
|
321
|
+
'Mock data handlers must have // @brms-mock-handler <source-or-page>: <operation> within the previous 3 lines.',
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
if (hasMockHandlerMarker && (hasExplicitNear || hasExplicitInterfaceMarker && /\bEiCommunicator\b|\bserviceConfig\b|service-config/.test(block.text))) {
|
|
325
|
+
addIssue(
|
|
326
|
+
issues,
|
|
327
|
+
'warn',
|
|
328
|
+
file,
|
|
329
|
+
lineNumber(text, absoluteIndex),
|
|
330
|
+
'@brms-mock-handler appears near explicit interface code; remove stale mock markers after wiring the real interface.',
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function inspectEditModel(file, text, issues) {
|
|
337
|
+
const tableRegex = /<VxePlusTable\b[\s\S]*?(?:\/>|<\/VxePlusTable>)/g
|
|
338
|
+
const tableMatches = [...text.matchAll(tableRegex)]
|
|
339
|
+
const editTableMatches = tableMatches.filter(match => /\b(?::is-edit|is-edit)\s*(?:=|\/?>|\s)/.test(match[0]))
|
|
340
|
+
const formModalMatches = [...text.matchAll(/<vxe-modal\b[\s\S]*?<VxePlusForm\b[\s\S]*?<\/vxe-modal>/g)]
|
|
341
|
+
const hasFormEditModal = formModalMatches.some(match => /@confirm\s*=|编辑|新增/.test(match[0]))
|
|
342
|
+
|
|
343
|
+
if (hasFormEditModal) {
|
|
344
|
+
for (const match of editTableMatches) {
|
|
345
|
+
addIssue(
|
|
346
|
+
issues,
|
|
347
|
+
'error',
|
|
348
|
+
file,
|
|
349
|
+
lineNumber(text, match.index),
|
|
350
|
+
'Choose one edit model: dialog edit pages must not set VxePlusTable :is-edit="true"; row edit uses :is-edit and column editRender without an edit modal.',
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const columnBlockRegex = /createColumns\s*\(([\s\S]*?)\n\s*\]\s*(?:,\s*\{[\s\S]*?\})?\s*\)\s*;?/g
|
|
356
|
+
if (hasFormEditModal) {
|
|
357
|
+
for (const match of text.matchAll(columnBlockRegex)) {
|
|
358
|
+
const block = match[1]
|
|
359
|
+
if (/\beditRender\s*:/.test(block)) {
|
|
360
|
+
addIssue(
|
|
361
|
+
issues,
|
|
362
|
+
'error',
|
|
363
|
+
file,
|
|
364
|
+
lineNumber(text, match.index),
|
|
365
|
+
'Dialog edit pages must not put editRender on normal table columns; editRender belongs to the VxePlusTable row edit pattern.',
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
for (const match of text.matchAll(/@cell-click\s*=/g)) {
|
|
372
|
+
addIssue(
|
|
373
|
+
issues,
|
|
374
|
+
'warn',
|
|
375
|
+
file,
|
|
376
|
+
lineNumber(text, match.index),
|
|
377
|
+
'Avoid @cell-click in generated tables; use tableLink for business-field navigation and toolbarButtons for add/edit/delete.',
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const scriptMatch = /<script\b[^>]*>([\s\S]*?)<\/script>/i.exec(text)
|
|
382
|
+
if (!scriptMatch)
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
const script = scriptMatch[1]
|
|
386
|
+
const scriptOffset = scriptMatch.index + scriptMatch[0].indexOf(script)
|
|
387
|
+
const cellClickFunctionPatterns = [
|
|
388
|
+
/(?:^|\n)\s*(?:async\s+)?function\s+handleCellClick\s*\([^)]*\)\s*\{/g,
|
|
389
|
+
/(?:^|\n)\s*(?:const|let)\s+handleCellClick\s*=\s*(?:async\s*)?(?:\([^)]*\)|\w+)\s*=>\s*\{/g,
|
|
390
|
+
/(?:^|\n)\s*(?:cellClick|onCellClick)\s*:\s*(?:async\s*)?(?:\([^)]*\)|\w+)\s*=>\s*\{/g,
|
|
391
|
+
]
|
|
392
|
+
|
|
393
|
+
for (const pattern of cellClickFunctionPatterns) {
|
|
394
|
+
for (const match of script.matchAll(pattern)) {
|
|
395
|
+
const openBraceIndex = script.indexOf('{', match.index)
|
|
396
|
+
if (openBraceIndex === -1) continue
|
|
397
|
+
const closeBraceIndex = findClosingBrace(script, openBraceIndex)
|
|
398
|
+
if (closeBraceIndex === -1) continue
|
|
399
|
+
const block = script.slice(match.index, closeBraceIndex + 1)
|
|
400
|
+
const line = lineNumber(text, scriptOffset + match.index)
|
|
401
|
+
if (/\bhandleEdit\s*\(|\beditingId\b|\bmodal\.value\s*=\s*true/.test(block)) {
|
|
402
|
+
addIssue(
|
|
403
|
+
issues,
|
|
404
|
+
'error',
|
|
405
|
+
file,
|
|
406
|
+
line,
|
|
407
|
+
'Do not use cell-click/handleCellClick to open an edit modal; use toolbarButtonClick, selected row handling, or tableLink.',
|
|
408
|
+
)
|
|
409
|
+
} else {
|
|
410
|
+
addIssue(
|
|
411
|
+
issues,
|
|
412
|
+
'warn',
|
|
413
|
+
file,
|
|
414
|
+
line,
|
|
415
|
+
'Only keep cell-click when the product doc explicitly requires row-level navigation; otherwise use tableLink or toolbarButtons.',
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function inspectVue(file, text, issues, repoRoot) {
|
|
423
|
+
validateViewPath(file, repoRoot, issues)
|
|
424
|
+
|
|
425
|
+
const hasExplicitInterfaceMarker = /@brms-explicit-interface\b/.test(text)
|
|
426
|
+
const hasValidExplicitInterfaceMarker = /@brms-explicit-interface\s+[^\n:]+:\s*[A-Za-z]\w*\.[A-Za-z]\w*/.test(text)
|
|
427
|
+
|
|
428
|
+
const templateInfo = getTemplateContent(text)
|
|
429
|
+
if (templateInfo) {
|
|
430
|
+
const { roots, firstRootIndex } = countTopLevelRoots(templateInfo.content)
|
|
431
|
+
if (roots !== 1) {
|
|
432
|
+
addIssue(
|
|
433
|
+
issues,
|
|
434
|
+
'error',
|
|
435
|
+
file,
|
|
436
|
+
lineNumber(text, templateInfo.offset + Math.max(firstRootIndex, 0)),
|
|
437
|
+
`Generated Vue templates must have exactly one top-level root; found ${roots}. Put dialogs inside the main root wrapper.`,
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const unknownComponents = [
|
|
443
|
+
'VxePlusDialog',
|
|
444
|
+
'FileUploadDialog',
|
|
445
|
+
'UploadDialog',
|
|
446
|
+
'BxUploadDialog',
|
|
447
|
+
'AttachmentDialog',
|
|
448
|
+
]
|
|
449
|
+
for (const component of unknownComponents) {
|
|
450
|
+
const pattern = new RegExp(`<${component}\\b|</${component}>|\\b${component}\\b`)
|
|
451
|
+
const match = pattern.exec(text)
|
|
452
|
+
if (match) {
|
|
453
|
+
addIssue(
|
|
454
|
+
issues,
|
|
455
|
+
'error',
|
|
456
|
+
file,
|
|
457
|
+
lineNumber(text, match.index),
|
|
458
|
+
`Do not use unknown generated component ${component}; follow references/05-dialog-patterns.md.`,
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
for (const match of text.matchAll(/\btitlePrefix\s*:/g)) {
|
|
464
|
+
addIssue(
|
|
465
|
+
issues,
|
|
466
|
+
'error',
|
|
467
|
+
file,
|
|
468
|
+
lineNumber(text, match.index),
|
|
469
|
+
'Do not use titlePrefix as required validation; put required validators in formOption.rules keyed by field.',
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
for (const match of text.matchAll(/\bitems\s*:\s*\[/g)) {
|
|
474
|
+
const openIndex = text.indexOf('[', match.index)
|
|
475
|
+
const closeIndex = findClosingPair(text, openIndex, '[', ']')
|
|
476
|
+
if (openIndex === -1 || closeIndex === -1)
|
|
477
|
+
continue
|
|
478
|
+
const itemsBlock = text.slice(openIndex, closeIndex + 1)
|
|
479
|
+
for (const requiredMatch of itemsBlock.matchAll(/\brequired\s*:\s*true\b/g)) {
|
|
480
|
+
addIssue(
|
|
481
|
+
issues,
|
|
482
|
+
'error',
|
|
483
|
+
file,
|
|
484
|
+
lineNumber(text, openIndex + requiredMatch.index),
|
|
485
|
+
'Do not put required: true inside formOption.items; required validation must live in formOption.rules.',
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
for (const match of text.matchAll(/<el-upload\b/g)) {
|
|
491
|
+
addIssue(
|
|
492
|
+
issues,
|
|
493
|
+
'warn',
|
|
494
|
+
file,
|
|
495
|
+
lineNumber(text, match.index),
|
|
496
|
+
'Avoid hand-written el-upload in generated prototypes; attachment actions must call useFileUpload().openFileBFSS0001.',
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const containerRegex = /<BxContainer\b[\s\S]*?<\/BxContainer>|<bx-container\b[\s\S]*?<\/bx-container>/g
|
|
501
|
+
for (const match of text.matchAll(containerRegex)) {
|
|
502
|
+
const block = match[0]
|
|
503
|
+
if (/<template\s+#(?:left|right|default|custom-btn)\b[\s\S]*?<(?:el-button|vxe-button)\b/i.test(block)) {
|
|
504
|
+
addIssue(
|
|
505
|
+
issues,
|
|
506
|
+
'error',
|
|
507
|
+
file,
|
|
508
|
+
lineNumber(text, match.index),
|
|
509
|
+
'Do not put generated action buttons in BxContainer slots unless explicitly requested; use table toolbarButtons or query card buttons.',
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
for (const match of text.matchAll(/<BxContainer\b[^>]*custom-btn-list\s*=\s*["'][\s\S]*?["'][^>]*>/g)) {
|
|
515
|
+
const tag = match[0]
|
|
516
|
+
if (/新增|编辑|删除|add|edit|del|delete|ADD|EDIT|DEL|DELETE/.test(tag)) {
|
|
517
|
+
addIssue(
|
|
518
|
+
issues,
|
|
519
|
+
'error',
|
|
520
|
+
file,
|
|
521
|
+
lineNumber(text, match.index),
|
|
522
|
+
'Do not put generated add/edit/delete actions in BxContainer custom-btn-list; use VxePlusTable toolbarButtons to avoid duplicate header/table buttons.',
|
|
523
|
+
)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const modalRegex = /<vxe-modal\b[\s\S]*?<\/vxe-modal>/g
|
|
528
|
+
for (const match of text.matchAll(modalRegex)) {
|
|
529
|
+
const block = match[0]
|
|
530
|
+
const line = lineNumber(text, match.index)
|
|
531
|
+
const openTag = /<vxe-modal\b[^>]*>/i.exec(block)?.[0] || ''
|
|
532
|
+
if (/附件|上传|upload|attachment/i.test(block)) {
|
|
533
|
+
addIssue(
|
|
534
|
+
issues,
|
|
535
|
+
'error',
|
|
536
|
+
file,
|
|
537
|
+
line,
|
|
538
|
+
'Do not write custom attachment modals; attachment actions must call useFileUpload().openFileBFSS0001.',
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
if (/<VxePlusForm\b[^>]*:form-config\s*=/.test(block)) {
|
|
542
|
+
addIssue(
|
|
543
|
+
issues,
|
|
544
|
+
'error',
|
|
545
|
+
file,
|
|
546
|
+
line,
|
|
547
|
+
'Dialog VxePlusForm must use :form-options, not :form-config.',
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
const formCount = (block.match(/<VxePlusForm\b/g) || []).length
|
|
551
|
+
const containerCount = (block.match(/<(?:BxContainer|bx-container)\b/g) || []).length
|
|
552
|
+
if (formCount === 1 && containerCount > 0) {
|
|
553
|
+
addIssue(
|
|
554
|
+
issues,
|
|
555
|
+
'error',
|
|
556
|
+
file,
|
|
557
|
+
line,
|
|
558
|
+
'Do not wrap a simple one-section dialog form with BxContainer; the vxe-modal title is enough.',
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
if (/\bshow-footer\b/.test(openTag)) {
|
|
562
|
+
const requiredButtonProps = [
|
|
563
|
+
'show-cancel-button',
|
|
564
|
+
'show-confirm-button',
|
|
565
|
+
'cancel-button-text',
|
|
566
|
+
'confirm-button-text',
|
|
567
|
+
]
|
|
568
|
+
const missingProps = requiredButtonProps.filter(prop => !new RegExp(`\\b${prop}\\b`).test(openTag))
|
|
569
|
+
if (missingProps.length) {
|
|
570
|
+
addIssue(
|
|
571
|
+
issues,
|
|
572
|
+
'error',
|
|
573
|
+
file,
|
|
574
|
+
line,
|
|
575
|
+
`vxe-modal with show-footer must explicitly set ${missingProps.join(', ')} so generated dialogs render cancel/confirm buttons.`,
|
|
576
|
+
)
|
|
577
|
+
}
|
|
578
|
+
if (/<template\s+#footer\b/i.test(block)) {
|
|
579
|
+
addIssue(
|
|
580
|
+
issues,
|
|
581
|
+
'warn',
|
|
582
|
+
file,
|
|
583
|
+
line,
|
|
584
|
+
'Prefer built-in vxe-modal confirm/cancel button props with @confirm over custom footer slots in generated form dialogs.',
|
|
585
|
+
)
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (/附件|上传|attachment|upload/i.test(text)) {
|
|
591
|
+
const importsUseFileUpload = /useFileUpload/.test(text)
|
|
592
|
+
const callsOpenFileBFSS0001 = /openFileBFSS0001\s*\(/.test(text)
|
|
593
|
+
const customAttachmentModal = /<vxe-modal\b[\s\S]*?(附件|上传|upload|attachment)[\s\S]*?<\/vxe-modal>/i.test(text)
|
|
594
|
+
|| /<el-dialog\b[\s\S]*?(附件|上传|upload|attachment)[\s\S]*?<\/el-dialog>/i.test(text)
|
|
595
|
+
|| /<el-upload\b/i.test(text)
|
|
596
|
+
if (customAttachmentModal && (!importsUseFileUpload || !callsOpenFileBFSS0001)) {
|
|
597
|
+
addIssue(
|
|
598
|
+
issues,
|
|
599
|
+
'error',
|
|
600
|
+
file,
|
|
601
|
+
1,
|
|
602
|
+
'Attachment upload/view must use useFileUpload().openFileBFSS0001 instead of a custom modal/upload component.',
|
|
603
|
+
)
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const scriptMatch = /<script\b[^>]*>([\s\S]*?)<\/script>/i.exec(text)
|
|
608
|
+
if (scriptMatch && /<script\b[^>]*\blang=["']ts["'][^>]*>/i.test(scriptMatch[0])) {
|
|
609
|
+
const script = scriptMatch[1]
|
|
610
|
+
const jsxMatch = /\breturn\s*<\s*[A-Za-z][\w-]*/.exec(script)
|
|
611
|
+
|| /=>\s*<\s*[A-Za-z][\w-]*/.exec(script)
|
|
612
|
+
if (jsxMatch) {
|
|
613
|
+
addIssue(
|
|
614
|
+
issues,
|
|
615
|
+
'error',
|
|
616
|
+
file,
|
|
617
|
+
lineNumber(text, scriptMatch.index + jsxMatch.index),
|
|
618
|
+
'Do not write JSX/TSX in generated .vue files; use Vue template slots instead.',
|
|
619
|
+
)
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const tableRegex = /<VxePlusTable\b[\s\S]*?(?:\/>|<\/VxePlusTable>)/g
|
|
624
|
+
for (const match of text.matchAll(tableRegex)) {
|
|
625
|
+
const tag = match[0]
|
|
626
|
+
const line = lineNumber(text, match.index)
|
|
627
|
+
const hasColumns = /:columns\s*=/.test(tag)
|
|
628
|
+
const hasData = /(?::data|v-bind:data)\s*=/.test(tag)
|
|
629
|
+
const hasBackendConfig = /serviceConfig|service-config/.test(tag)
|
|
630
|
+
|
|
631
|
+
if (hasColumns && !hasData && !hasBackendConfig) {
|
|
632
|
+
addIssue(issues, 'error', file, line, 'VxePlusTable with columns must bind mock data using :data="...".')
|
|
633
|
+
}
|
|
634
|
+
if (hasBackendConfig && !hasExplicitInterfaceMarker) {
|
|
635
|
+
addIssue(issues, 'error', file, line, 'serviceConfig/service-config is allowed only when the source document explicitly names the interface; add @brms-explicit-interface with the source and service.method.')
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const columnBlockRegex = /createColumns\s*\(([\s\S]*?)\n\s*\]\s*(?:,\s*\{[\s\S]*?\})?\s*\)\s*;?/g
|
|
640
|
+
for (const match of text.matchAll(columnBlockRegex)) {
|
|
641
|
+
const block = match[1]
|
|
642
|
+
const line = lineNumber(text, match.index)
|
|
643
|
+
if (/(field\s*:\s*['"](?:action|actions|operation|operate)['"])|(title\s*:\s*['"]操作['"])/i.test(block)) {
|
|
644
|
+
addIssue(issues, 'error', file, line, 'Do not generate action/operation table columns.')
|
|
645
|
+
}
|
|
646
|
+
if (/type\s*:\s*['"]checkbox['"]/.test(block)) {
|
|
647
|
+
addIssue(issues, 'error', file, line, 'Do not generate checkbox columns inside createColumns; use :show-checkbox="true".')
|
|
648
|
+
}
|
|
649
|
+
if (!/\{\s*width\s*:\s*undefined\s*\}/.test(match[0]) && /field\s*:/.test(block)) {
|
|
650
|
+
addIssue(issues, 'warn', file, line, 'Prefer createColumns([...], { width: undefined }) for generated data columns.')
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const alwaysForbiddenBackend = [
|
|
655
|
+
/from\s+['"]@\/api['"]/,
|
|
656
|
+
/\bhttp\.(send|get|post|form)\s*\(/,
|
|
657
|
+
]
|
|
658
|
+
for (const pattern of alwaysForbiddenBackend) {
|
|
659
|
+
const match = pattern.exec(text)
|
|
660
|
+
if (match) {
|
|
661
|
+
addIssue(issues, 'error', file, lineNumber(text, match.index), 'Generated prototypes must not use REST/http APIs; BRMS explicit interfaces should use EiInfo/EiCommunicator.')
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const explicitBackend = [
|
|
666
|
+
/\bEiCommunicator\b/,
|
|
667
|
+
/from\s+['"]@eplat\/ei['"]/,
|
|
668
|
+
/\bserviceConfig\b/,
|
|
669
|
+
/service-config/,
|
|
670
|
+
]
|
|
671
|
+
for (const pattern of explicitBackend) {
|
|
672
|
+
const match = pattern.exec(text)
|
|
673
|
+
if (match && !hasExplicitInterfaceMarker) {
|
|
674
|
+
addIssue(issues, 'error', file, lineNumber(text, match.index), 'EiInfo/serviceConfig backend wiring requires an @brms-explicit-interface marker proving the product document named the service and method.')
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (hasExplicitInterfaceMarker) {
|
|
679
|
+
if (/\bEiCommunicator\.send\s*\(/.test(text) && !/new\s+EiInfo\s*\(/.test(text)) {
|
|
680
|
+
addIssue(issues, 'warn', file, 1, 'Explicit EiCommunicator.send usage should normally build an EiInfo request object.')
|
|
681
|
+
}
|
|
682
|
+
if (/\bserviceConfig\b/.test(text) && !/serviceName\s*:\s*['"][^'"]+['"]/.test(text)) {
|
|
683
|
+
addIssue(issues, 'error', file, 1, 'Explicit serviceConfig must include a concrete serviceName.')
|
|
684
|
+
}
|
|
685
|
+
if (!hasValidExplicitInterfaceMarker) {
|
|
686
|
+
addIssue(issues, 'error', file, 1, '@brms-explicit-interface marker must include the source document/section and service.method, for example: @brms-explicit-interface product-doc: BMBF56.query.')
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
for (const match of text.matchAll(/<BxContainer\b[^>]*custom-btn-auto[^>]*>/g)) {
|
|
691
|
+
const tag = match[0]
|
|
692
|
+
if (/结果|列表|项目列表|数据列表|Result|List/i.test(tag)) {
|
|
693
|
+
addIssue(issues, 'error', file, lineNumber(text, match.index), 'Do not put custom-btn-auto on result/table headers.')
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
for (const match of text.matchAll(/path\s*:\s*['"]\/web\/(?:[A-Z0-9]+\/)+[A-Z0-9]+['"]/g)) {
|
|
698
|
+
addIssue(
|
|
699
|
+
issues,
|
|
700
|
+
'error',
|
|
701
|
+
file,
|
|
702
|
+
lineNumber(text, match.index),
|
|
703
|
+
'Runtime navigation must use /web/<PAGE_CODE>, for example /web/DMDA01, not /web/DM/DA/DMDA01.',
|
|
704
|
+
)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
for (const match of text.matchAll(/['"]\/web\/(?:[A-Z0-9]+\/)+[A-Z0-9]+['"]/g)) {
|
|
708
|
+
addIssue(
|
|
709
|
+
issues,
|
|
710
|
+
'error',
|
|
711
|
+
file,
|
|
712
|
+
lineNumber(text, match.index),
|
|
713
|
+
'Runtime navigation/link strings must use /web/<PAGE_CODE>, for example /web/DMDA01, not /web/DM/DA/DMDA01.',
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
for (const match of text.matchAll(/path\s*:\s*['"]\/web\/([A-Z0-9]+)['"]/g)) {
|
|
718
|
+
const code = match[1]
|
|
719
|
+
const pageCodeMessage = validatePageCode(code)
|
|
720
|
+
if (pageCodeMessage)
|
|
721
|
+
addIssue(issues, 'error', file, lineNumber(text, match.index), pageCodeMessage)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
inspectEditModel(file, text, issues)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function inspectRoute(file, text, issues) {
|
|
728
|
+
if (/[\\/]localMenu\.ts$/.test(file)) {
|
|
729
|
+
addIssue(issues, 'error', file, 1, 'Do not edit localMenu.ts for generated prototypes; create a fresh route config file.')
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (!/export\s+default\s+\[/.test(text)) {
|
|
733
|
+
addIssue(issues, 'warn', file, 1, 'Generated route config should default-export an array.')
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const topLevelRoutes = extractDefaultRouteObjects(text)
|
|
737
|
+
const parentRoutes = topLevelRoutes.filter(route => route.hasChildren)
|
|
738
|
+
const topLevelPageRoutes = topLevelRoutes.filter(route => route.component && !route.hasChildren)
|
|
739
|
+
if (parentRoutes.length === 1 && topLevelPageRoutes.length) {
|
|
740
|
+
const parent = parentRoutes[0]
|
|
741
|
+
const childCodes = [...parent.text.matchAll(/\b(?:name|path)\s*:\s*['"]\/?([A-Z0-9]{6,})['"]/g)]
|
|
742
|
+
.map(match => match[1])
|
|
743
|
+
.filter(code => !/Menu$/.test(code))
|
|
744
|
+
const childPrefixes = new Set(childCodes.map(code => code.slice(0, 4)))
|
|
745
|
+
|
|
746
|
+
for (const route of topLevelPageRoutes) {
|
|
747
|
+
const code = pageCodeFromComponent(route.component) || route.name || route.path.replace(/^\//, '')
|
|
748
|
+
if (!validatePageCode(code) && childPrefixes.has(code.slice(0, 4))) {
|
|
749
|
+
addIssue(
|
|
750
|
+
issues,
|
|
751
|
+
'error',
|
|
752
|
+
file,
|
|
753
|
+
lineNumber(text, route.index),
|
|
754
|
+
`${code} belongs inside the same parent menu children as ${[...childPrefixes][0]} pages${route.isHide ? ' with meta.isHide: true' : ''}; do not put generated detail/jump-only pages as top-level route siblings.`,
|
|
755
|
+
)
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
for (const match of text.matchAll(/path\s*:\s*['"]\/(?:[A-Z0-9]+\/)+([A-Z][A-Z0-9]*)['"]/g)) {
|
|
761
|
+
addIssue(
|
|
762
|
+
issues,
|
|
763
|
+
'error',
|
|
764
|
+
file,
|
|
765
|
+
lineNumber(text, match.index),
|
|
766
|
+
'Generated dynamic route path must be /<PAGE_CODE> because auth.ts prefixes it with /web; keep physical folders only in component.',
|
|
767
|
+
)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
for (const match of text.matchAll(/\b(?:path|name)\s*:\s*['"]\/?([A-Z0-9]+)['"]/g)) {
|
|
771
|
+
const code = match[1]
|
|
772
|
+
if (/Menu$/.test(code))
|
|
773
|
+
continue
|
|
774
|
+
const pageCodeMessage = validatePageCode(code)
|
|
775
|
+
if (pageCodeMessage)
|
|
776
|
+
addIssue(issues, 'error', file, lineNumber(text, match.index), pageCodeMessage)
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
for (const match of text.matchAll(/\bcomponent\s*:\s*['"]([^'"]+)['"]/g)) {
|
|
780
|
+
const component = match[1]
|
|
781
|
+
const code = pageCodeFromComponent(component)
|
|
782
|
+
const pageCodeMessage = validatePageCode(code)
|
|
783
|
+
if (pageCodeMessage) {
|
|
784
|
+
addIssue(issues, 'error', file, lineNumber(text, match.index), pageCodeMessage)
|
|
785
|
+
continue
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const expectedComponent = `/${code.slice(0, 2)}/${code.slice(2, 4)}/${code}`
|
|
789
|
+
if (component !== expectedComponent) {
|
|
790
|
+
addIssue(
|
|
791
|
+
issues,
|
|
792
|
+
'error',
|
|
793
|
+
file,
|
|
794
|
+
lineNumber(text, match.index),
|
|
795
|
+
`Generated route component must match page code folders: ${code} should use component '${expectedComponent}'.`,
|
|
796
|
+
)
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const { repoRoot, paths } = parseArgs(args)
|
|
802
|
+
const files = paths
|
|
803
|
+
.map((target) => path.resolve(repoRoot, target))
|
|
804
|
+
.flatMap(walk)
|
|
805
|
+
.filter((file) => /\.(vue|ts)$/.test(file))
|
|
806
|
+
|
|
807
|
+
const issues = []
|
|
808
|
+
|
|
809
|
+
for (const file of files) {
|
|
810
|
+
const text = fs.readFileSync(file, 'utf8')
|
|
811
|
+
if (file.endsWith('.vue')) {
|
|
812
|
+
inspectMockMarkers(file, text, issues, { isVue: true, isDataFile: false })
|
|
813
|
+
inspectVue(file, text, issues, repoRoot)
|
|
814
|
+
}
|
|
815
|
+
if (path.basename(file) === 'data.ts') {
|
|
816
|
+
inspectMockMarkers(file, text, issues, { isVue: false, isDataFile: true })
|
|
817
|
+
}
|
|
818
|
+
if (file.includes(`${path.sep}routers${path.sep}config${path.sep}`) && file.endsWith('.ts')) {
|
|
819
|
+
inspectRoute(file, text, issues)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const rel = (file) => path.relative(repoRoot, file).replaceAll(path.sep, '/')
|
|
824
|
+
const errors = issues.filter((issue) => issue.severity === 'error')
|
|
825
|
+
const warnings = issues.filter((issue) => issue.severity === 'warn')
|
|
826
|
+
|
|
827
|
+
if (!issues.length) {
|
|
828
|
+
console.log('Generated prototype static validation passed.')
|
|
829
|
+
process.exit(0)
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
for (const issue of issues) {
|
|
833
|
+
console.log(`${issue.severity.toUpperCase()} ${rel(issue.file)}:${issue.line} ${issue.message}`)
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
console.log('')
|
|
837
|
+
console.log(`Static validation found ${errors.length} error(s) and ${warnings.length} warning(s).`)
|
|
838
|
+
process.exit(errors.length ? 1 : 0)
|