tha-cli 0.1.3 → 0.1.4
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/ARCHITECTURE.md +56 -31
- package/README.md +38 -24
- package/package.json +1 -1
- package/tha/cli.mjs +2 -0
- package/tha/commands/check.mjs +17 -18
- package/tha/commands/init.mjs +112 -0
- package/tha/commands/watch.mjs +67 -0
- package/tha/eslint-rules/executor-no-cross-import.js +53 -0
- package/tha/eslint-rules/executor-no-import-api.js +69 -0
- package/tha/eslint-rules/executor-no-import-utils.js +68 -0
- package/tha/eslint-rules/hub-no-import-steps.js +51 -0
- package/tha/eslint-rules/index.js +42 -0
- package/tha/eslint-rules/store-no-import-api.js +57 -0
- package/tha/eslint-rules/util-no-import-ui.js +78 -0
- package/tha/eslint-rules/view-no-create-module.js +68 -0
- package/tha/eslint-rules/view-no-import-api.js +87 -0
- package/tha/utils/eslint.mjs +288 -0
- package/tha/verify/boundary.mjs +0 -219
- package/tha/verify/rules.mjs +0 -91
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// ============ ESLint 自动配置:检测、安装、写配置、验证 ============
|
|
2
|
+
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { execSync } from 'node:child_process'
|
|
5
|
+
import { exists, read, writeAtomic } from './files.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 检测项目环境
|
|
9
|
+
* @param root 项目根目录
|
|
10
|
+
* @returns { hasEslint, packageManager, hasEslintConfig, hasVue }
|
|
11
|
+
*/
|
|
12
|
+
export function detectEslintEnv(root) {
|
|
13
|
+
const packageJsonPath = path.join(root, 'package.json')
|
|
14
|
+
let hasEslint = false
|
|
15
|
+
let hasVue = false
|
|
16
|
+
|
|
17
|
+
if (exists(packageJsonPath)) {
|
|
18
|
+
try {
|
|
19
|
+
const pkg = JSON.parse(read(packageJsonPath))
|
|
20
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
21
|
+
hasEslint = !!deps.eslint
|
|
22
|
+
hasVue = !!(deps.vue || deps['vue-router'])
|
|
23
|
+
} catch {
|
|
24
|
+
// package.json 解析失败,按没有处理
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 判断包管理器
|
|
29
|
+
let packageManager = 'npm'
|
|
30
|
+
if (exists(path.join(root, 'pnpm-lock.yaml'))) packageManager = 'pnpm'
|
|
31
|
+
else if (exists(path.join(root, 'yarn.lock'))) packageManager = 'yarn'
|
|
32
|
+
|
|
33
|
+
// 检测有没有 ESLint 配置文件(新旧格式都算)
|
|
34
|
+
const configFiles = [
|
|
35
|
+
'eslint.config.js',
|
|
36
|
+
'eslint.config.mjs',
|
|
37
|
+
'eslint.config.cjs',
|
|
38
|
+
'.eslintrc.json',
|
|
39
|
+
'.eslintrc.js',
|
|
40
|
+
'.eslintrc.cjs',
|
|
41
|
+
'.eslintrc.yml'
|
|
42
|
+
]
|
|
43
|
+
const hasEslintConfig = configFiles.some((file) => exists(path.join(root, file)))
|
|
44
|
+
|
|
45
|
+
return { hasEslint, packageManager, hasEslintConfig, hasVue }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 自动安装 ESLint 依赖
|
|
50
|
+
* @param root 项目根目录
|
|
51
|
+
* @param env 检测结果
|
|
52
|
+
* @returns { installed: string[], failed: string[] }
|
|
53
|
+
*/
|
|
54
|
+
export function installEslintDeps(root, env) {
|
|
55
|
+
const installed = []
|
|
56
|
+
const failed = []
|
|
57
|
+
|
|
58
|
+
const packages = ['eslint', '@eslint/js', '@typescript-eslint/parser', '@typescript-eslint/eslint-plugin']
|
|
59
|
+
if (env.hasVue) packages.push('eslint-plugin-vue')
|
|
60
|
+
|
|
61
|
+
const pm = env.packageManager
|
|
62
|
+
const installCmd = pm === 'npm'
|
|
63
|
+
? `npm install -D ${packages.join(' ')}`
|
|
64
|
+
: pm === 'yarn'
|
|
65
|
+
? `yarn add -D ${packages.join(' ')}`
|
|
66
|
+
: `pnpm add -D ${packages.join(' ')}`
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
execSync(installCmd, { cwd: root, stdio: 'inherit' })
|
|
70
|
+
installed.push(...packages)
|
|
71
|
+
} catch (error) {
|
|
72
|
+
failed.push(...packages)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { installed, failed }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 自动写 ESLint 配置(flat config 格式,ESLint 9+)
|
|
80
|
+
* @param root 项目根目录
|
|
81
|
+
* @param env 检测结果
|
|
82
|
+
* @returns { written: string, skipped: boolean }
|
|
83
|
+
*/
|
|
84
|
+
export function writeEslintConfig(root, env) {
|
|
85
|
+
// 已有配置文件,跳过(后面可以做追加逻辑)
|
|
86
|
+
if (env.hasEslintConfig) {
|
|
87
|
+
return { written: null, skipped: true }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 生成 flat config 格式(eslint.config.js)—— 最小配置,只保证 tha 规则能跑
|
|
91
|
+
const configContent = `import tsparser from '@typescript-eslint/parser'
|
|
92
|
+
import vueParser from 'vue-eslint-parser'
|
|
93
|
+
import tha from 'tha-cli/tha/eslint-rules/index.js'
|
|
94
|
+
|
|
95
|
+
export default [
|
|
96
|
+
{
|
|
97
|
+
ignores: ['dist/**', 'node_modules/**']
|
|
98
|
+
},
|
|
99
|
+
// .ts 文件直接用 typescript parser
|
|
100
|
+
{
|
|
101
|
+
files: ['**/*.ts'],
|
|
102
|
+
languageOptions: {
|
|
103
|
+
parser: tsparser,
|
|
104
|
+
ecmaVersion: 'latest',
|
|
105
|
+
sourceType: 'module'
|
|
106
|
+
},
|
|
107
|
+
plugins: {
|
|
108
|
+
tha
|
|
109
|
+
},
|
|
110
|
+
rules: {
|
|
111
|
+
// THA 架构边界规则
|
|
112
|
+
'tha/hub-no-import-steps': 'error',
|
|
113
|
+
'tha/executor-no-cross-import': 'error',
|
|
114
|
+
'tha/executor-no-import-api': 'error',
|
|
115
|
+
'tha/executor-no-import-utils': 'error',
|
|
116
|
+
'tha/view-no-import-api': 'error',
|
|
117
|
+
'tha/view-no-create-module': 'error',
|
|
118
|
+
'tha/store-no-import-api': 'error',
|
|
119
|
+
'tha/util-no-import-ui': 'error'
|
|
120
|
+
},
|
|
121
|
+
settings: {
|
|
122
|
+
tha: {
|
|
123
|
+
dirs: {
|
|
124
|
+
domains: 'src/domains',
|
|
125
|
+
views: 'src/views',
|
|
126
|
+
stores: 'src/stores',
|
|
127
|
+
api: 'src/api',
|
|
128
|
+
utils: 'src/utils'
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
// .vue 文件先用 vue-eslint-parser 解析模板,再用 typescript parser 解析 script
|
|
134
|
+
{
|
|
135
|
+
files: ['**/*.vue'],
|
|
136
|
+
languageOptions: {
|
|
137
|
+
parser: vueParser,
|
|
138
|
+
parserOptions: {
|
|
139
|
+
parser: tsparser,
|
|
140
|
+
ecmaVersion: 'latest',
|
|
141
|
+
sourceType: 'module'
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
plugins: {
|
|
145
|
+
tha
|
|
146
|
+
},
|
|
147
|
+
rules: {
|
|
148
|
+
// THA 架构边界规则
|
|
149
|
+
'tha/hub-no-import-steps': 'error',
|
|
150
|
+
'tha/executor-no-cross-import': 'error',
|
|
151
|
+
'tha/executor-no-import-api': 'error',
|
|
152
|
+
'tha/executor-no-import-utils': 'error',
|
|
153
|
+
'tha/view-no-import-api': 'error',
|
|
154
|
+
'tha/view-no-create-module': 'error',
|
|
155
|
+
'tha/store-no-import-api': 'error',
|
|
156
|
+
'tha/util-no-import-ui': 'error'
|
|
157
|
+
},
|
|
158
|
+
settings: {
|
|
159
|
+
tha: {
|
|
160
|
+
dirs: {
|
|
161
|
+
domains: 'src/domains',
|
|
162
|
+
views: 'src/views',
|
|
163
|
+
stores: 'src/stores',
|
|
164
|
+
api: 'src/api',
|
|
165
|
+
utils: 'src/utils'
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
`
|
|
172
|
+
|
|
173
|
+
const configPath = path.join(root, 'eslint.config.js')
|
|
174
|
+
writeAtomic(configPath, configContent)
|
|
175
|
+
|
|
176
|
+
return { written: 'eslint.config.js', skipped: false }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 验证 ESLint 规则是否生效
|
|
181
|
+
* @param root 项目根目录
|
|
182
|
+
* @returns { ok: boolean, message: string }
|
|
183
|
+
*/
|
|
184
|
+
export function verifyEslintRules(root) {
|
|
185
|
+
try {
|
|
186
|
+
// 跑 eslint --print-config,看输出里有没有 tha 规则
|
|
187
|
+
const output = execSync('npx eslint --print-config src/domains/user/index.ts', {
|
|
188
|
+
cwd: root,
|
|
189
|
+
encoding: 'utf8',
|
|
190
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const config = JSON.parse(output)
|
|
194
|
+
const hasThaRules = Object.keys(config.rules || {}).some((key) => key.startsWith('tha/'))
|
|
195
|
+
|
|
196
|
+
if (hasThaRules) {
|
|
197
|
+
return { ok: true, message: 'ESLint 规则已生效' }
|
|
198
|
+
} else {
|
|
199
|
+
return { ok: false, message: 'ESLint 规则未生效,请检查配置文件' }
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
// 文件不存在或者 eslint 跑不起来,不阻塞
|
|
203
|
+
return { ok: false, message: '无法验证 ESLint 规则(可能是项目还没有 domains 目录)' }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 检查 ESLint 配置完整性:有没有 tha 规则
|
|
209
|
+
* @param root 项目根目录
|
|
210
|
+
* @returns [{ rule, level, file, message }]
|
|
211
|
+
*/
|
|
212
|
+
export function checkEslintConfig(root) {
|
|
213
|
+
const problems = []
|
|
214
|
+
|
|
215
|
+
// 找 ESLint 配置文件(先找新格式,再找旧格式)
|
|
216
|
+
const configFiles = [
|
|
217
|
+
'eslint.config.js',
|
|
218
|
+
'eslint.config.mjs',
|
|
219
|
+
'eslint.config.cjs',
|
|
220
|
+
'.eslintrc.json',
|
|
221
|
+
'.eslintrc.js',
|
|
222
|
+
'.eslintrc.cjs',
|
|
223
|
+
'.eslintrc.yml'
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
const configFile = configFiles.find((file) => exists(path.join(root, file)))
|
|
227
|
+
|
|
228
|
+
if (!configFile) {
|
|
229
|
+
problems.push({
|
|
230
|
+
rule: 'ESLINT-CONFIG-MISSING',
|
|
231
|
+
level: 'warn',
|
|
232
|
+
file: '.eslintrc.json',
|
|
233
|
+
message: '没有 ESLint 配置文件,THA 规则不会生效'
|
|
234
|
+
})
|
|
235
|
+
return problems
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 读配置文件
|
|
239
|
+
let config
|
|
240
|
+
try {
|
|
241
|
+
if (configFile.endsWith('.json')) {
|
|
242
|
+
config = JSON.parse(read(path.join(root, configFile)))
|
|
243
|
+
} else {
|
|
244
|
+
// .js / .cjs 配置文件不好直接读,跳过解析,只提示
|
|
245
|
+
problems.push({
|
|
246
|
+
rule: 'ESLINT-CONFIG-UNCHECKED',
|
|
247
|
+
level: 'warn',
|
|
248
|
+
file: configFile,
|
|
249
|
+
message: 'JS 格式的 ESLint 配置无法自动检查,请确认有没有加 tha 规则'
|
|
250
|
+
})
|
|
251
|
+
return problems
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
problems.push({
|
|
255
|
+
rule: 'ESLINT-CONFIG-PARSE-ERROR',
|
|
256
|
+
level: 'error',
|
|
257
|
+
file: configFile,
|
|
258
|
+
message: 'ESLint 配置文件解析失败'
|
|
259
|
+
})
|
|
260
|
+
return problems
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 检查 plugins 里有没有 tha
|
|
264
|
+
const plugins = config.plugins || []
|
|
265
|
+
if (!plugins.includes('tha')) {
|
|
266
|
+
problems.push({
|
|
267
|
+
rule: 'ESLINT-PLUGIN-MISSING',
|
|
268
|
+
level: 'error',
|
|
269
|
+
file: configFile,
|
|
270
|
+
message: 'ESLint 配置里没有 tha 插件,THA 规则不会生效'
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 检查 rules 里有没有 tha 规则
|
|
275
|
+
const rules = config.rules || {}
|
|
276
|
+
const thaRules = Object.keys(rules).filter((key) => key.startsWith('tha/'))
|
|
277
|
+
|
|
278
|
+
if (thaRules.length === 0) {
|
|
279
|
+
problems.push({
|
|
280
|
+
rule: 'ESLINT-RULES-MISSING',
|
|
281
|
+
level: 'error',
|
|
282
|
+
file: configFile,
|
|
283
|
+
message: 'ESLint 配置里没有 tha 规则,边界检查不会生效'
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return problems
|
|
288
|
+
}
|
package/tha/verify/boundary.mjs
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
// ============ 护栏检查:按「层」判 import 越界,不按模块 ============
|
|
2
|
-
// 模块级(中枢 / 执行器 / 零件)按 <domains>/<模块> 扫;
|
|
3
|
-
// 层级(视图 / 状态 / 基础设施)全局扫一遍 —— 因为文件边界是层边界,不是模块边界。
|
|
4
|
-
// 所有判定只看运行时 import(import type 放行):类型不构成依赖。
|
|
5
|
-
// 目录前缀一律由目录映射推出,规则本身不认识具体目录名。
|
|
6
|
-
|
|
7
|
-
import path from 'node:path'
|
|
8
|
-
import { importsFrom } from './ast.mjs'
|
|
9
|
-
import { PURE_UTILS, RULES } from './rules.mjs'
|
|
10
|
-
import { importSpec } from '../config/paths.mjs'
|
|
11
|
-
import { read, relativePath, walkFiles } from '../utils/files.mjs'
|
|
12
|
-
|
|
13
|
-
const PURE_NAMES = PURE_UTILS.map((spec) => spec.replace(/^@\/utils\//, ''))
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* 护栏检查入口
|
|
17
|
-
* @param modules 模块描述列表(见 scan.mjs 的 describeModule)
|
|
18
|
-
* @param root 项目根目录
|
|
19
|
-
* @param dirs 目录映射
|
|
20
|
-
* @returns 偏离清单 [{ rule, level, file, line, message, basis, fix }]
|
|
21
|
-
*/
|
|
22
|
-
export function checkBoundaries(modules, root, dirs) {
|
|
23
|
-
const problems = []
|
|
24
|
-
importCache.clear()
|
|
25
|
-
|
|
26
|
-
for (const module of modules) inspectModule(module, root, dirs, problems)
|
|
27
|
-
|
|
28
|
-
inspectViews(root, dirs, problems)
|
|
29
|
-
inspectStores(root, dirs, problems)
|
|
30
|
-
inspectUtils(root, dirs, problems)
|
|
31
|
-
|
|
32
|
-
return problems.sort((left, right) => left.file.localeCompare(right.file) || left.line - right.line)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function report(problems, rule, file, line, note = '') {
|
|
36
|
-
const meta = RULES[rule]
|
|
37
|
-
problems.push({
|
|
38
|
-
rule,
|
|
39
|
-
level: meta.level,
|
|
40
|
-
file,
|
|
41
|
-
line,
|
|
42
|
-
message: meta.title,
|
|
43
|
-
note,
|
|
44
|
-
basis: meta.basis,
|
|
45
|
-
fix: meta.fix
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const importCache = new Map()
|
|
50
|
-
|
|
51
|
-
function imports(filename) {
|
|
52
|
-
const cached = importCache.get(filename)
|
|
53
|
-
if (cached) return cached
|
|
54
|
-
|
|
55
|
-
const parsed = importsFrom(filename, read(filename))
|
|
56
|
-
importCache.set(filename, parsed)
|
|
57
|
-
return parsed
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** 模块级:中枢不许抓零件、中枢应单例、执行器互不引用、执行器只认 ctx */
|
|
61
|
-
function inspectModule(module, root, dirs, problems) {
|
|
62
|
-
if (module.hub) {
|
|
63
|
-
const hubFile = path.join(root, module.hub)
|
|
64
|
-
const hubText = read(hubFile)
|
|
65
|
-
|
|
66
|
-
for (const imported of importsFrom(hubFile, hubText)) {
|
|
67
|
-
if (/^(\.\.?\/)+steps\//.test(imported.spec)) {
|
|
68
|
-
report(problems, 'HUB-01', module.hub, imported.line, `import ${imported.spec}`)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!isSingletonHub(hubText)) {
|
|
73
|
-
report(problems, 'HUB-03', module.hub, 1, '没找到缓存的实例(每次调用都会新建一个)')
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const transportPrefixes = [
|
|
78
|
-
importSpec(dirs, 'api'),
|
|
79
|
-
importSpec(dirs, 'stores'),
|
|
80
|
-
importSpec(dirs, 'router'),
|
|
81
|
-
'@/store'
|
|
82
|
-
]
|
|
83
|
-
|
|
84
|
-
for (const executor of module.executors) {
|
|
85
|
-
for (const imported of imports(executor)) {
|
|
86
|
-
const file = relativePath(root, executor)
|
|
87
|
-
|
|
88
|
-
if (/^(\.\.?\/)+executors\//.test(imported.spec) || imported.spec.startsWith('./')) {
|
|
89
|
-
report(problems, 'EXE-01', file, imported.line, `import ${imported.spec}`)
|
|
90
|
-
}
|
|
91
|
-
if (matchesAnyPrefix(imported.spec, transportPrefixes)) {
|
|
92
|
-
report(problems, 'EXE-02', file, imported.line, `import ${imported.spec}`)
|
|
93
|
-
}
|
|
94
|
-
if (isForbiddenUtil(imported.spec, dirs)) {
|
|
95
|
-
report(problems, 'EXE-03', file, imported.line, `import ${imported.spec}`)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
for (const step of module.steps) {
|
|
101
|
-
for (const imported of imports(step)) {
|
|
102
|
-
const file = relativePath(root, step)
|
|
103
|
-
|
|
104
|
-
if (matchesAnyPrefix(imported.spec, transportPrefixes)) {
|
|
105
|
-
report(problems, 'EXE-02', file, imported.line, `import ${imported.spec}`)
|
|
106
|
-
}
|
|
107
|
-
if (isForbiddenUtil(imported.spec, dirs)) {
|
|
108
|
-
report(problems, 'EXE-03', file, imported.line, `import ${imported.spec}`)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// 零件复用数:只被 0 或 1 个执行器引用 = 抽早了或死代码
|
|
114
|
-
for (const step of module.steps) {
|
|
115
|
-
const name = path.basename(step, '.ts')
|
|
116
|
-
const users = new Set()
|
|
117
|
-
|
|
118
|
-
for (const executor of module.executors) {
|
|
119
|
-
for (const imported of imports(executor)) {
|
|
120
|
-
if (new RegExp(`steps/${escapeRegExp(name)}$`).test(imported.spec)) {
|
|
121
|
-
users.add(executor)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (users.size < 2) {
|
|
127
|
-
report(
|
|
128
|
-
problems,
|
|
129
|
-
'STEP-01',
|
|
130
|
-
relativePath(root, step),
|
|
131
|
-
1,
|
|
132
|
-
users.size === 0 ? '没有被任何执行器使用' : '只有 1 个执行器使用'
|
|
133
|
-
)
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** 视图层:不碰网络层、不碰执行器与零件、不自己造中枢实例 */
|
|
139
|
-
function inspectViews(root, dirs, problems) {
|
|
140
|
-
const apiBase = importSpec(dirs, 'api')
|
|
141
|
-
const utilsBase = importSpec(dirs, 'utils')
|
|
142
|
-
const domainsBase = importSpec(dirs, 'domains')
|
|
143
|
-
const transportUtils = new Set([`${utilsBase}/request`, `${utilsBase}/stream`, `${utilsBase}/token`])
|
|
144
|
-
const factories = new RegExp(`^${escapeRegExp(domainsBase)}\\/[^/]+$`)
|
|
145
|
-
|
|
146
|
-
for (const file of walkFiles(path.join(root, dirs.views), ['.ts', '.vue'])) {
|
|
147
|
-
const rel = relativePath(root, file)
|
|
148
|
-
for (const imported of imports(file)) {
|
|
149
|
-
if (
|
|
150
|
-
imported.spec.startsWith(`${apiBase}/`) ||
|
|
151
|
-
transportUtils.has(imported.spec) ||
|
|
152
|
-
new RegExp(`${escapeRegExp(domainsBase)}\\/[^/]+\\/(executors|steps)\\/`).test(imported.spec)
|
|
153
|
-
) {
|
|
154
|
-
report(problems, 'VIEW-01', rel, imported.line, `import ${imported.spec}`)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (factories.test(imported.spec)) {
|
|
158
|
-
const factory = imported.names.find((name) => /^create[A-Z]\w*Module$/.test(name))
|
|
159
|
-
if (factory) report(problems, 'VIEW-02', rel, imported.line, `import { ${factory} } from '${imported.spec}'`)
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** 状态层:不发请求 */
|
|
166
|
-
function inspectStores(root, dirs, problems) {
|
|
167
|
-
const apiBase = importSpec(dirs, 'api')
|
|
168
|
-
|
|
169
|
-
for (const file of walkFiles(path.join(root, dirs.stores), ['.ts'])) {
|
|
170
|
-
const rel = relativePath(root, file)
|
|
171
|
-
for (const imported of imports(file)) {
|
|
172
|
-
if (imported.spec.startsWith(`${apiBase}/`)) {
|
|
173
|
-
report(problems, 'STORE-01', rel, imported.line, `import ${imported.spec}`)
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** 基础设施:只上报不越权(不做展示、不感知业务模块) */
|
|
180
|
-
function inspectUtils(root, dirs, problems) {
|
|
181
|
-
const viewsBase = importSpec(dirs, 'views')
|
|
182
|
-
const domainsBase = importSpec(dirs, 'domains')
|
|
183
|
-
|
|
184
|
-
for (const file of walkFiles(path.join(root, dirs.utils), ['.ts'])) {
|
|
185
|
-
const rel = relativePath(root, file)
|
|
186
|
-
for (const imported of imports(file)) {
|
|
187
|
-
if (
|
|
188
|
-
imported.spec === 'element-plus' ||
|
|
189
|
-
imported.spec.startsWith(`${viewsBase}/`) ||
|
|
190
|
-
imported.spec.startsWith(`${domainsBase}/`)
|
|
191
|
-
) {
|
|
192
|
-
report(problems, 'UTIL-01', rel, imported.line, `import ${imported.spec}`)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function matchesAnyPrefix(spec, prefixes) {
|
|
199
|
-
return prefixes.some((prefix) => spec === prefix || spec.startsWith(`${prefix}/`))
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function isForbiddenUtil(spec, dirs) {
|
|
203
|
-
const base = importSpec(dirs, 'utils')
|
|
204
|
-
if (spec === base) return true
|
|
205
|
-
if (!spec.startsWith(`${base}/`)) return false
|
|
206
|
-
return !PURE_NAMES.includes(spec.slice(base.length + 1))
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** 中枢是否模块级单例:惰性缓存(let + if (!x))或模块级常量 */
|
|
210
|
-
function isSingletonHub(text) {
|
|
211
|
-
if (/(?:export\s+)?const\s+\w+\s*(?::\s*[^=]+)?=\s*create[A-Z]\w*Module\s*\(/.test(text)) return true
|
|
212
|
-
|
|
213
|
-
const cached = [...text.matchAll(/\blet\s+([A-Za-z_$][\w$]*)/g)].map((match) => match[1])
|
|
214
|
-
return cached.some((name) => new RegExp(`if\\s*\\(\\s*!\\s*${escapeRegExp(name)}\\s*\\)`).test(text))
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function escapeRegExp(value) {
|
|
218
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
219
|
-
}
|
package/tha/verify/rules.mjs
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
// ============ 护栏规则表:每条规则只在这里定义一次 ============
|
|
2
|
-
// 规则 id 是「文档 ↔ 检查器」的唯一纽带:架构图改了哪句,就改这条规则的 basis;
|
|
3
|
-
// 输出里出现哪个 id,就能回溯到哪句架构约束。
|
|
4
|
-
// level:error = 破架构(退出码 1);warn = 灰区(--strict 时才算失败)。
|
|
5
|
-
// 判断逻辑在 boundary.mjs,这里只放「说给人和 CI 听的话」。
|
|
6
|
-
//
|
|
7
|
-
// 文案口径:title / fix 是给开发看的结果行,只用业务语言,不出现中枢 / 零件 / 生成位这类架构名词;
|
|
8
|
-
// basis 保留架构原话,用于回溯到文档。
|
|
9
|
-
|
|
10
|
-
export const RULES = {
|
|
11
|
-
'HUB-01': {
|
|
12
|
-
level: 'error',
|
|
13
|
-
title: '模块入口绕开动作,直接调内部函数',
|
|
14
|
-
basis: '中枢只做分发:把意图交给执行器;禁止直接依赖执行器内部步骤',
|
|
15
|
-
fix: '把这类控制动作放进动作文件(如 stop),或由 context.ts 提供'
|
|
16
|
-
},
|
|
17
|
-
'HUB-03': {
|
|
18
|
-
level: 'warn',
|
|
19
|
-
title: '模块入口不是单例,被重复创建',
|
|
20
|
-
basis: '每个领域全局唯一实例,视图不能各造一个「私人大脑」',
|
|
21
|
-
fix: '在模块顶层用 let 缓存:if (!module) module = createXxxModule()'
|
|
22
|
-
},
|
|
23
|
-
'EXE-01': {
|
|
24
|
-
level: 'error',
|
|
25
|
-
title: '两个动作文件互相 import',
|
|
26
|
-
basis: '执行器之间互不引用:共用逻辑下沉成零件,用例顺序留在各自执行器里',
|
|
27
|
-
fix: '把共用的那段抽到 steps/,两个动作都调它'
|
|
28
|
-
},
|
|
29
|
-
'EXE-02': {
|
|
30
|
-
level: 'error',
|
|
31
|
-
title: '动作文件绕开 ctx,直接调接口或改状态',
|
|
32
|
-
basis: '执行器只认 ctx:网络走 ctx.api,状态走 ctx.state',
|
|
33
|
-
fix: '改从 ctx.api / ctx.state 取;缺什么就往 context.ts 里加'
|
|
34
|
-
},
|
|
35
|
-
'EXE-03': {
|
|
36
|
-
level: 'error',
|
|
37
|
-
title: '动作文件直接用工具库(该从 ctx 拿)',
|
|
38
|
-
basis: '执行器只认 ctx:日志/时钟/流/鉴权走 ctx.ports,判定走 ctx.errors',
|
|
39
|
-
fix: '把实现放进 context.ts 的端口;确实是纯函数的登记到纯函数白名单'
|
|
40
|
-
},
|
|
41
|
-
'STEP-01': {
|
|
42
|
-
level: 'warn',
|
|
43
|
-
title: '共用的函数只有一个动作在用',
|
|
44
|
-
basis: '零件是「同一段步骤被第二个用例用到」才下沉的;只有一个使用者是过度设计',
|
|
45
|
-
fix: '先内联回动作文件;真有第二个动作用时再抽出来'
|
|
46
|
-
},
|
|
47
|
-
'VIEW-01': {
|
|
48
|
-
level: 'error',
|
|
49
|
-
title: '页面绕开模块入口,直接调接口',
|
|
50
|
-
basis: '视图只表达意图:动作走域中枢,数据直读状态层',
|
|
51
|
-
fix: '改调 useXxxModule() 上的动作;缺动作就在模块入口加一个'
|
|
52
|
-
},
|
|
53
|
-
'VIEW-02': {
|
|
54
|
-
level: 'error',
|
|
55
|
-
title: '页面自己创建了模块实例',
|
|
56
|
-
basis: '每个领域全局唯一实例:视图只能经 useXxxModule() 取,不能自己 create',
|
|
57
|
-
fix: '改用 useXxxModule();需要独立能力就在模块入口加动作'
|
|
58
|
-
},
|
|
59
|
-
'STORE-01': {
|
|
60
|
-
level: 'error',
|
|
61
|
-
title: '状态文件里发请求',
|
|
62
|
-
basis: '状态层只存状态,不发请求',
|
|
63
|
-
fix: '把请求挪进动作文件,拿到结果再写回 state'
|
|
64
|
-
},
|
|
65
|
-
'UTIL-01': {
|
|
66
|
-
level: 'error',
|
|
67
|
-
title: '工具文件里用了 UI 库或业务模块',
|
|
68
|
-
basis: '传输层只上报不越权:不感知业务模块,不做展示',
|
|
69
|
-
fix: '提示交给页面(订阅错误总线);业务判断交给动作文件'
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* 执行器/零件允许直接 import 的纯函数工具
|
|
75
|
-
* 判据:无副作用、不持有状态、不依赖运行时环境(换实现无意义)
|
|
76
|
-
* 新增一条必须写理由:它为什么不属于「外界」
|
|
77
|
-
*/
|
|
78
|
-
export const PURE_UTILS = ['@/utils/adapters', '@/utils/format', '@/utils/number']
|
|
79
|
-
|
|
80
|
-
/** 基础设施里属于「能力」而非「纯函数」的模块:必须经 ctx.ports 注入 */
|
|
81
|
-
export const PORT_UTILS = [
|
|
82
|
-
'@/utils/stream',
|
|
83
|
-
'@/utils/request',
|
|
84
|
-
'@/utils/auth',
|
|
85
|
-
'@/utils/logger',
|
|
86
|
-
'@/utils/stopwatch',
|
|
87
|
-
'@/utils/time',
|
|
88
|
-
'@/utils/token',
|
|
89
|
-
'@/utils/errorBus',
|
|
90
|
-
'@/utils/error'
|
|
91
|
-
]
|