tha-cli 0.1.2 → 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 +58 -33
- package/README.md +40 -26
- 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
package/ARCHITECTURE.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# THA 架构设计
|
|
2
2
|
|
|
3
3
|
> 本文讲**为什么这么设计**:要解决什么、靠什么结构解决、代价是什么。
|
|
4
|
-
> 怎么安装、`config.ts` 怎么写、命令怎么用,见
|
|
5
|
-
>
|
|
4
|
+
> 怎么安装、`config.ts` 怎么写、命令怎么用,见 README.md。
|
|
5
|
+
> 每条边界规则的实现见 `tha/eslint-rules/`,本文引用的都是规则的设计依据。
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -103,30 +103,53 @@ view → hub → executor → step
|
|
|
103
103
|
|
|
104
104
|
---
|
|
105
105
|
|
|
106
|
-
## 5.
|
|
106
|
+
## 5. 边界不靠人记,靠 ESLint 实时查
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
架构约束写在文档里,没人会读;写成可执行的检查,才有约束力。而且,**事后检查不如实时检查**——写错的那一刻编辑器就有红线,比提交前跑一遍 CLI 有效得多。
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
因此边界检查全部交给 ESLint,CLI 只负责自动装和写配置。
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
判断逻辑在 `tha/verify/boundary.mjs`。改文案不动逻辑,改逻辑不动文案。
|
|
112
|
+
### 5.1 为什么选 ESLint,不自己写检查器
|
|
114
113
|
|
|
115
|
-
|
|
114
|
+
| 方案 | 优点 | 缺点 |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| 自己写检查器 | 依赖少,完全可控 | 编辑器要自己写插件,VSCode / WebStorm 都得写一套;热更新要自己做 |
|
|
117
|
+
| ESLint | 编辑器原生支持(VSCode / WebStorm 都有扩展),实时红线;生态成熟 | 依赖包多,要手动装 |
|
|
116
118
|
|
|
117
|
-
|
|
119
|
+
选 ESLint 的原因:**不干扰开发者现有习惯**。开发者已经在用 ESLint,不需要再学新工具;VSCode / WebStorm 的 ESLint 扩展已经装好,打开项目就生效。
|
|
118
120
|
|
|
119
|
-
|
|
121
|
+
代价是依赖包多,但这个问题由 `tha init` 自动解决——一行命令装完所有依赖、写好配置,开发者不用动手。
|
|
120
122
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
### 5.2 8 条边界规则
|
|
124
|
+
|
|
125
|
+
每条规则是一个独立的 ESLint 插件文件,放在 `tha/eslint-rules/` 目录下:
|
|
126
|
+
|
|
127
|
+
| 规则 ID | 层级 | 检查内容 |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `tha/hub-no-import-steps` | HUB-01 | 中枢不许 import 执行器内部步骤 |
|
|
130
|
+
| `tha/executor-no-cross-import` | EXE-01 | 执行器之间不许互相 import |
|
|
131
|
+
| `tha/executor-no-import-api` | EXE-02 | 执行器不许 import 网络层、状态层、路由 |
|
|
132
|
+
| `tha/executor-no-import-utils` | EXE-03 | 执行器不许 import 传输工具,必须经 ctx 注入 |
|
|
133
|
+
| `tha/view-no-import-api` | VIEW-01 | 视图不许直接 import api 层、传输工具、执行器内部 |
|
|
134
|
+
| `tha/view-no-create-module` | VIEW-02 | 视图不许自己创建中枢实例 |
|
|
135
|
+
| `tha/store-no-import-api` | STORE-01 | 状态层不许发请求 |
|
|
136
|
+
| `tha/util-no-import-ui` | UTIL-01 | 基础工具不许 import UI 组件 |
|
|
137
|
+
|
|
138
|
+
规则只检查单文件内的 import 语句,不做跨文件分析——ESLint 擅长这个,跨文件规则交给 `tha check` 做。
|
|
128
139
|
|
|
129
|
-
### 5.3 `
|
|
140
|
+
### 5.3 `tha init` 自动完成一切
|
|
141
|
+
|
|
142
|
+
`tha init` 做五件事:
|
|
143
|
+
|
|
144
|
+
1. **检测环境**:项目里有没有 ESLint、有没有 Vue
|
|
145
|
+
2. **自动装依赖**:缺什么装什么(eslint、@typescript-eslint、eslint-plugin-vue)
|
|
146
|
+
3. **自动写配置**:生成 `eslint.config.js`,把 8 条规则写进去
|
|
147
|
+
4. **自动跑 sync**:生成标准代码
|
|
148
|
+
5. **自动改 dev script**:把 `tha watch` 加进 `npm run dev`
|
|
149
|
+
|
|
150
|
+
开发者不需要手动装任何包、不需要手动写任何配置。一行 `tha init` 之后,编辑器里就有实时红线了。
|
|
151
|
+
|
|
152
|
+
### 5.4 `import type` 一律放行
|
|
130
153
|
|
|
131
154
|
类型不构成运行时依赖。只查值导入,规则才不至于因噎废食。
|
|
132
155
|
|
|
@@ -167,20 +190,22 @@ src/domains/doc/index.ts:1 HUB-03
|
|
|
167
190
|
|
|
168
191
|
## 9. 规则索引
|
|
169
192
|
|
|
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
|
-
|
|
193
|
+
| id | 级别 | 约束原话(`basis`) | 实现位置 |
|
|
194
|
+
|---|---|---|---|
|
|
195
|
+
| HUB-01 | error | 中枢只做分发:把意图交给执行器;禁止直接依赖执行器内部步骤 | ESLint |
|
|
196
|
+
| HUB-03 | warn | 每个领域全局唯一实例,视图不能各造一个「私人大脑」 | 待实现 |
|
|
197
|
+
| EXE-01 | error | 执行器之间互不引用:共用逻辑下沉成零件,用例顺序留在各自执行器里 | ESLint |
|
|
198
|
+
| EXE-02 | error | 执行器只认 ctx:网络走 ctx.api,状态走 ctx.state | ESLint |
|
|
199
|
+
| EXE-03 | error | 执行器只认 ctx:日志 / 时钟 / 流 / 鉴权走 ctx.ports,判定走 ctx.errors | ESLint |
|
|
200
|
+
| STEP-01 | warn | 零件是「同一段步骤被第二个用例用到」才下沉的;只有一个使用者是过度设计 | 待实现 |
|
|
201
|
+
| VIEW-01 | error | 视图只表达意图:动作走域中枢,数据直读状态层 | ESLint |
|
|
202
|
+
| VIEW-02 | error | 每个领域全局唯一实例:视图只能经 useXxxModule() 取,不能自己 create | ESLint |
|
|
203
|
+
| STORE-01 | error | 状态层只存状态,不发请求 | ESLint |
|
|
204
|
+
| UTIL-01 | error | 传输层只上报不越权:不感知业务模块,不做展示 | ESLint |
|
|
205
|
+
|
|
206
|
+
ESLint 规则实现:`tha/eslint-rules/` 配置自动生成:`tha/utils/eslint.mjs`
|
|
207
|
+
|
|
208
|
+
`HUB-03` 和 `STEP-01` 是跨文件规则,ESLint 单文件检查不好做,后续版本会补回 `tha check` 里。
|
|
184
209
|
|
|
185
210
|
`EXE-03` 提到的 `ctx.ports` / `ctx.errors` 是架构原话里的完整形态;生成器当前的 `context.ts` 只给最小集(`state` / `api` / `ui` / `run` / `can`),需要时按 3.2 往上下文里加。
|
|
186
211
|
|
|
@@ -188,6 +213,6 @@ src/domains/doc/index.ts:1 HUB-03
|
|
|
188
213
|
|
|
189
214
|
## 10. 相关文档
|
|
190
215
|
|
|
191
|
-
-
|
|
216
|
+
- README.md——安装、命令、`config.ts` 全量配置项、契约与退出码
|
|
192
217
|
- `tha/generate/domain/`——每一类生成物的模板,是「形状」的可执行版本
|
|
193
218
|
- `tha/verify/rules.mjs`——规则真源,改架构先改这里
|
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# tha-cli
|
|
2
2
|
|
|
3
|
-
THA
|
|
3
|
+
THA 架构的构建期工具。**一行 init 完成全部配置:自动装依赖、自动写 ESLint 规则、自动生成代码、自动接入开发流程。**
|
|
4
4
|
|
|
5
5
|
- **生成器**:读 `domains/<域>/config.ts`,生成标准文件(基线 5 个,按 `pages` 追加详情 / 表单)和全局注册表。
|
|
6
|
-
-
|
|
6
|
+
- **边界检查**:内置 8 条 ESLint 规则,`init` 自动安装 ESLint 并写好配置,编辑器里实时红线提示。
|
|
7
|
+
- **热更新**:`tha watch` 监听配置文件变化,改完自动跑 sync,无需手动执行命令。
|
|
7
8
|
- **自带基线**:`init` 自动补齐生成物所需的组件和工具,空项目也可直接使用。
|
|
8
9
|
|
|
9
|
-
设计思想、取舍与代价见
|
|
10
|
+
设计思想、取舍与代价见 ARCHITECTURE.md;本文只讲怎么用。
|
|
10
11
|
|
|
11
12
|
---
|
|
12
13
|
|
|
@@ -30,7 +31,7 @@ utils 基础 纯函数、远端传输
|
|
|
30
31
|
|
|
31
32
|
这套约束的目的很单一:业务逻辑集中维护,边界由工具保障,无需人工记忆。
|
|
32
33
|
|
|
33
|
-
为什么这么切分、每条约束的原话和代价,见
|
|
34
|
+
为什么这么切分、每条约束的原话和代价,见 ARCHITECTURE.md。
|
|
34
35
|
|
|
35
36
|
---
|
|
36
37
|
|
|
@@ -39,7 +40,8 @@ utils 基础 纯函数、远端传输
|
|
|
39
40
|
| 模式 | 输入 | 产物 |
|
|
40
41
|
|---|---|---|
|
|
41
42
|
| 生成器 | `config.ts` | 标准 CRUD 的 api / store / context / 执行器 / 页面 / 路由 / 菜单 |
|
|
42
|
-
|
|
|
43
|
+
| ESLint 规则 | 内置 8 条边界规则 | `init` 自动写进 `eslint.config.js`,编辑器实时检查 |
|
|
44
|
+
| 配置检查 | 项目源码 | 契约比对 + 文件一致性 + ESLint 配置完整性 |
|
|
43
45
|
|
|
44
46
|
它不生成业务逻辑。`config.ts` 声明模块的字段与动作范围,工具据此生成标准代码;复杂逻辑由开发者编写,置于自定义执行器中,工具不覆盖。
|
|
45
47
|
|
|
@@ -62,12 +64,21 @@ npx tha-cli check
|
|
|
62
64
|
## 快速开始
|
|
63
65
|
|
|
64
66
|
```bash
|
|
65
|
-
tha init doc #
|
|
67
|
+
tha init doc # 自动装 ESLint + 写配置 + 生成代码 + 改 dev script
|
|
66
68
|
# 编辑 src/domains/doc/config.ts
|
|
67
|
-
tha
|
|
68
|
-
tha check # 检查:配置 + 契约 + 边界检查 + 文件一致性
|
|
69
|
+
# 保存后 tha watch 会自动跑 sync,无需手动执行
|
|
69
70
|
```
|
|
70
71
|
|
|
72
|
+
一行 `tha init` 完成全部初始化:
|
|
73
|
+
|
|
74
|
+
1. 检测项目环境(有没有 ESLint、有没有 Vue)
|
|
75
|
+
2. 自动安装所需依赖(eslint、@typescript-eslint、eslint-plugin-vue)
|
|
76
|
+
3. 自动写 `eslint.config.js`,内置 8 条边界规则
|
|
77
|
+
4. 自动生成标准代码
|
|
78
|
+
5. 自动改 `package.json` 的 dev script,把 `tha watch` 加进去
|
|
79
|
+
|
|
80
|
+
编辑器打开项目后,VSCode / WebStorm 的 ESLint 扩展会自动识别配置,写错 import 立刻有红线提示。
|
|
81
|
+
|
|
71
82
|
CI:
|
|
72
83
|
|
|
73
84
|
```bash
|
|
@@ -82,9 +93,10 @@ tha check --json # 机器可消费
|
|
|
82
93
|
|
|
83
94
|
| 命令 | 作用 |
|
|
84
95
|
|---|---|
|
|
85
|
-
| `tha init <域>` |
|
|
96
|
+
| `tha init <域>` | 一键初始化:自动装 ESLint + 写配置 + 生成代码 + 改 dev script |
|
|
86
97
|
| `tha sync [<域>]` | 生成 / 更新标准 CRUD 与注册表;补一次中枢骨架 |
|
|
87
|
-
| `tha
|
|
98
|
+
| `tha watch` | 监听 `domains/*/config.ts` 变化,自动跑 sync(debounce 500ms) |
|
|
99
|
+
| `tha check` | 只检查不写:配置 + 契约(响应与请求体)+ 文件一致性 + ESLint 配置完整性 |
|
|
88
100
|
| `tha diff` | 显示将要发生的文件变化 |
|
|
89
101
|
| `tha remove <域>` | 删模块:生成物清掉,手写文件保留;保留的文件若还引用已删生成物会明确报出来 |
|
|
90
102
|
| `tha adopt <文件...>` | 这个文件以后你自己维护:不再自动生成,但分层规则照样查 |
|
|
@@ -298,29 +310,31 @@ api: { base: '/docs', contract: 'docs/openapi.json' } // 接后端契约比对
|
|
|
298
310
|
|
|
299
311
|
---
|
|
300
312
|
|
|
301
|
-
##
|
|
313
|
+
## 边界检查:ESLint 实时红线
|
|
302
314
|
|
|
303
|
-
|
|
315
|
+
边界检查全部交给 ESLint,`tha init` 自动完成全部配置。编辑器打开项目后,写错 import 立刻有红线提示,不需要等提交前跑 CLI。
|
|
304
316
|
|
|
305
|
-
|
|
317
|
+
8 条单文件规则,每条对应一个 ESLint 插件文件:
|
|
318
|
+
|
|
319
|
+
| 规则 | 级别 | 检查内容 |
|
|
306
320
|
|---|---|---|
|
|
307
|
-
|
|
|
308
|
-
|
|
|
309
|
-
|
|
|
310
|
-
|
|
|
311
|
-
|
|
|
312
|
-
|
|
|
313
|
-
|
|
|
314
|
-
|
|
|
315
|
-
| STORE-01 | error | 状态层 import 网络层 |
|
|
316
|
-
| UTIL-01 | error | 基础设施 import UI 库或业务模块 |
|
|
321
|
+
| `tha/hub-no-import-steps` | error | 中枢直接 import `./steps/*` |
|
|
322
|
+
| `tha/executor-no-cross-import` | error | 执行器之间互相 import |
|
|
323
|
+
| `tha/executor-no-import-api` | error | 执行器值导入网络层、状态层、路由 |
|
|
324
|
+
| `tha/executor-no-import-utils` | error | 执行器值导入 `@/utils/*`(纯函数白名单除外) |
|
|
325
|
+
| `tha/view-no-import-api` | error | 视图值导入网络层 / 传输层 / 执行器内部 |
|
|
326
|
+
| `tha/view-no-create-module` | error | 视图直接 `createXxxModule()`,绕开单例 |
|
|
327
|
+
| `tha/store-no-import-api` | error | 状态层 import 网络层 |
|
|
328
|
+
| `tha/util-no-import-ui` | error | 基础设施 import UI 库或业务模块 |
|
|
317
329
|
|
|
318
330
|
约定:
|
|
319
331
|
|
|
320
332
|
- `import type` 一律放行(类型不构成运行时依赖)。
|
|
321
|
-
-
|
|
322
|
-
-
|
|
323
|
-
-
|
|
333
|
+
- 规则实现:`tha/eslint-rules/` 目录下,每条规则一个文件。
|
|
334
|
+
- 配置自动生成:`tha init` 检测项目环境,自动装依赖、自动写 `eslint.config.js`。
|
|
335
|
+
- VSCode / WebStorm 的 ESLint 扩展原生支持,不需要额外装插件。
|
|
336
|
+
|
|
337
|
+
跨文件规则(中枢单例检查、零件复用数检查)是 warn 级别,后续版本会补回 `tha check` 命令里。
|
|
324
338
|
|
|
325
339
|
---
|
|
326
340
|
|
package/package.json
CHANGED
package/tha/cli.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { runDiff } from './commands/diff.mjs'
|
|
|
6
6
|
import { runSync } from './commands/sync.mjs'
|
|
7
7
|
import { runRemove } from './commands/remove.mjs'
|
|
8
8
|
import { runAdopt } from './commands/adopt.mjs'
|
|
9
|
+
import { runWatch } from './commands/watch.mjs'
|
|
9
10
|
import { configureStyle, ui } from './utils/style.mjs'
|
|
10
11
|
|
|
11
12
|
const COMMANDS = {
|
|
@@ -13,6 +14,7 @@ const COMMANDS = {
|
|
|
13
14
|
check: runCheck,
|
|
14
15
|
diff: runDiff,
|
|
15
16
|
sync: runSync,
|
|
17
|
+
watch: runWatch,
|
|
16
18
|
remove: runRemove,
|
|
17
19
|
adopt: runAdopt
|
|
18
20
|
}
|
package/tha/commands/check.mjs
CHANGED
|
@@ -4,10 +4,10 @@ import { buildPlan } from '../generate/plan.mjs'
|
|
|
4
4
|
import { comparePlan } from '../utils/compare.mjs'
|
|
5
5
|
import { parseArgs } from '../utils/args.mjs'
|
|
6
6
|
import { printErrors, printHeader } from '../utils/output.mjs'
|
|
7
|
-
import { checkBoundaries } from '../verify/boundary.mjs'
|
|
8
7
|
import { checkContracts } from '../verify/contract.mjs'
|
|
9
8
|
import { checkOpenApiContracts } from '../verify/openapi.mjs'
|
|
10
9
|
import { describeConfigured } from '../verify/scan.mjs'
|
|
10
|
+
import { checkEslintConfig } from '../utils/eslint.mjs'
|
|
11
11
|
import { glyph, ui } from '../utils/style.mjs'
|
|
12
12
|
|
|
13
13
|
export function runCheck({ root, args }) {
|
|
@@ -49,17 +49,19 @@ export function runCheck({ root, args }) {
|
|
|
49
49
|
]
|
|
50
50
|
const contractErrors = contracts.filter((item) => (item.level ?? 'error') === 'error')
|
|
51
51
|
const contractWarns = contracts.filter((item) => item.level === 'warn')
|
|
52
|
-
const guards = checkBoundaries(modules, root, project.dirs)
|
|
53
|
-
const guardErrors = guards.filter((item) => item.level === 'error')
|
|
54
|
-
const guardWarns = guards.filter((item) => item.level === 'warn')
|
|
55
52
|
|
|
56
|
-
//
|
|
53
|
+
// ESLint 配置完整性检查
|
|
54
|
+
const eslintProblems = checkEslintConfig(root)
|
|
55
|
+
const eslintErrors = eslintProblems.filter((item) => item.level === 'error')
|
|
56
|
+
const eslintWarns = eslintProblems.filter((item) => item.level === 'warn')
|
|
57
|
+
|
|
58
|
+
// 契约违约、漂移、手改、ESLint 配置错误,都让 CI 红;warn 只在 --strict 时红
|
|
57
59
|
const failed =
|
|
58
60
|
drift.length > 0 ||
|
|
59
61
|
blocked > 0 ||
|
|
60
62
|
contractErrors.length > 0 ||
|
|
61
|
-
|
|
62
|
-
(flags.strict && (contractWarns.length > 0 ||
|
|
63
|
+
eslintErrors.length > 0 ||
|
|
64
|
+
(flags.strict && (contractWarns.length > 0 || eslintWarns.length > 0))
|
|
63
65
|
|
|
64
66
|
if (failed) process.exitCode = 1
|
|
65
67
|
|
|
@@ -118,21 +120,18 @@ export function runCheck({ root, args }) {
|
|
|
118
120
|
}
|
|
119
121
|
}
|
|
120
122
|
|
|
121
|
-
if (
|
|
123
|
+
if (eslintProblems.length > 0) {
|
|
122
124
|
console.log('')
|
|
123
125
|
console.log(
|
|
124
|
-
` ${ui.
|
|
125
|
-
|
|
126
|
-
`
|
|
126
|
+
` ${ui.warning(glyph.warn)} ${ui.label(
|
|
127
|
+
`ESLint 配置:${eslintErrors.length} 处错误、${eslintWarns.length} 处提醒`,
|
|
128
|
+
`ESLint config: ${eslintErrors.length} errors, ${eslintWarns.length} warnings`
|
|
127
129
|
)}`
|
|
128
130
|
)
|
|
129
|
-
for (const item of
|
|
130
|
-
const badge = item.level === 'error' ? ui.danger(
|
|
131
|
-
console.log(` ${ui.path(
|
|
132
|
-
console.log(`
|
|
133
|
-
if (item.note) console.log(` ${ui.muted(item.note)}`)
|
|
134
|
-
console.log(` 应该:${item.fix}`)
|
|
135
|
-
console.log(` ${ui.muted(`依据:${item.basis}`)}`)
|
|
131
|
+
for (const item of eslintProblems) {
|
|
132
|
+
const badge = item.level === 'error' ? ui.danger(glyph.err) : ui.warning(glyph.warn)
|
|
133
|
+
console.log(` ${badge} ${ui.path(item.file)}`)
|
|
134
|
+
console.log(` ${ui.muted(item.message)}`)
|
|
136
135
|
}
|
|
137
136
|
}
|
|
138
137
|
|
package/tha/commands/init.mjs
CHANGED
|
@@ -9,6 +9,8 @@ import { parseArgs } from '../utils/args.mjs'
|
|
|
9
9
|
import { absolute, exists, read, writeAtomic } from '../utils/files.mjs'
|
|
10
10
|
import { glyph, ui } from '../utils/style.mjs'
|
|
11
11
|
import { printNextSteps } from '../utils/output.mjs'
|
|
12
|
+
import { detectEslintEnv, installEslintDeps, writeEslintConfig, verifyEslintRules } from '../utils/eslint.mjs'
|
|
13
|
+
import { runSync } from './sync.mjs'
|
|
12
14
|
|
|
13
15
|
// 生成物会 import 的基线文件:空项目里没有,init 时补上(不覆盖已有文件)
|
|
14
16
|
const BASELINE = [
|
|
@@ -145,6 +147,48 @@ export async function runInit({ root, args }) {
|
|
|
145
147
|
const baseline = flags.bare ? { created: [], skipped: [] } : ensureBaseline(root, dirs)
|
|
146
148
|
const dirsConfig = flags.config ? ensureDirsConfig(root) : null
|
|
147
149
|
|
|
150
|
+
// ESLint 自动配置:检测 → 装依赖 → 写配置 → 验证
|
|
151
|
+
let eslintResult = null
|
|
152
|
+
if (!flags.bare && !flags['dry-run']) {
|
|
153
|
+
const env = detectEslintEnv(root)
|
|
154
|
+
let installed = []
|
|
155
|
+
let installFailed = []
|
|
156
|
+
|
|
157
|
+
if (!env.hasEslint) {
|
|
158
|
+
console.log('')
|
|
159
|
+
console.log(` ${ui.label('检测到项目没有 ESLint,自动安装...', 'Installing ESLint...')}`)
|
|
160
|
+
const result = installEslintDeps(root, env)
|
|
161
|
+
installed = result.installed
|
|
162
|
+
installFailed = result.failed
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const configResult = writeEslintConfig(root, env)
|
|
166
|
+
const verifyResult = env.hasEslint || installed.length > 0
|
|
167
|
+
? verifyEslintRules(root)
|
|
168
|
+
: { ok: false, message: '' }
|
|
169
|
+
|
|
170
|
+
eslintResult = { env, installed, installFailed, configResult, verifyResult }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 自动跑一次 sync,直接生成全套文件
|
|
174
|
+
let syncResult = null
|
|
175
|
+
if (!flags.bare && !flags['dry-run'] && !flags.json) {
|
|
176
|
+
console.log('')
|
|
177
|
+
console.log(` ${ui.label('自动生成代码...', 'Generating code...')}`)
|
|
178
|
+
try {
|
|
179
|
+
await runSync({ root, args: [name, '--yes'] })
|
|
180
|
+
syncResult = { ok: true }
|
|
181
|
+
} catch (error) {
|
|
182
|
+
syncResult = { ok: false, message: error.message }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 自动改 package.json 的 dev script,加上 tha watch
|
|
187
|
+
let packageJsonResult = null
|
|
188
|
+
if (!flags.bare && !flags['dry-run']) {
|
|
189
|
+
packageJsonResult = updateDevScript(root)
|
|
190
|
+
}
|
|
191
|
+
|
|
148
192
|
if (flags.json) {
|
|
149
193
|
console.log(
|
|
150
194
|
JSON.stringify(
|
|
@@ -179,6 +223,46 @@ export async function runInit({ root, args }) {
|
|
|
179
223
|
console.log(` ${ui.muted(`目录映射示例已写入 ${dirsConfig},改完删掉即回默认`)}`)
|
|
180
224
|
}
|
|
181
225
|
|
|
226
|
+
// ESLint 配置结果
|
|
227
|
+
if (eslintResult) {
|
|
228
|
+
console.log('')
|
|
229
|
+
if (eslintResult.installed.length > 0) {
|
|
230
|
+
console.log(` ${ui.success(glyph.ok)} ${ui.label('ESLint 依赖已安装', 'ESLint installed')}`)
|
|
231
|
+
console.log(` ${ui.muted(eslintResult.installed.join(', '))}`)
|
|
232
|
+
}
|
|
233
|
+
if (eslintResult.installFailed.length > 0) {
|
|
234
|
+
console.log(` ${ui.warning(glyph.warn)} ${ui.label('ESLint 部分依赖安装失败', 'Some ESLint deps failed')}`)
|
|
235
|
+
console.log(` ${ui.muted('请手动安装:npm i -D ' + eslintResult.installFailed.join(' '))}`)
|
|
236
|
+
}
|
|
237
|
+
if (eslintResult.configResult.written) {
|
|
238
|
+
console.log(` ${ui.success(glyph.ok)} ${ui.label('ESLint 配置已生成', 'ESLint config created')}`)
|
|
239
|
+
console.log(` ${ui.path(eslintResult.configResult.written)}`)
|
|
240
|
+
}
|
|
241
|
+
if (eslintResult.configResult.skipped) {
|
|
242
|
+
console.log(` ${ui.muted('已有 ESLint 配置,未覆盖(可手动加 tha 规则)')}`)
|
|
243
|
+
}
|
|
244
|
+
if (eslintResult.verifyResult.ok) {
|
|
245
|
+
console.log(` ${ui.success(glyph.ok)} ${ui.label('THA 规则已生效', 'THA rules active')}`)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// sync 结果
|
|
250
|
+
if (syncResult) {
|
|
251
|
+
console.log('')
|
|
252
|
+
if (syncResult.ok) {
|
|
253
|
+
console.log(` ${ui.success(glyph.ok)} ${ui.label('代码已自动生成', 'Code generated')}`)
|
|
254
|
+
} else {
|
|
255
|
+
console.log(` ${ui.warning(glyph.warn)} ${ui.label('自动生成失败,可手动跑 tha sync', 'Auto sync failed')}`)
|
|
256
|
+
console.log(` ${ui.muted(syncResult.message)}`)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// package.json 结果
|
|
261
|
+
if (packageJsonResult && packageJsonResult.updated) {
|
|
262
|
+
console.log(` ${ui.success(glyph.ok)} ${ui.label('已加入 dev script', 'Added to dev script')}`)
|
|
263
|
+
console.log(` ${ui.muted('npm run dev 现在会自动跑 tha watch')}`)
|
|
264
|
+
}
|
|
265
|
+
|
|
182
266
|
console.log('')
|
|
183
267
|
printNextSteps()
|
|
184
268
|
} finally {
|
|
@@ -221,6 +305,34 @@ function ensureDirsConfig(root) {
|
|
|
221
305
|
return CONFIG_FILE
|
|
222
306
|
}
|
|
223
307
|
|
|
308
|
+
/**
|
|
309
|
+
* 自动改 package.json 的 dev script,加上 tha watch
|
|
310
|
+
*/
|
|
311
|
+
function updateDevScript(root) {
|
|
312
|
+
const pkgPath = path.join(root, 'package.json')
|
|
313
|
+
if (!exists(pkgPath)) return null
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const pkg = JSON.parse(read(pkgPath))
|
|
317
|
+
if (!pkg.scripts) pkg.scripts = {}
|
|
318
|
+
|
|
319
|
+
const currentDev = pkg.scripts.dev || ''
|
|
320
|
+
if (currentDev.includes('tha watch')) {
|
|
321
|
+
return { updated: false, reason: 'already has tha watch' }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 把 tha watch 加到 dev 命令前面
|
|
325
|
+
pkg.scripts.dev = currentDev
|
|
326
|
+
? `tha watch & ${currentDev}`
|
|
327
|
+
: 'tha watch'
|
|
328
|
+
|
|
329
|
+
writeAtomic(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
330
|
+
return { updated: true, script: pkg.scripts.dev }
|
|
331
|
+
} catch {
|
|
332
|
+
return { updated: false, reason: 'package.json parse failed' }
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
224
336
|
async function ask(rl, { cn, en, hint, fallback }) {
|
|
225
337
|
const suffix = fallback ? ` ${ui.muted(`[${fallback}]`)}` : ''
|
|
226
338
|
const answer = (
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// ============ tha watch:监听 config.ts 变化,自动跑 sync ============
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { loadDirs, relPath } from '../config/paths.mjs'
|
|
6
|
+
import { runSync } from './sync.mjs'
|
|
7
|
+
import { glyph, ui } from '../utils/style.mjs'
|
|
8
|
+
|
|
9
|
+
const DEBOUNCE_MS = 500
|
|
10
|
+
let timer = null
|
|
11
|
+
let running = false
|
|
12
|
+
|
|
13
|
+
export async function runWatch({ root, args }) {
|
|
14
|
+
const { dirs } = loadDirs(root)
|
|
15
|
+
const domainsDir = path.join(root, dirs.domains)
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(domainsDir)) {
|
|
18
|
+
throw new Error(`没有 ${dirs.domains} 目录:先运行 tha init <模块>`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log('')
|
|
22
|
+
console.log(` ${ui.heading('监听模式', 'watch · auto sync on config change')}`)
|
|
23
|
+
console.log(` ${ui.rule(40)}`)
|
|
24
|
+
console.log(` ${ui.muted('监听目录:')} ${ui.path(dirs.domains)}`)
|
|
25
|
+
console.log(` ${ui.muted('改 config.ts 会自动跑 sync,按 Ctrl+C 退出')}`)
|
|
26
|
+
console.log('')
|
|
27
|
+
|
|
28
|
+
// 监听整个 domains 目录
|
|
29
|
+
fs.watch(domainsDir, { recursive: true }, (eventType, filename) => {
|
|
30
|
+
if (!filename) return
|
|
31
|
+
|
|
32
|
+
// 只关心 config.ts 文件
|
|
33
|
+
if (!filename.endsWith('config.ts')) return
|
|
34
|
+
|
|
35
|
+
// debounce:500ms 内只跑一次
|
|
36
|
+
clearTimeout(timer)
|
|
37
|
+
timer = setTimeout(() => {
|
|
38
|
+
handleConfigChange(root, dirs, filename)
|
|
39
|
+
}, DEBOUNCE_MS)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// 保持进程运行
|
|
43
|
+
await new Promise(() => {})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function handleConfigChange(root, dirs, filename) {
|
|
47
|
+
if (running) {
|
|
48
|
+
console.log(` ${ui.muted('sync 正在跑,等跑完再触发...')}`)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
running = true
|
|
53
|
+
console.log(` ${ui.accent(glyph.dot)} config 变化:${ui.path(filename)}`)
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// 从路径里提取模块名
|
|
57
|
+
// filename 是相对 domainsDir 的路径,比如 user/config.ts
|
|
58
|
+
const moduleName = filename.split('/')[0]
|
|
59
|
+
|
|
60
|
+
await runSync({ root, args: [moduleName, '--yes'] })
|
|
61
|
+
console.log(` ${ui.success(glyph.ok)} 已同步\n`)
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.log(` ${ui.danger(glyph.err)} 同步失败:${error.message}\n`)
|
|
64
|
+
} finally {
|
|
65
|
+
running = false
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EXE-01:执行器之间不许互相 import
|
|
3
|
+
* 共用逻辑应该下沉成零件(steps/),不应该横向依赖
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
|
|
8
|
+
const DEFAULT_DIRS = {
|
|
9
|
+
domains: 'src/domains'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getDirs(context) {
|
|
13
|
+
return { ...DEFAULT_DIRS, ...(context.settings?.tha?.dirs || {}) }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isExecutorFile(filename, dirs) {
|
|
17
|
+
const domainsDir = dirs.domains.replace(/\\/g, '/')
|
|
18
|
+
const normalized = filename.replace(/\\/g, '/')
|
|
19
|
+
// 文件在 domains/<模块>/executors/ 下
|
|
20
|
+
return new RegExp(`/${domainsDir}/[^/]+/executors/`).test(normalized)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
meta: {
|
|
25
|
+
type: 'problem',
|
|
26
|
+
docs: {
|
|
27
|
+
description: '执行器之间不许互相 import,共用逻辑请下沉成零件',
|
|
28
|
+
recommended: true
|
|
29
|
+
},
|
|
30
|
+
schema: []
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
create(context) {
|
|
34
|
+
const dirs = getDirs(context)
|
|
35
|
+
const filename = context.filename
|
|
36
|
+
|
|
37
|
+
if (!isExecutorFile(filename, dirs)) return {}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
ImportDeclaration(node) {
|
|
41
|
+
const importPath = node.source.value
|
|
42
|
+
|
|
43
|
+
// import 了另一个执行器(相对路径指向 executors/)
|
|
44
|
+
if (/^(\.\.?\/)+executors\//.test(importPath) || importPath.startsWith('./')) {
|
|
45
|
+
context.report({
|
|
46
|
+
node,
|
|
47
|
+
message: '执行器之间不许互相 import,共用逻辑请下沉成零件(steps/)'
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|