tha-cli 0.0.0-stage → 0.1.1

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.
Files changed (52) hide show
  1. package/ARCHITECTURE.md +193 -0
  2. package/LICENSE +21 -0
  3. package/README.md +406 -2
  4. package/package.json +42 -4
  5. package/tha/cli.mjs +116 -0
  6. package/tha/commands/adopt.mjs +119 -0
  7. package/tha/commands/check.mjs +226 -0
  8. package/tha/commands/diff.mjs +70 -0
  9. package/tha/commands/init.mjs +262 -0
  10. package/tha/commands/remove.mjs +328 -0
  11. package/tha/commands/sync.mjs +826 -0
  12. package/tha/config/field.mjs +178 -0
  13. package/tha/config/load.mjs +188 -0
  14. package/tha/config/manifest.mjs +75 -0
  15. package/tha/config/paths.mjs +124 -0
  16. package/tha/config/project.mjs +52 -0
  17. package/tha/config/rules.mjs +85 -0
  18. package/tha/config/schema.mjs +306 -0
  19. package/tha/config/template.mjs +78 -0
  20. package/tha/generate/domain/api.mjs +67 -0
  21. package/tha/generate/domain/context.mjs +76 -0
  22. package/tha/generate/domain/detail.mjs +35 -0
  23. package/tha/generate/domain/entry.mjs +91 -0
  24. package/tha/generate/domain/executors.mjs +93 -0
  25. package/tha/generate/domain/form.mjs +34 -0
  26. package/tha/generate/domain/index.mjs +72 -0
  27. package/tha/generate/domain/permissions.mjs +35 -0
  28. package/tha/generate/domain/store.mjs +68 -0
  29. package/tha/generate/domain/view.mjs +29 -0
  30. package/tha/generate/global.mjs +111 -0
  31. package/tha/generate/header.mjs +29 -0
  32. package/tha/generate/names.mjs +45 -0
  33. package/tha/generate/plan.mjs +41 -0
  34. package/tha/kit/ThaCrudPage.vue +344 -0
  35. package/tha/kit/ThaDetail.vue +146 -0
  36. package/tha/kit/ThaForm.vue +194 -0
  37. package/tha/kit/request.ts +30 -0
  38. package/tha/kit/transport.ts +11 -0
  39. package/tha/utils/args.mjs +59 -0
  40. package/tha/utils/compare.mjs +38 -0
  41. package/tha/utils/files.mjs +175 -0
  42. package/tha/utils/format.mjs +23 -0
  43. package/tha/utils/output.mjs +79 -0
  44. package/tha/utils/style.mjs +147 -0
  45. package/tha/utils/ts.mjs +73 -0
  46. package/tha/verify/ast.mjs +143 -0
  47. package/tha/verify/boundary.mjs +219 -0
  48. package/tha/verify/contract.mjs +186 -0
  49. package/tha/verify/openapi.mjs +345 -0
  50. package/tha/verify/rules.mjs +91 -0
  51. package/tha/verify/scan.mjs +59 -0
  52. package/tha.mjs +22 -0
@@ -0,0 +1,143 @@
1
+ // ============ AST 工具:import 关系与中枢函数体检查 ============
2
+ // 契约检查与护栏检查共用这一份实现:同一件事只解析一次,判定口径不会分叉。
3
+ // .vue 文件先抽 script 块再解析,行号回填到文件真实行。
4
+
5
+ import { ts } from '../utils/ts.mjs'
6
+
7
+ /**
8
+ * 解析文件(.vue 只取 script 块)
9
+ * @param file 文件绝对路径(用扩展名判断是否 .vue)
10
+ * @param text 文件内容
11
+ * @returns 每个 script 块与其起始行偏移
12
+ */
13
+ function parseBlocks(file, text) {
14
+ if (!file.endsWith('.vue')) {
15
+ return [{ sourceFile: ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS), offset: 0 }]
16
+ }
17
+
18
+ const blocks = []
19
+ const pattern = /<script\b[^>]*>([\s\S]*?)<\/script>/gi
20
+ let match
21
+
22
+ while ((match = pattern.exec(text))) {
23
+ const code = match[1]
24
+ const codeStart = match.index + match[0].indexOf(code)
25
+ const offset = text.slice(0, codeStart).split('\n').length - 1
26
+ blocks.push({
27
+ sourceFile: ts.createSourceFile(file, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS),
28
+ offset
29
+ })
30
+ }
31
+
32
+ return blocks.length > 0
33
+ ? blocks
34
+ : [{ sourceFile: ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS), offset: 0 }]
35
+ }
36
+
37
+ function isTypeOnly(node) {
38
+ if (ts.isExportDeclaration(node)) return node.isTypeOnly === true
39
+
40
+ const clause = node.importClause
41
+ if (!clause) return false
42
+ if (clause.isTypeOnly) return true
43
+ if (clause.name) return false
44
+
45
+ const bindings = clause.namedBindings
46
+ if (!bindings) return false
47
+ if (ts.isNamespaceImport(bindings)) return false
48
+
49
+ return bindings.elements.length > 0 && bindings.elements.every((element) => element.isTypeOnly)
50
+ }
51
+
52
+ function specifierOf(node) {
53
+ if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
54
+ const specifier = node.moduleSpecifier
55
+ if (specifier && ts.isStringLiteral(specifier)) return specifier
56
+ }
57
+ return null
58
+ }
59
+
60
+ function importedNames(node) {
61
+ if (ts.isExportDeclaration(node)) {
62
+ const clause = node.exportClause
63
+ if (!clause || !ts.isNamedExports(clause)) return []
64
+ return clause.elements.filter((element) => !element.isTypeOnly).map((element) => element.name.text)
65
+ }
66
+
67
+ const clause = node.importClause
68
+ if (!clause) return []
69
+
70
+ const names = []
71
+ if (clause.name) names.push(clause.name.text)
72
+
73
+ const bindings = clause.namedBindings
74
+ if (bindings) {
75
+ if (ts.isNamespaceImport(bindings)) names.push(bindings.name.text)
76
+ else names.push(...bindings.elements.filter((element) => !element.isTypeOnly).map((element) => element.name.text))
77
+ }
78
+
79
+ return names
80
+ }
81
+
82
+ /**
83
+ * 提取文件里的运行时依赖(含 re-export;`import type` 不计)
84
+ * @param file 文件绝对路径
85
+ * @param text 文件内容
86
+ * @returns [{ spec, line, names }]:模块路径、真实行号、引入的本地名
87
+ */
88
+ export function importsFrom(file, text) {
89
+ const imports = []
90
+
91
+ for (const block of parseBlocks(file, text)) {
92
+ const { sourceFile, offset } = block
93
+
94
+ for (const node of sourceFile.statements) {
95
+ const specifier = specifierOf(node)
96
+ if (!specifier || isTypeOnly(node)) continue
97
+
98
+ imports.push({
99
+ spec: specifier.text,
100
+ line: offset + sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
101
+ names: importedNames(node)
102
+ })
103
+ }
104
+ }
105
+
106
+ return imports
107
+ }
108
+
109
+ /**
110
+ * 按名字找函数声明
111
+ * @param sourceFile TS 语法树
112
+ * @param name 函数名
113
+ * @returns 函数节点或 null
114
+ */
115
+ export function findFunction(sourceFile, name) {
116
+ for (const statement of sourceFile.statements) {
117
+ if (!ts.isFunctionDeclaration(statement)) continue
118
+ if (statement.name?.text === name) return statement
119
+ }
120
+ return null
121
+ }
122
+
123
+ /**
124
+ * 中枢函数体违约:if 分支 / await / 直接调用 ctx.api
125
+ * @param hub 中枢函数节点
126
+ * @returns 违约描述列表
127
+ */
128
+ export function hubViolations(hub) {
129
+ const found = new Set()
130
+
131
+ const visit = (node) => {
132
+ if (ts.isIfStatement(node)) found.add('出现了 if 分支')
133
+ else if (ts.isAwaitExpression(node)) found.add('出现了 await')
134
+ else if (ts.isPropertyAccessExpression(node) && node.getText().startsWith('ctx.api')) {
135
+ found.add('直接调用了 ctx.api')
136
+ }
137
+ ts.forEachChild(node, visit)
138
+ }
139
+
140
+ if (hub.body) visit(hub.body)
141
+
142
+ return [...found]
143
+ }
@@ -0,0 +1,219 @@
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
+ }
@@ -0,0 +1,186 @@
1
+ // ============ 契约检查:中枢活在不在、导出名对不对、有没有变厚 ============
2
+ // 两种模块都要过:config 驱动的(标准 5 文件 + 动作挂载)和纯手写的(只有中枢契约)。
3
+ // 手写模块的视图目录名不一定等于模块名(如 chat → views/Home),所以不查页面模板。
4
+
5
+ import path from 'node:path'
6
+ import { ts } from '../utils/ts.mjs'
7
+ import { exists, listFiles, read } from '../utils/files.mjs'
8
+ import { findFunction, hubViolations } from './ast.mjs'
9
+ import { pascal } from '../generate/names.mjs'
10
+ import { relPath } from '../config/paths.mjs'
11
+
12
+ const PAGE_LABEL = { list: '列表页', detail: '详情页', form: '表单页' }
13
+
14
+ /**
15
+ * 契约检查:中枢是否存在、createXxxModule 是否存在、中枢是否违约(if / await / ctx.api)、
16
+ * config 模块另外检查页面模板与动作挂载。只做静态分析,不执行模块代码。
17
+ * @param modules 模块描述列表(source = 'config' | 'manual')
18
+ * @param root 项目根目录
19
+ * @param dirs 目录映射
20
+ * @returns 问题清单 [{ file, line, message }]
21
+ */
22
+ export function checkContracts(modules, root, dirs) {
23
+ const problems = []
24
+
25
+ for (const module of modules) checkModule(module, root, dirs, problems)
26
+
27
+ return problems
28
+ }
29
+
30
+ function checkModule(module, root, dirs, problems) {
31
+ const type = pascal(module.name)
32
+ const hubRel = relPath(dirs, 'domains', module.name, 'index.ts')
33
+ const hubFile = path.join(root, hubRel)
34
+ const manual = module.source === 'manual'
35
+
36
+ // 1. 中枢必须存在(sync 只补一次骨架,删了就是真没了)
37
+ if (!exists(hubFile)) {
38
+ problems.push({
39
+ file: hubRel,
40
+ message: `missing hub file ${hubRel}, run tha sync ${module.name} to scaffold it (少了模块入口,运行 sync 会补一个)`
41
+ })
42
+ return
43
+ }
44
+
45
+ // 2. 页面模板:只对 config 模块要求;按 pages 逐页检查
46
+ // (手写模块的视图目录名不必等于模块名,所以整体跳过)
47
+ if (!manual) {
48
+ const pages = module.domain.pages ?? ['list']
49
+ const pageFiles = { list: 'Index.vue', detail: 'Detail.vue', form: 'Form.vue' }
50
+
51
+ for (const page of pages) {
52
+ const viewRel = relPath(dirs, 'views', module.name, pageFiles[page] ?? 'Index.vue')
53
+ if (!exists(path.join(root, viewRel))) {
54
+ problems.push({
55
+ file: viewRel,
56
+ message: `missing ${page} page, run tha sync to generate it, or write it yourself then adopt (少了这个页面)`
57
+ })
58
+ }
59
+ }
60
+ }
61
+
62
+ const text = read(hubFile)
63
+ const expected = `create${type}Module`
64
+ const alias = `use${type}Module`
65
+ const sourceFile = ts.createSourceFile(hubFile, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
66
+
67
+ // 3. 装配入口必须存在:createXxxModule(生成器产出的装配函数)或 useXxxModule(单例访问)
68
+ const hub = findFunction(sourceFile, expected) ?? findFunction(sourceFile, alias)
69
+ if (!hub) {
70
+ problems.push({
71
+ file: hubRel,
72
+ message: `expected ${expected}() or ${alias}() in hub, the file may have been edited (模块入口里少了这两个函数)`
73
+ })
74
+ return
75
+ }
76
+
77
+ // 4. 中枢违约:只挂载,不判断、不 await、不调接口
78
+ for (const violation of hubViolations(hub)) {
79
+ problems.push({
80
+ file: hubRel,
81
+ message: `hub must only mount actions, found: ${violation} (模块入口出现了不该有的逻辑)`
82
+ })
83
+ }
84
+
85
+ // 5. 动作必须挂上(只对 config 模块:手写模块的动作由用例决定)
86
+ if (manual) return
87
+
88
+ // 6. 声明了权限码,执行器里就得真有判断(adopt 之后最容易丢的就是这一行)
89
+ checkPermissionGuards(module, root, dirs, problems)
90
+
91
+ const mounted = mountedActions(hub)
92
+ const crud = module.domain.crud ?? {}
93
+ const pages = module.domain.pages ?? ['list']
94
+
95
+ const required = ['refresh']
96
+ if (crud.create) required.push('create')
97
+ if (crud.update) required.push('update')
98
+ if (crud.remove) required.push('remove')
99
+ if (pages.includes('detail') || pages.includes('form')) required.push('load')
100
+
101
+ // 兼容旧骨架:save 同时承担新增与编辑
102
+ const legacySave = mounted.has('save')
103
+
104
+ for (const action of required) {
105
+ if (mounted.has(action)) continue
106
+ if (legacySave && (action === 'create' || action === 'update')) continue
107
+ problems.push({
108
+ file: hubRel,
109
+ message: `missing action ${action} in hub, mount it or drop the capability from config (入口没挂这个动作)`
110
+ })
111
+ }
112
+ }
113
+
114
+ /**
115
+ * 声明了权限码,执行器里就必须真有一处 can(...) 判断。
116
+ * adopt 之后生成物归人管,生成模板里的 ctx.can 可能被删掉 —— 这里是唯一还能发现的地方。
117
+ */
118
+ function checkPermissionGuards(module, root, dirs, problems) {
119
+ const codes = module.domain.permissions
120
+ if (!codes || Object.keys(codes).length === 0) return
121
+
122
+ const executorsRel = relPath(dirs, 'domains', module.name, 'executors')
123
+ const dir = path.join(root, executorsRel)
124
+ if (!exists(dir)) return
125
+
126
+ const files = listFiles(dir, '.ts')
127
+ if (files.length === 0) return
128
+
129
+ const hasGuard = files.some((file) => /\bcan\s*\(/.test(stripComments(read(path.join(dir, file)))))
130
+
131
+ if (hasGuard) return
132
+
133
+ problems.push({
134
+ file: executorsRel,
135
+ message:
136
+ 'permissions declared, but no ctx.can() anywhere in the action files (配了权限码却没做权限判断,等于没开权限)'
137
+ })
138
+ }
139
+
140
+ function stripComments(text) {
141
+ return text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '')
142
+ }
143
+
144
+ function unwrapExpr(node) {
145
+ let current = node
146
+
147
+ while (
148
+ ts.isParenthesizedExpression(current) ||
149
+ ts.isAsExpression(current) ||
150
+ ts.isTypeAssertionExpression(current) ||
151
+ (typeof ts.isSatisfiesExpression === 'function' && ts.isSatisfiesExpression(current)) ||
152
+ ts.isNonNullExpression(current)
153
+ ) {
154
+ current = current.expression
155
+ }
156
+
157
+ return current
158
+ }
159
+
160
+ /**
161
+ * 中枢 return 的对象字面量里挂了哪些动作名
162
+ */
163
+ function mountedActions(hub) {
164
+ const actions = new Set()
165
+ const body = hub.body
166
+ if (!body) return actions
167
+
168
+ for (const statement of body.statements) {
169
+ if (!ts.isReturnStatement(statement) || !statement.expression) continue
170
+
171
+ const expression = unwrapExpr(statement.expression)
172
+ if (!ts.isObjectLiteralExpression(expression)) continue
173
+
174
+ for (const property of expression.properties) {
175
+ if (
176
+ ts.isPropertyAssignment(property) ||
177
+ ts.isMethodDeclaration(property) ||
178
+ ts.isShorthandPropertyAssignment(property)
179
+ ) {
180
+ actions.add(property.name.getText().replace(/['"]/g, ''))
181
+ }
182
+ }
183
+ }
184
+
185
+ return actions
186
+ }