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.
- package/ARCHITECTURE.md +193 -0
- package/LICENSE +21 -0
- package/README.md +406 -2
- package/package.json +42 -4
- package/tha/cli.mjs +116 -0
- package/tha/commands/adopt.mjs +119 -0
- package/tha/commands/check.mjs +226 -0
- package/tha/commands/diff.mjs +70 -0
- package/tha/commands/init.mjs +262 -0
- package/tha/commands/remove.mjs +328 -0
- package/tha/commands/sync.mjs +826 -0
- package/tha/config/field.mjs +178 -0
- package/tha/config/load.mjs +188 -0
- package/tha/config/manifest.mjs +75 -0
- package/tha/config/paths.mjs +124 -0
- package/tha/config/project.mjs +52 -0
- package/tha/config/rules.mjs +85 -0
- package/tha/config/schema.mjs +306 -0
- package/tha/config/template.mjs +78 -0
- package/tha/generate/domain/api.mjs +67 -0
- package/tha/generate/domain/context.mjs +76 -0
- package/tha/generate/domain/detail.mjs +35 -0
- package/tha/generate/domain/entry.mjs +91 -0
- package/tha/generate/domain/executors.mjs +93 -0
- package/tha/generate/domain/form.mjs +34 -0
- package/tha/generate/domain/index.mjs +72 -0
- package/tha/generate/domain/permissions.mjs +35 -0
- package/tha/generate/domain/store.mjs +68 -0
- package/tha/generate/domain/view.mjs +29 -0
- package/tha/generate/global.mjs +111 -0
- package/tha/generate/header.mjs +29 -0
- package/tha/generate/names.mjs +45 -0
- package/tha/generate/plan.mjs +41 -0
- package/tha/kit/ThaCrudPage.vue +344 -0
- package/tha/kit/ThaDetail.vue +146 -0
- package/tha/kit/ThaForm.vue +194 -0
- package/tha/kit/request.ts +30 -0
- package/tha/kit/transport.ts +11 -0
- package/tha/utils/args.mjs +59 -0
- package/tha/utils/compare.mjs +38 -0
- package/tha/utils/files.mjs +175 -0
- package/tha/utils/format.mjs +23 -0
- package/tha/utils/output.mjs +79 -0
- package/tha/utils/style.mjs +147 -0
- package/tha/utils/ts.mjs +73 -0
- package/tha/verify/ast.mjs +143 -0
- package/tha/verify/boundary.mjs +219 -0
- package/tha/verify/contract.mjs +186 -0
- package/tha/verify/openapi.mjs +345 -0
- package/tha/verify/rules.mjs +91 -0
- package/tha/verify/scan.mjs +59 -0
- package/tha.mjs +22 -0
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# THA 架构设计
|
|
2
|
+
|
|
3
|
+
> 本文讲**为什么这么设计**:要解决什么、靠什么结构解决、代价是什么。
|
|
4
|
+
> 怎么安装、`config.ts` 怎么写、命令怎么用,见 [README.md](./README.md)。
|
|
5
|
+
> 每条约束的原话在 `tha/verify/rules.mjs`,本文引用的都是那里的 `basis`。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. 要解决的问题
|
|
10
|
+
|
|
11
|
+
前端业务模块的腐化不是某一处写错,而是**每一处单看都合理、累积起来就不可维护**。典型的五种症状:
|
|
12
|
+
|
|
13
|
+
| 症状 | 后果 |
|
|
14
|
+
|---|---|
|
|
15
|
+
| 页面直接调接口 | 接口一改,页面跟着改 |
|
|
16
|
+
| 状态层里发请求 | 状态与 IO 耦合在一起,无法独立测试 |
|
|
17
|
+
| 执行器互相 import | 修改一个用例,会影响多个相关模块 |
|
|
18
|
+
| 执行器绕开上下文自己拿依赖 | 依赖关系散落各处,换实现要全局搜 |
|
|
19
|
+
| 一个模块被创建多份 | 「私人大脑」,状态不同步 |
|
|
20
|
+
|
|
21
|
+
共同点是:code review 无法有效识别这类问题,只能依赖结构约束。因此 THA 不从「编码规范」入手,而是**先定义业务模块的固定结构**。
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 2. 一个模块的形状
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
view 表达意图,不碰实现
|
|
29
|
+
↓
|
|
30
|
+
hub 单例,只分发,不写逻辑
|
|
31
|
+
↓
|
|
32
|
+
executor 用例,依赖全部从 ctx 来
|
|
33
|
+
↓
|
|
34
|
+
step 被第二个用例复用时才下沉
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| 层 | 职责 | 不做什么 |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| view 页面 | 调中枢的动作、读状态 | 不调接口、不建实例、不碰执行器内部 |
|
|
40
|
+
| hub 中枢 | 模块级单例;把动作挂上去 | 不写业务逻辑,不 import `steps/` |
|
|
41
|
+
| executor 执行器 | 写用例,依赖全部走 ctx | 不 import 网络层/状态层/路由,不互相 import |
|
|
42
|
+
| step 零件 | 被多个执行器复用的函数 | 不预判未来,只有一个使用者时不下沉 |
|
|
43
|
+
| context | 提供 state / api / ui / run / can | 不写业务判断 |
|
|
44
|
+
|
|
45
|
+
形状固定之后,**「这段逻辑该放哪」就不再需要讨论**:有副作用的 IO 进执行器,被复用的纯步骤进步骤,页面只表达意图。
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 3. 四条结构决定
|
|
50
|
+
|
|
51
|
+
### 3.1 中枢只分发
|
|
52
|
+
|
|
53
|
+
> 「中枢只做分发:把意图交给执行器;禁止直接依赖执行器内部步骤」(HUB-01)
|
|
54
|
+
|
|
55
|
+
入口一旦开始写逻辑,就会长成上帝对象:先是一个 `if`,然后是一次请求,最后整个模块的业务都集中在 `index.ts` 里。所以中枢只做两件事:**单例缓存、挂载动作**。控制类逻辑(比如中断当前任务)也必须放进执行器,不允许写在入口。
|
|
56
|
+
|
|
57
|
+
### 3.2 执行器只认 ctx
|
|
58
|
+
|
|
59
|
+
> 「执行器只认 ctx:网络走 ctx.api,状态走 ctx.state」(EXE-02)
|
|
60
|
+
|
|
61
|
+
生成器的 `context.ts` 提供五样东西:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
interface Context {
|
|
65
|
+
state // 状态
|
|
66
|
+
api // 网络
|
|
67
|
+
ui // toast
|
|
68
|
+
run // 统一的 pending / error / finally 包装
|
|
69
|
+
can // 权限判定,入口和页面按钮共用
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
这是**最小集**,不够就往 `context.ts` 里加——加在上下文,不加在执行器里。好处是:换实现只改一处;写用例测试时注入一个模拟的 ctx 就能运行,无需启动 store 和网络。
|
|
74
|
+
|
|
75
|
+
### 3.3 执行器互不引用
|
|
76
|
+
|
|
77
|
+
> 「执行器之间互不引用:共用逻辑下沉成零件,用例顺序留在各自执行器里」(EXE-01)
|
|
78
|
+
|
|
79
|
+
两个用例要共用一段逻辑,正确做法是抽到 `steps/`,不是 A import B。执行器之间的横向引用会形成网状结构,而非树形结构——网状结构的改动影响范围无法预估。
|
|
80
|
+
|
|
81
|
+
### 3.4 零件按需下沉,不预判
|
|
82
|
+
|
|
83
|
+
> 「零件是「同一段步骤被第二个用例用到」才下沉的;只有一个使用者是过度设计」(STEP-01)
|
|
84
|
+
|
|
85
|
+
这是全套规则里唯一**反对抽象**的一条,而且是 warn 不是 error。它防的是「我先抽个工具函数,以后肯定用得上」——过度设计比重复更贵,因为重复看得见,过度设计看不见。
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 4. 依赖方向必须单向
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
view → hub → executor → step
|
|
93
|
+
↑
|
|
94
|
+
ctx 提供一切外部能力
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
配套两条边界:
|
|
98
|
+
|
|
99
|
+
- **状态层只存状态,不发请求**(STORE-01)。请求属于用例,状态只负责被写入。
|
|
100
|
+
- **传输层只上报不越权**(UTIL-01)。错误总线只管把错误抛上来,不决定怎么展示、不感知业务模块。
|
|
101
|
+
|
|
102
|
+
单向依赖的价值并非为了代码美观,而是**改动影响范围可预测**:改 `steps/` 只影响调用它的执行器,改 `utils/` 不影响任何业务判断。
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 5. 边界不靠人记,靠工具查
|
|
107
|
+
|
|
108
|
+
架构约束写在文档里,没人会读;写成可执行的检查,才有约束力。三个机制:
|
|
109
|
+
|
|
110
|
+
### 5.1 规则表与判断逻辑分离
|
|
111
|
+
|
|
112
|
+
`tha/verify/rules.mjs` 只保留规则的自然语言描述——`id` / `level` / `title` / `basis` / `fix`;
|
|
113
|
+
判断逻辑在 `tha/verify/boundary.mjs`。改文案不动逻辑,改逻辑不动文案。
|
|
114
|
+
|
|
115
|
+
### 5.2 每条规则带 `basis`
|
|
116
|
+
|
|
117
|
+
`basis` 直接引用架构原话。输出违规时,能回溯到「哪条约束被破了」,而不是「工具说不行」。
|
|
118
|
+
|
|
119
|
+
违规条目按这个形状输出:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
src/domains/doc/index.ts:1 HUB-03
|
|
123
|
+
问题:模块入口不是单例,被重复创建
|
|
124
|
+
没找到缓存的实例(每次调用都会新建一个)
|
|
125
|
+
应该:在模块顶层用 let 缓存:if (!module) module = createXxxModule()
|
|
126
|
+
依据:每个领域全局唯一实例,视图不能各造一个「私人大脑」
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 5.3 `import type` 一律放行
|
|
130
|
+
|
|
131
|
+
类型不构成运行时依赖。只查值导入,规则才不至于因噎废食。
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 6. 生成与检查分离
|
|
136
|
+
|
|
137
|
+
| | 负责什么 | 不负责什么 |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| 生成器(sync) | 标准形态:`config.ts` → 5 个文件 + 注册表 | 不生成业务逻辑 |
|
|
140
|
+
| 检查器(check) | 所有代码,包括完全手写的模块 | 不自动修改代码 |
|
|
141
|
+
|
|
142
|
+
`adopt` 的语义最能说明这个分工:**停止生成,但持续校验**。文件移交后,生成器不再写入,边界检查仍然执行。这样「生成」和「约束」就是两件可以独立演进的事——生成器可以不处理某个文件,但架构约束不能有盲区。
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 7. 配置面收窄
|
|
147
|
+
|
|
148
|
+
`config.ts` 顶层只有 9 个键,多写一个直接报错,当次一个文件都不生成。
|
|
149
|
+
|
|
150
|
+
逻辑:**配置项每多一个,生成器要处理的组合就翻一倍,边界就越难保障**。宁可让人在代码里写,也不让配置去表达——代码里的复杂度是局部的,配置里的复杂度是全局的。
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 8. 代价
|
|
155
|
+
|
|
156
|
+
说清楚取舍的另一面:
|
|
157
|
+
|
|
158
|
+
1. **只覆盖标准 CRUD**。工作流、嵌套表单、自定义布局仍然人写,工具只保证不覆盖、不误报。
|
|
159
|
+
2. **小模块显得重**。一个简单模块也要 5~6 个文件,抽象成本前置了。
|
|
160
|
+
3. **有学习曲线**。得先理解 hub / executor / step / ctx 四层语义才能顺畅写。
|
|
161
|
+
4. **原型阶段会变慢**。强约束要求先想清楚形状再动手。
|
|
162
|
+
5. **生成物是复制不是引用**。CLI 升级后,项目里已有的基线组件不会自动更新。
|
|
163
|
+
|
|
164
|
+
所以它的定位不是「通用代码生成器」,而是:**让一个人写业务的时候,不需要记架构**。约束由工具承担,人只负责业务本身。
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 9. 规则索引
|
|
169
|
+
|
|
170
|
+
| id | 级别 | 约束原话(`basis`) |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| HUB-01 | error | 中枢只做分发:把意图交给执行器;禁止直接依赖执行器内部步骤 |
|
|
173
|
+
| HUB-03 | warn | 每个领域全局唯一实例,视图不能各造一个「私人大脑」 |
|
|
174
|
+
| EXE-01 | error | 执行器之间互不引用:共用逻辑下沉成零件,用例顺序留在各自执行器里 |
|
|
175
|
+
| EXE-02 | error | 执行器只认 ctx:网络走 ctx.api,状态走 ctx.state |
|
|
176
|
+
| EXE-03 | error | 执行器只认 ctx:日志 / 时钟 / 流 / 鉴权走 ctx.ports,判定走 ctx.errors |
|
|
177
|
+
| STEP-01 | warn | 零件是「同一段步骤被第二个用例用到」才下沉的;只有一个使用者是过度设计 |
|
|
178
|
+
| VIEW-01 | error | 视图只表达意图:动作走域中枢,数据直读状态层 |
|
|
179
|
+
| VIEW-02 | error | 每个领域全局唯一实例:视图只能经 useXxxModule() 取,不能自己 create |
|
|
180
|
+
| STORE-01 | error | 状态层只存状态,不发请求 |
|
|
181
|
+
| UTIL-01 | error | 传输层只上报不越权:不感知业务模块,不做展示 |
|
|
182
|
+
|
|
183
|
+
规则真源:[`tha/verify/rules.mjs`](./tha/verify/rules.mjs) 判断逻辑:[`tha/verify/boundary.mjs`](./tha/verify/boundary.mjs)
|
|
184
|
+
|
|
185
|
+
`EXE-03` 提到的 `ctx.ports` / `ctx.errors` 是架构原话里的完整形态;生成器当前的 `context.ts` 只给最小集(`state` / `api` / `ui` / `run` / `can`),需要时按 3.2 往上下文里加。
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 10. 相关文档
|
|
190
|
+
|
|
191
|
+
- [README.md](./README.md)——安装、命令、`config.ts` 全量配置项、契约与退出码
|
|
192
|
+
- `tha/generate/domain/`——每一类生成物的模板,是「形状」的可执行版本
|
|
193
|
+
- `tha/verify/rules.mjs`——规则真源,改架构先改这里
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 温强
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,407 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tha-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
THA 架构的构建期工具。**生成器负责产出标准代码,检查器负责保障分层边界不被破坏。**
|
|
4
|
+
|
|
5
|
+
- **生成器**:读 `domains/<域>/config.ts`,生成标准文件(基线 5 个,按 `pages` 追加详情 / 表单)和全局注册表。
|
|
6
|
+
- **检查器**:没有 `config.ts` 的手写模块,不生成,只按层查边界。
|
|
7
|
+
- **自带基线**:`init` 自动补齐生成物所需的组件和工具,空项目也可直接使用。
|
|
8
|
+
|
|
9
|
+
设计思想、取舍与代价见 [ARCHITECTURE.md](./ARCHITECTURE.md);本文只讲怎么用。
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## THA 架构
|
|
14
|
+
|
|
15
|
+
分层,依赖只能向下:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
views 页面 只调中枢,不碰网络层和执行器内部
|
|
19
|
+
domains 领域 中枢(单例,只挂动作)→ 执行器(用例)→ 零件(被复用的函数)
|
|
20
|
+
stores 状态 只管状态和原子更新,不发请求
|
|
21
|
+
api 网络 请求和类型
|
|
22
|
+
utils 基础 纯函数、远端传输
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
一个领域只有一个入口:
|
|
26
|
+
|
|
27
|
+
- **中枢** `index.ts` 对外只暴露 `useXxxModule()`,模块级单例,重复创建即违规。
|
|
28
|
+
- **执行器** 写用例,依赖全部走 `context.ts` 注入,不直接 import 网络层、状态层、路由。
|
|
29
|
+
- **零件** 是被多个执行器复用的函数,只被一个执行器用时不算零件。
|
|
30
|
+
|
|
31
|
+
这套约束的目的很单一:业务逻辑集中维护,边界由工具保障,无需人工记忆。
|
|
32
|
+
|
|
33
|
+
为什么这么切分、每条约束的原话和代价,见 [ARCHITECTURE.md](./ARCHITECTURE.md)。
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## tha-cli 做什么
|
|
38
|
+
|
|
39
|
+
| 模式 | 输入 | 产物 |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| 生成器 | `config.ts` | 标准 CRUD 的 api / store / context / 执行器 / 页面 / 路由 / 菜单 |
|
|
42
|
+
| 检查器 | 任意源码 | 违规清单(10 条边界规则 + 契约比对) |
|
|
43
|
+
|
|
44
|
+
它不生成业务逻辑。`config.ts` 声明模块的字段与动作范围,工具据此生成标准代码;复杂逻辑由开发者编写,置于自定义执行器中,工具不覆盖。
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 安装
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm i -D tha-cli
|
|
52
|
+
npx tha-cli check
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
前置条件:
|
|
56
|
+
|
|
57
|
+
- **TypeScript 项目**(`.ts` / `.vue`)。纯 JS 项目不支持——模块靠 `config.ts` 识别,生成物也全是 TS。
|
|
58
|
+
- 不需要自己装 `typescript`。CLI 自带一份 5.x,只用于解析源码(读 `config.ts`、遍历语法结构),不参与你项目的构建——你项目里用 5.x 还是 7.x 都不影响它。
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 快速开始
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
tha init doc # 建 config 模板,不生成代码
|
|
66
|
+
# 编辑 src/domains/doc/config.ts
|
|
67
|
+
tha sync # 生成 5 个文件;中枢缺失时补一次骨架
|
|
68
|
+
tha check # 检查:配置 + 契约 + 边界检查 + 文件一致性
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
CI:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
tha check # 有 error 即非零退出
|
|
75
|
+
tha check --strict # 警告也算失败
|
|
76
|
+
tha check --json # 机器可消费
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 命令
|
|
82
|
+
|
|
83
|
+
| 命令 | 作用 |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `tha init <域>` | 建 config 模板 + 补齐基线(`--bare` 只写 config) |
|
|
86
|
+
| `tha sync [<域>]` | 生成 / 更新标准 CRUD 与注册表;补一次中枢骨架 |
|
|
87
|
+
| `tha check` | 只检查不写:配置 + 契约(响应与请求体)+ 边界检查 + 文件一致性 |
|
|
88
|
+
| `tha diff` | 显示将要发生的文件变化 |
|
|
89
|
+
| `tha remove <域>` | 删模块:生成物清掉,手写文件保留;保留的文件若还引用已删生成物会明确报出来 |
|
|
90
|
+
| `tha adopt <文件...>` | 这个文件以后你自己维护:不再自动生成,但分层规则照样查 |
|
|
91
|
+
| `tha adopt <文件...> --release` | 交回生成管理 |
|
|
92
|
+
|
|
93
|
+
参数按用途分组:
|
|
94
|
+
|
|
95
|
+
- 初始化:`--bare`(不补基线)、`--config`(写目录映射示例)
|
|
96
|
+
- 写盘:`--all`、`--force`、`--yes`(非交互确认;`adopt` 接受但忽略,它只改清单)、`--prune`(含清空目录)、`--dry-run`
|
|
97
|
+
- 检查:`--strict`、`--json`
|
|
98
|
+
- 定位:`--root`
|
|
99
|
+
- 交接:`--release`
|
|
100
|
+
|
|
101
|
+
非交互环境(脚本 / CI)不加 `--yes` 不会写盘。
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 一个标准域的文件
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
<domains>/<域>/
|
|
109
|
+
├── config.ts 手写:唯一真源
|
|
110
|
+
├── index.ts 手写:中枢(单例 + 只挂载动作;缺失时补骨架)
|
|
111
|
+
├── permissions.ts 按需生成:权限码 + canXxx 骨架(声明了权限码才有)
|
|
112
|
+
├── context.ts 生成:依赖注入 + 权限判定
|
|
113
|
+
├── executors/
|
|
114
|
+
│ ├── crud.generated.ts 生成:refresh / load / create / update / remove(按 crud 裁剪)
|
|
115
|
+
│ └── <用例>.ts 手写:自定义用例(可选)
|
|
116
|
+
└── steps/ 手写:被多个用例复用的零件(可选)
|
|
117
|
+
|
|
118
|
+
<api>/<域>.ts 生成:类型 + CRUD 接口(按 crud 裁剪)
|
|
119
|
+
<stores>/<域>/index.ts 生成:状态 + 原子更新(按 crud 裁剪)
|
|
120
|
+
<views>/<域>/Index.vue 生成:列表页
|
|
121
|
+
<views>/<域>/Detail.vue pages 含 detail 时生成
|
|
122
|
+
<views>/<域>/Form.vue pages 含 form 时生成
|
|
123
|
+
|
|
124
|
+
<router>/routes.generated.ts 全局生成
|
|
125
|
+
<layouts>/menu.generated.ts 全局生成
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`<domains>` / `<api>` / `<views>` 是目录映射的键,默认 `src/domains`、`src/api`、`src/views`(见「目录映射」)。
|
|
129
|
+
|
|
130
|
+
中枢骨架带单例,视图只调 `useXxxModule()`:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
let docModule: DocModule | null = null
|
|
134
|
+
|
|
135
|
+
export function useDocModule(): DocModule {
|
|
136
|
+
if (!docModule) docModule = createDocModule()
|
|
137
|
+
return docModule
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 内部工厂不导出:视图拿不到第二份
|
|
141
|
+
function createDocModule(ctx = createDocContext()): DocModule { ... }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 配置项:一个 config 的完整形状
|
|
147
|
+
|
|
148
|
+
`tha init doc` 生成的骨架只开最基本的:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
export const docConfig = {
|
|
152
|
+
name: 'doc', // 内部名:目录、action、变量名都从它推
|
|
153
|
+
title: '文档', // 显示名:菜单、页面标题
|
|
154
|
+
path: '/doc', // 浏览器地址
|
|
155
|
+
api: { base: '/docs' }, // 接口前缀
|
|
156
|
+
fields: {
|
|
157
|
+
title: { label: '标题', type: 'string', required: true, search: true }
|
|
158
|
+
},
|
|
159
|
+
pages: ['list', 'detail'],
|
|
160
|
+
list: {},
|
|
161
|
+
crud: {},
|
|
162
|
+
permissions: {}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
可选项都填上:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
export const docConfig = {
|
|
170
|
+
name: 'doc',
|
|
171
|
+
title: '文档',
|
|
172
|
+
path: '/doc',
|
|
173
|
+
|
|
174
|
+
api: {
|
|
175
|
+
base: '/docs',
|
|
176
|
+
contract: 'docs/openapi.json' // 填了才做字段比对
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
fields: {
|
|
180
|
+
title: { label: '标题', type: 'string', required: true, search: true },
|
|
181
|
+
summary: { label: '摘要', type: 'text' },
|
|
182
|
+
wordCount: { label: '字数', type: 'number', list: true, search: true },
|
|
183
|
+
published: { label: '已发布', type: 'boolean', default: false },
|
|
184
|
+
status: {
|
|
185
|
+
label: '状态',
|
|
186
|
+
type: 'enum',
|
|
187
|
+
default: 'draft',
|
|
188
|
+
search: true,
|
|
189
|
+
options: [
|
|
190
|
+
{ label: '草稿', value: 'draft', color: 'gray' },
|
|
191
|
+
{ label: '已发布', value: 'published', color: 'ok' },
|
|
192
|
+
{ label: '已下线', value: 'offline', color: 'warn' }
|
|
193
|
+
]
|
|
194
|
+
},
|
|
195
|
+
publishedAt: { label: '发布时间', type: 'datetime', list: true },
|
|
196
|
+
createdAt: { label: '创建日期', type: 'date', readonly: true }
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
pages: ['list', 'detail', 'form'],
|
|
200
|
+
list: { pageSize: 50, orderBy: 'createdAt', order: 'desc' },
|
|
201
|
+
crud: { create: true, update: true, remove: false },
|
|
202
|
+
permissions: { view: 'doc:view', create: 'doc:create', update: 'doc:update' }
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 顶层配置
|
|
207
|
+
|
|
208
|
+
| 键 | 必填 | 取值 | 默认 | 说明 |
|
|
209
|
+
|---|---|---|---|---|
|
|
210
|
+
| `name` | 是 | 小驼峰,如 `doc` / `userProfile` | — | 内部名,决定目录、action 名 |
|
|
211
|
+
| `title` | 是 | 非空字符串 | — | 显示名 |
|
|
212
|
+
| `path` | 是 | `/doc` 或 `/sys/doc`,不能带尾斜杠 | — | 页面地址 |
|
|
213
|
+
| `api.base` | 是 | `/docs`,不能带尾斜杠 | — | 接口前缀 |
|
|
214
|
+
| `api.contract` | 否 | 项目内 `.json` / `.yaml` / `.yml` 相对路径 | 无 | OpenAPI 文件,填了才比对字段 |
|
|
215
|
+
| `fields` | 是 | 对象,至少一个字段 | — | 字段声明 |
|
|
216
|
+
| `pages` | 否 | `list` / `detail` / `form` 的数组 | `['list','detail']` | 要生成哪几个页面 |
|
|
217
|
+
| `list.pageSize` | 否 | 1–100 的整数 | `20` | 每页条数 |
|
|
218
|
+
| `list.orderBy` | 否 | `fields` 里声明过的字段名 | 无 | 列表默认按这个字段排序 |
|
|
219
|
+
| `list.order` | 否 | `asc` / `desc` | `desc` | 与 `orderBy` 配合使用 |
|
|
220
|
+
| `crud` | 否 | `create` / `update` / `remove` 布尔 | 全 `true` | 关掉的动作整条链都不生成 |
|
|
221
|
+
| `permissions` | 否 | `view` / `create` / `update` / `remove` 权限码 | `{}` | 非空才生成 `permissions.ts` |
|
|
222
|
+
|
|
223
|
+
顶层**只有这 9 个键**,多写一个直接报「不认识的配置项」,一个文件都不会写。
|
|
224
|
+
|
|
225
|
+
### 字段配置
|
|
226
|
+
|
|
227
|
+
| 键 | 必填 | 取值 | 默认 | 说明 |
|
|
228
|
+
|---|---|---|---|---|
|
|
229
|
+
| `label` | 是 | 非空字符串 | — | 列表列头、表单项标签 |
|
|
230
|
+
| `type` | 是 | `string` / `text` / `number` / `boolean` / `enum` / `date` / `datetime` | — | `text` 是长文本 |
|
|
231
|
+
| `required` | 否 | 布尔 | `false` | 必填校验 + 契约比对用 |
|
|
232
|
+
| `list` | 否 | 布尔 | `true` | 是否出现在列表列(写 `false` 才关) |
|
|
233
|
+
| `form` | 否 | 布尔 | `true` | 是否出现在表单(写 `false` 才关) |
|
|
234
|
+
| `search` | 否 | 布尔 | `false` | 是否可搜索 |
|
|
235
|
+
| `readonly` | 否 | 布尔 | `false` | 只读:不出现在表单,只在详情/列表显示 |
|
|
236
|
+
| `options` | `enum` 时必填 | `[{ label, value, color }]` | — | `color` 取 `gray` / `ok` / `warn` / `red` / `blue` |
|
|
237
|
+
| `default` | 否 | 与类型匹配的值 | 无 | `enum` 的 `default` 必须在 `options` 的 value 里 |
|
|
238
|
+
|
|
239
|
+
`id` 由 CLI 生成,**不要在 `fields` 里写 `id`**。
|
|
240
|
+
|
|
241
|
+
### 会直接报错的写法
|
|
242
|
+
|
|
243
|
+
| 写法 | 报什么 |
|
|
244
|
+
|---|---|
|
|
245
|
+
| 顶层多一个键(如 `filters`、`templates`) | 不认识的配置项;该次 `sync` 一个文件都不写 |
|
|
246
|
+
| 字段名叫 `id`,或 `Title` 这种大写开头 | 字段名必须小写开头 |
|
|
247
|
+
| `readonly: true` 同时 `form: true` | 只读字段不能进表单 |
|
|
248
|
+
| `search: true` 用在 `text` / `boolean` | 只有 string / enum / number / date / datetime 可搜索 |
|
|
249
|
+
| `enum` 没写 `options`,或 `default` 不在 `options` 里 | 枚举字段必须给非空 options;default 必须在其中 |
|
|
250
|
+
| `pages` 里没有 `list`,或同一页写两遍 | list 必须保留(列表页是入口);重复的页面要删掉 |
|
|
251
|
+
| `pages` 含 `form` 但 `crud` 全关 | form 页需要 create 或 update 至少开一个 |
|
|
252
|
+
| `name: 'views'` 这种撞目录名的 | 不能使用保留名(api / stores / domains / views / router / layouts / components / utils) |
|
|
253
|
+
| `list.pageSize: 0` 或 `500` | 必须是 1–100 的整数 |
|
|
254
|
+
| `list.orderBy` 指向 `fields` 里没有的字段,或 enum / boolean | 排序字段必须已声明,且类型可排序 |
|
|
255
|
+
| 只写 `list.order` 不写 `orderBy`,或 `order` 不是 asc / desc | order 需与 orderBy 配合使用;仅支持 asc、desc |
|
|
256
|
+
|
|
257
|
+
### 常见组合
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
pages: ['list'] // 只要列表
|
|
261
|
+
crud: { create: false, update: false, remove: false } // 只读模块
|
|
262
|
+
pages: ['list', 'form'] // 不要详情,只在列表 + 表单页
|
|
263
|
+
permissions: { view: 'doc:view', update: 'doc:update' } // 只声明用得上的权限码
|
|
264
|
+
api: { base: '/docs', contract: 'docs/openapi.json' } // 接后端契约比对
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 目录映射:自定义源码目录
|
|
270
|
+
|
|
271
|
+
项目根建 `tha.config.json`:
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
{ "dirs": { "views": "src/pages", "stores": "src/model", "api": "src/services", "utils": "src/shared" } }
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
可用键:`domains` / `views` / `stores` / `api` / `router` / `layouts` / `components` / `utils`。
|
|
278
|
+
不写就用默认值;写错(绝对路径、`..`、未知键)直接报错,一个文件都不生成。
|
|
279
|
+
生成代码里的 import 会自动跟着改(`src/services` → `@/services`)。
|
|
280
|
+
|
|
281
|
+
`tha init <域> --config` 会写一份全默认值的示例,改完删掉即回默认。
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## 所有权:四种文件,四种处理策略
|
|
286
|
+
|
|
287
|
+
| 所有权 | 怎么识别 | sync | check |
|
|
288
|
+
|---|---|---|---|
|
|
289
|
+
| generated | 有 `@tha-generated` 生成头,校验值匹配 | 可以更新 | 照常查边界 |
|
|
290
|
+
| handwritten | 没有生成头,且不在交接清单 | 永不覆盖 | 提示,照常查边界 |
|
|
291
|
+
| adopted | 在 `.tha/manifest.json` 的 `manual` 里 | 永不覆盖、不校验文件一致性 | 不算失败,照常查边界 |
|
|
292
|
+
| edited | 有生成头但校验值不匹配 | 拒绝,除非 `--force` | 报告文件与生成器不一致 |
|
|
293
|
+
|
|
294
|
+
`adopt` 只接受构建期会生成的文件(用 `tha diff` 查看)。清单写在
|
|
295
|
+
`.tha/manifest.json`,建议提交到版本库。
|
|
296
|
+
|
|
297
|
+
**adopt 的语义**:工具不再生成该文件,但持续校验其边界合规性。
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## 护栏:10 条边界规则
|
|
302
|
+
|
|
303
|
+
规则唯一真源在 `tha/verify/rules.mjs`,每条带 `id` / `level` / `basis`(架构原话)/ `fix`。
|
|
304
|
+
|
|
305
|
+
| id | 级别 | 判据 |
|
|
306
|
+
|---|---|---|
|
|
307
|
+
| HUB-01 | error | 中枢直接 import `./steps/*` |
|
|
308
|
+
| HUB-03 | warn | 中枢不是模块级单例 |
|
|
309
|
+
| EXE-01 | error | 执行器之间互相 import |
|
|
310
|
+
| EXE-02 | error | 执行器 / 零件值导入网络层、状态层、路由 |
|
|
311
|
+
| EXE-03 | error | 执行器 / 零件值导入 `@/utils/*`(纯函数白名单除外) |
|
|
312
|
+
| STEP-01 | warn | 零件只被 0 或 1 个执行器使用 |
|
|
313
|
+
| VIEW-01 | error | 视图值导入网络层 / 传输层 / 执行器内部 |
|
|
314
|
+
| VIEW-02 | error | 视图直接 `createXxxModule()`,绕开单例 |
|
|
315
|
+
| STORE-01 | error | 状态层 import 网络层 |
|
|
316
|
+
| UTIL-01 | error | 基础设施 import UI 库或业务模块 |
|
|
317
|
+
|
|
318
|
+
约定:
|
|
319
|
+
|
|
320
|
+
- `import type` 一律放行(类型不构成运行时依赖)。
|
|
321
|
+
- 检查**按层扫**:`views/`、`stores/`、`utils/` 全局一遍,`domains/<域>/` 按模块。
|
|
322
|
+
- 手写模块靠目录形状识别:有 `index.ts` / `executors/` / `context.ts`,但没有 `config.ts`。
|
|
323
|
+
- `PURE_UTILS` 白名单在 `rules.mjs` 里声明,不再靠 eslint 注释豁免。
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## 契约与权限:声明即生效
|
|
328
|
+
|
|
329
|
+
这两项最容易出现「配置已声明、产物已生成,但对应功能未生效」的情况,因此检查口径需明确。
|
|
330
|
+
|
|
331
|
+
**字段契约**对两个方向都比(`api.contract` 指向的 OpenAPI,`.json` / `.yaml` / `.yml` 都认):
|
|
332
|
+
|
|
333
|
+
| 方向 | 取自 | 级别 |
|
|
334
|
+
|---|---|---|
|
|
335
|
+
| 响应:config 字段后端没有 | GET 200 / POST 201 的返回数据结构(`{code,data}` 包装自动解一层) | error |
|
|
336
|
+
| 响应:类型不一致 | 同上 | error |
|
|
337
|
+
| 响应:`format` / 必填 / 后端多字段 | 同上 | warn |
|
|
338
|
+
| 请求体:表单会提交的字段(`form !== false`)后端不收 | POST 请求体 → `create`;PUT / PATCH → `update` | error |
|
|
339
|
+
| 请求体:类型不一致 | 同上 | error |
|
|
340
|
+
| 请求体:后端必填、config 里没有可提交字段 | 同上 | error |
|
|
341
|
+
| 请求体:后端有可选字段 config 没声明 | 同上 | warn |
|
|
342
|
+
| 契约里没写请求体 | — | 一条 warn,跳过,不逐字段误报 |
|
|
343
|
+
|
|
344
|
+
**权限守卫**:`permissions` 非空的模块,执行器里必须至少有**一处** `can(...)` 判断(注释不算)。一处都没有 → error,提示「声明了 permissions,但执行器里没有任何 can(...) 判断」。
|
|
345
|
+
|
|
346
|
+
这条专门覆盖 `adopt` 场景:生成模板里本来每个动作前都有 `ctx.can('view')`,一旦这个文件交给手写,生成器不再管理它,只有这条检查还能发现「权限判断被移除」。
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## 退出码
|
|
351
|
+
|
|
352
|
+
文件与生成器不一致 / 手写冲突 / 契约失败(响应或请求体)/ 权限守卫缺失 / 检查器 error 任一存在 → `1`;
|
|
353
|
+
`--strict` 模式下契约与检查器的 warn 也视为失败。
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## 注意事项
|
|
358
|
+
|
|
359
|
+
实际使用中需注意以下事项:
|
|
360
|
+
|
|
361
|
+
1. **只覆盖标准 CRUD**。列表 / 详情 / 表单、增删改查、搜索分页是标准形态;工作流、嵌套表单、自定义布局这类复杂模块仍然人写,工具只保证不覆盖、不误报。
|
|
362
|
+
2. **自带组件是复制,不是引用**。`init` / `sync` 会把 `ThaCrudPage.vue` 等基线组件复制进项目。之后 CLI 升级,项目里那份**不会自动更新**:新能力(比如 `list.orderBy`)在老项目里不生效。要么删除该文件后重新执行 `sync`,要么手动将更新同步过去。
|
|
363
|
+
3. **`list.orderBy` 只排前端**。它是 `ThaCrudPage` 的本地排序,不会给后端接口加查询参数。要后端排序,那是接口契约的事。
|
|
364
|
+
4. **`api.contract` 的 YAML 解析依赖 `yaml` 包**,随 CLI 一起装,不需要单独安装。
|
|
365
|
+
5. **`config.ts` 顶层只有 9 个键**。多写一个直接报错,该次 `sync` 一个文件都不生成。收窄配置面是有意的,否则无法保障边界完整性。
|
|
366
|
+
6. **非交互不写盘**。脚本里不加 `--yes`,`sync` / `remove` 只打印计划。
|
|
367
|
+
7. **adopt 后工具不再生成该文件,但持续校验分层规则**。文件移交后,工具停止生成,边界检查仍然生效。
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## 开发
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
node test/guard.test.mjs # 造 10 种违规,断言 10/10 命中 + 退出码 1
|
|
375
|
+
node test/snapshot.test.mjs # 固定配置 → 与 test/snapshots/ 逐字节比对(--update 重记)
|
|
376
|
+
node test/openapi.test.mjs # 契约比对:方言、YAML、请求体方向
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
目录:
|
|
380
|
+
|
|
381
|
+
```
|
|
382
|
+
tha/
|
|
383
|
+
├── cli.mjs 入口分发
|
|
384
|
+
├── commands/ init / sync / check / diff / remove / adopt
|
|
385
|
+
├── kit/ 自带页面与基线(ThaCrudPage / ThaDetail / ThaForm / request / transport)
|
|
386
|
+
├── config/ paths(目录映射)、project(config + 手写模块)、manifest(交接清单)
|
|
387
|
+
├── generate/ plan + 模板(api、store、context、executors、entry、detail、form、permissions)
|
|
388
|
+
├── verify/ rules(规则真源)、ast、boundary、contract、openapi、scan
|
|
389
|
+
└── utils/ files / compare / output / style / format
|
|
390
|
+
test/ guard.test.mjs、snapshot.test.mjs、openapi.test.mjs、snapshots/
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## 发布
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
npm test # guard + snapshot + openapi 三套
|
|
399
|
+
npm pack --dry-run # 看会打进包里哪些文件
|
|
400
|
+
npm publish
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
包里只带 `tha.mjs` + `tha/` + `README.md` + `ARCHITECTURE.md`(`LICENSE` 由 npm 自动带上);`test/` 与 `snapshots/` 不进包,`typescript` 也不进包——它写在 `dependencies` 里,安装时由 npm 单独下载。
|
|
404
|
+
|
|
405
|
+
`typescript` 是普通依赖(`^5`),不参与你项目的构建。运行时先找项目里的那份(API 齐全才用),不齐就退回 CLI 自带的那份;两边都不能用时才报错,不会静默跳过检查。
|
|
406
|
+
|
|
407
|
+
包名 `tha-cli` 若在 npm 上已被占用,改 `package.json` 的 `name` 再发;命令名由 `bin` 决定,仍然是 `tha`。
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tha-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "THA 架构的构建期工具:产线生成标准增删改查,护栏守住分层边界。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "温强 <373601967@qq.com>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://gitee.com/JonnyWen6699/tha-cli.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://gitee.com/JonnyWen6699/tha-cli#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://gitee.com/JonnyWen6699/tha-cli/issues"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"tha": "tha.mjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"tha.mjs",
|
|
20
|
+
"tha",
|
|
21
|
+
"README.md",
|
|
22
|
+
"ARCHITECTURE.md"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.17"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "node test/guard.test.mjs && node test/snapshot.test.mjs && node test/openapi.test.mjs",
|
|
29
|
+
"snapshot": "node test/snapshot.test.mjs --update"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"tha",
|
|
33
|
+
"codegen",
|
|
34
|
+
"architecture",
|
|
35
|
+
"boundary",
|
|
36
|
+
"vue",
|
|
37
|
+
"cli"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"typescript": "^5",
|
|
42
|
+
"yaml": "^2.9.1"
|
|
43
|
+
}
|
|
44
|
+
}
|