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.
@@ -0,0 +1,68 @@
1
+ /**
2
+ * EXE-03:执行器/零件不许 import utils/(纯函数白名单除外)
3
+ * 日志/时钟/流/鉴权这些必须经 ctx.ports 注入
4
+ */
5
+
6
+ const DEFAULT_DIRS = {
7
+ domains: 'src/domains',
8
+ utils: 'src/utils'
9
+ }
10
+
11
+ // 纯函数白名单:无副作用、不持有状态、不依赖运行时环境
12
+ const PURE_UTILS = ['@/utils/adapters', '@/utils/format', '@/utils/number']
13
+
14
+ function getDirs(context) {
15
+ return { ...DEFAULT_DIRS, ...(context.settings?.tha?.dirs || {}) }
16
+ }
17
+
18
+ function isExecutorOrStepFile(filename, dirs) {
19
+ const domainsDir = dirs.domains.replace(/\\/g, '/')
20
+ const normalized = filename.replace(/\\/g, '/')
21
+ return new RegExp(`/${domainsDir}/[^/]+/(executors|steps)/`).test(normalized)
22
+ }
23
+
24
+ function importSpec(dirs, key) {
25
+ const base = dirs[key].startsWith('src/') ? dirs[key].slice('src/'.length) : dirs[key]
26
+ return `@/${base}`
27
+ }
28
+
29
+ export default {
30
+ meta: {
31
+ type: 'problem',
32
+ docs: {
33
+ description: '执行器/零件不许 import utils/,能力必须经 ctx.ports 注入',
34
+ recommended: true
35
+ },
36
+ schema: []
37
+ },
38
+
39
+ create(context) {
40
+ const dirs = getDirs(context)
41
+ const filename = context.filename
42
+
43
+ if (!isExecutorOrStepFile(filename, dirs)) return {}
44
+
45
+ const utilsBase = importSpec(dirs, 'utils')
46
+
47
+ return {
48
+ ImportDeclaration(node) {
49
+ const importPath = node.source.value
50
+
51
+ // 不是 import utils/ 的,跳过
52
+ if (!importPath.startsWith(`${utilsBase}/`) && importPath !== utilsBase) return
53
+
54
+ // 白名单里的纯函数,放行
55
+ const isPure = PURE_UTILS.some(
56
+ (pure) => importPath === pure || importPath.startsWith(`${pure}/`)
57
+ )
58
+
59
+ if (!isPure) {
60
+ context.report({
61
+ node,
62
+ message: `执行器不许直接 import ${importPath},能力必须经 ctx.ports 注入`
63
+ })
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * HUB-01:中枢不许 import steps/
3
+ * 中枢只做分发,把意图交给执行器,不直接调内部步骤
4
+ */
5
+
6
+ const DEFAULT_DIRS = {
7
+ domains: 'src/domains'
8
+ }
9
+
10
+ function getDirs(context) {
11
+ return { ...DEFAULT_DIRS, ...(context.settings?.tha?.dirs || {}) }
12
+ }
13
+
14
+ function isHubFile(filename, dirs) {
15
+ const domainsDir = dirs.domains.replace(/\\/g, '/')
16
+ const normalized = filename.replace(/\\/g, '/')
17
+ // 文件是 domains/<模块>/index.ts(中枢入口)
18
+ return new RegExp(`/${domainsDir}/[^/]+/index\\.ts$`).test(normalized)
19
+ }
20
+
21
+ export default {
22
+ meta: {
23
+ type: 'problem',
24
+ docs: {
25
+ description: '中枢不许 import steps/,只做分发,把意图交给执行器',
26
+ recommended: true
27
+ },
28
+ schema: []
29
+ },
30
+
31
+ create(context) {
32
+ const dirs = getDirs(context)
33
+ const filename = context.filename
34
+
35
+ if (!isHubFile(filename, dirs)) return {}
36
+
37
+ return {
38
+ ImportDeclaration(node) {
39
+ const importPath = node.source.value
40
+
41
+ // import 了 steps/ 下的文件
42
+ if (/^(\.\.?\/)+steps\//.test(importPath)) {
43
+ context.report({
44
+ node,
45
+ message: '中枢不许直接 import steps/,把这类控制动作放进执行器文件'
46
+ })
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * tha-cli ESLint 插件
3
+ * 把 THA 架构的边界规则做成 ESLint 规则,编辑器里实时红线
4
+ */
5
+
6
+ import hubNoImportSteps from './hub-no-import-steps.js'
7
+ import executorNoCrossImport from './executor-no-cross-import.js'
8
+ import executorNoImportApi from './executor-no-import-api.js'
9
+ import executorNoImportUtils from './executor-no-import-utils.js'
10
+ import viewNoImportApi from './view-no-import-api.js'
11
+ import viewNoCreateModule from './view-no-create-module.js'
12
+ import storeNoImportApi from './store-no-import-api.js'
13
+ import utilNoImportUi from './util-no-import-ui.js'
14
+
15
+ export const rules = {
16
+ 'hub-no-import-steps': hubNoImportSteps,
17
+ 'executor-no-cross-import': executorNoCrossImport,
18
+ 'executor-no-import-api': executorNoImportApi,
19
+ 'executor-no-import-utils': executorNoImportUtils,
20
+ 'view-no-import-api': viewNoImportApi,
21
+ 'view-no-create-module': viewNoCreateModule,
22
+ 'store-no-import-api': storeNoImportApi,
23
+ 'util-no-import-ui': utilNoImportUi
24
+ }
25
+
26
+ export const configs = {
27
+ recommended: {
28
+ plugins: ['tha'],
29
+ rules: {
30
+ 'tha/hub-no-import-steps': 'error',
31
+ 'tha/executor-no-cross-import': 'error',
32
+ 'tha/executor-no-import-api': 'error',
33
+ 'tha/executor-no-import-utils': 'error',
34
+ 'tha/view-no-import-api': 'error',
35
+ 'tha/view-no-create-module': 'error',
36
+ 'tha/store-no-import-api': 'error',
37
+ 'tha/util-no-import-ui': 'error'
38
+ }
39
+ }
40
+ }
41
+
42
+ export default { rules, configs }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * STORE-01:状态层不许 import api 层
3
+ * 状态层只存状态,不发请求
4
+ */
5
+
6
+ const DEFAULT_DIRS = {
7
+ stores: 'src/stores',
8
+ api: 'src/api'
9
+ }
10
+
11
+ function getDirs(context) {
12
+ return { ...DEFAULT_DIRS, ...(context.settings?.tha?.dirs || {}) }
13
+ }
14
+
15
+ function isStoreFile(filename, dirs) {
16
+ const storesDir = dirs.stores.replace(/\\/g, '/')
17
+ const normalized = filename.replace(/\\/g, '/')
18
+ return new RegExp(`/${storesDir}/`).test(normalized)
19
+ }
20
+
21
+ function importSpec(dirs, key) {
22
+ const base = dirs[key].startsWith('src/') ? dirs[key].slice('src/'.length) : dirs[key]
23
+ return `@/${base}`
24
+ }
25
+
26
+ export default {
27
+ meta: {
28
+ type: 'problem',
29
+ docs: {
30
+ description: '状态层不许 import api 层,状态层只存状态,不发请求',
31
+ recommended: true
32
+ },
33
+ schema: []
34
+ },
35
+
36
+ create(context) {
37
+ const dirs = getDirs(context)
38
+ const filename = context.filename
39
+
40
+ if (!isStoreFile(filename, dirs)) return {}
41
+
42
+ const apiBase = importSpec(dirs, 'api')
43
+
44
+ return {
45
+ ImportDeclaration(node) {
46
+ const importPath = node.source.value
47
+
48
+ if (importPath.startsWith(`${apiBase}/`)) {
49
+ context.report({
50
+ node,
51
+ message: '状态层不许 import api 层,把请求挪进执行器,拿到结果再写回 state'
52
+ })
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * UTIL-01:基础设施不许 import UI 库或业务模块
3
+ * 传输层只上报不越权:不感知业务模块,不做展示
4
+ */
5
+
6
+ const DEFAULT_DIRS = {
7
+ utils: 'src/utils',
8
+ views: 'src/views',
9
+ domains: 'src/domains'
10
+ }
11
+
12
+ function getDirs(context) {
13
+ return { ...DEFAULT_DIRS, ...(context.settings?.tha?.dirs || {}) }
14
+ }
15
+
16
+ function isUtilFile(filename, dirs) {
17
+ const utilsDir = dirs.utils.replace(/\\/g, '/')
18
+ const normalized = filename.replace(/\\/g, '/')
19
+ return new RegExp(`/${utilsDir}/`).test(normalized)
20
+ }
21
+
22
+ function importSpec(dirs, key) {
23
+ const base = dirs[key].startsWith('src/') ? dirs[key].slice('src/'.length) : dirs[key]
24
+ return `@/${base}`
25
+ }
26
+
27
+ export default {
28
+ meta: {
29
+ type: 'problem',
30
+ docs: {
31
+ description: '基础设施不许 import UI 库或业务模块',
32
+ recommended: true
33
+ },
34
+ schema: []
35
+ },
36
+
37
+ create(context) {
38
+ const dirs = getDirs(context)
39
+ const filename = context.filename
40
+
41
+ if (!isUtilFile(filename, dirs)) return {}
42
+
43
+ const viewsBase = importSpec(dirs, 'views')
44
+ const domainsBase = importSpec(dirs, 'domains')
45
+
46
+ return {
47
+ ImportDeclaration(node) {
48
+ const importPath = node.source.value
49
+
50
+ // import 了 UI 库
51
+ if (importPath === 'element-plus') {
52
+ context.report({
53
+ node,
54
+ message: '基础设施不许 import UI 库,提示交给页面'
55
+ })
56
+ return
57
+ }
58
+
59
+ // import 了视图层
60
+ if (importPath.startsWith(`${viewsBase}/`)) {
61
+ context.report({
62
+ node,
63
+ message: '基础设施不许 import 视图层'
64
+ })
65
+ return
66
+ }
67
+
68
+ // import 了业务模块
69
+ if (importPath.startsWith(`${domainsBase}/`)) {
70
+ context.report({
71
+ node,
72
+ message: '基础设施不许 import 业务模块'
73
+ })
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * VIEW-02:视图不许自己 create 模块实例
3
+ * 每个领域全局唯一实例,视图只能经 useXxxModule() 取
4
+ */
5
+
6
+ const DEFAULT_DIRS = {
7
+ views: 'src/views',
8
+ domains: 'src/domains'
9
+ }
10
+
11
+ function getDirs(context) {
12
+ return { ...DEFAULT_DIRS, ...(context.settings?.tha?.dirs || {}) }
13
+ }
14
+
15
+ function isViewFile(filename, dirs) {
16
+ const viewsDir = dirs.views.replace(/\\/g, '/')
17
+ const normalized = filename.replace(/\\/g, '/')
18
+ return new RegExp(`/${viewsDir}/`).test(normalized)
19
+ }
20
+
21
+ function importSpec(dirs, key) {
22
+ const base = dirs[key].startsWith('src/') ? dirs[key].slice('src/'.length) : dirs[key]
23
+ return `@/${base}`
24
+ }
25
+
26
+ export default {
27
+ meta: {
28
+ type: 'problem',
29
+ docs: {
30
+ description: '视图不许自己 create 模块实例,必须用 useXxxModule()',
31
+ recommended: true
32
+ },
33
+ schema: []
34
+ },
35
+
36
+ create(context) {
37
+ const dirs = getDirs(context)
38
+ const filename = context.filename
39
+
40
+ if (!isViewFile(filename, dirs)) return {}
41
+
42
+ const domainsBase = importSpec(dirs, 'domains')
43
+ // 匹配 @/domains/user 这种(模块入口)
44
+ const factoryPattern = new RegExp(`^${domainsBase}/[^/]+$`)
45
+
46
+ return {
47
+ ImportDeclaration(node) {
48
+ const importPath = node.source.value
49
+
50
+ if (!factoryPattern.test(importPath)) return
51
+
52
+ // 看 import 的名字里有没有 createXxxModule
53
+ const importedNames = node.specifiers
54
+ .filter((spec) => spec.type === 'ImportSpecifier')
55
+ .map((spec) => spec.imported.name)
56
+
57
+ const factory = importedNames.find((name) => /^create[A-Z]\w*Module$/.test(name))
58
+
59
+ if (factory) {
60
+ context.report({
61
+ node,
62
+ message: `视图不许自己 create 模块实例,改用 useXxxModule()`
63
+ })
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * VIEW-01:视图不许直接 import 网络层、传输工具、执行器内部
3
+ * 视图只表达意图:动作走域中枢,数据直读状态层
4
+ */
5
+
6
+ const DEFAULT_DIRS = {
7
+ views: 'src/views',
8
+ api: 'src/api',
9
+ utils: 'src/utils',
10
+ domains: 'src/domains'
11
+ }
12
+
13
+ function getDirs(context) {
14
+ return { ...DEFAULT_DIRS, ...(context.settings?.tha?.dirs || {}) }
15
+ }
16
+
17
+ function isViewFile(filename, dirs) {
18
+ const viewsDir = dirs.views.replace(/\\/g, '/')
19
+ const normalized = filename.replace(/\\/g, '/')
20
+ return new RegExp(`/${viewsDir}/`).test(normalized)
21
+ }
22
+
23
+ function importSpec(dirs, key) {
24
+ const base = dirs[key].startsWith('src/') ? dirs[key].slice('src/'.length) : dirs[key]
25
+ return `@/${base}`
26
+ }
27
+
28
+ export default {
29
+ meta: {
30
+ type: 'problem',
31
+ docs: {
32
+ description: '视图不许直接 import 网络层、传输工具、执行器内部',
33
+ recommended: true
34
+ },
35
+ schema: []
36
+ },
37
+
38
+ create(context) {
39
+ const dirs = getDirs(context)
40
+ const filename = context.filename
41
+
42
+ if (!isViewFile(filename, dirs)) return {}
43
+
44
+ const apiBase = importSpec(dirs, 'api')
45
+ const utilsBase = importSpec(dirs, 'utils')
46
+ const domainsBase = importSpec(dirs, 'domains')
47
+
48
+ // 传输工具:request / stream / token
49
+ const transportUtils = new Set([
50
+ `${utilsBase}/request`,
51
+ `${utilsBase}/stream`,
52
+ `${utilsBase}/token`
53
+ ])
54
+
55
+ return {
56
+ ImportDeclaration(node) {
57
+ const importPath = node.source.value
58
+
59
+ // import 了 api 层
60
+ if (importPath.startsWith(`${apiBase}/`)) {
61
+ context.report({
62
+ node,
63
+ message: '视图不许直接 import api 层,动作走域中枢'
64
+ })
65
+ return
66
+ }
67
+
68
+ // import 了传输工具
69
+ if (transportUtils.has(importPath)) {
70
+ context.report({
71
+ node,
72
+ message: '视图不许直接 import 传输工具,动作走域中枢'
73
+ })
74
+ return
75
+ }
76
+
77
+ // import 了执行器/零件内部
78
+ if (new RegExp(`^${domainsBase}/[^/]+/(executors|steps)/`).test(importPath)) {
79
+ context.report({
80
+ node,
81
+ message: '视图不许直接 import 执行器内部,走域中枢的动作'
82
+ })
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }