@manohub/app-kit 0.1.0

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 (103) hide show
  1. package/CONTRACT.md +592 -0
  2. package/README.md +27 -0
  3. package/dist/atoms/app-button.d.ts +90 -0
  4. package/dist/atoms/app-button.js +74 -0
  5. package/dist/atoms/app-checkbox.d.ts +86 -0
  6. package/dist/atoms/app-checkbox.js +78 -0
  7. package/dist/atoms/app-icon-button.d.ts +79 -0
  8. package/dist/atoms/app-icon-button.js +54 -0
  9. package/dist/atoms/app-input.d.ts +118 -0
  10. package/dist/atoms/app-input.js +83 -0
  11. package/dist/atoms/app-layout.d.ts +348 -0
  12. package/dist/atoms/app-layout.js +102 -0
  13. package/dist/atoms/app-notice.d.ts +90 -0
  14. package/dist/atoms/app-notice.js +59 -0
  15. package/dist/atoms/app-query-state.d.ts +191 -0
  16. package/dist/atoms/app-query-state.js +161 -0
  17. package/dist/atoms/app-radio-card.d.ts +80 -0
  18. package/dist/atoms/app-radio-card.js +72 -0
  19. package/dist/atoms/app-search-box.d.ts +80 -0
  20. package/dist/atoms/app-search-box.js +64 -0
  21. package/dist/atoms/app-select.d.ts +106 -0
  22. package/dist/atoms/app-select.js +74 -0
  23. package/dist/atoms/app-switch.d.ts +72 -0
  24. package/dist/atoms/app-switch.js +66 -0
  25. package/dist/atoms/app-textarea.d.ts +89 -0
  26. package/dist/atoms/app-textarea.js +61 -0
  27. package/dist/atoms/app-tooltip.d.ts +70 -0
  28. package/dist/atoms/app-tooltip.js +48 -0
  29. package/dist/atoms/atoms.css +679 -0
  30. package/dist/atoms/state-illustration.d.ts +21 -0
  31. package/dist/atoms/state-illustration.js +101 -0
  32. package/dist/components/app-badge.d.ts +72 -0
  33. package/dist/components/app-badge.js +40 -0
  34. package/dist/components/app-dialog.d.ts +164 -0
  35. package/dist/components/app-dialog.js +121 -0
  36. package/dist/components/app-filter.d.ts +173 -0
  37. package/dist/components/app-filter.js +200 -0
  38. package/dist/components/app-form.d.ts +446 -0
  39. package/dist/components/app-form.js +159 -0
  40. package/dist/components/app-pagination.d.ts +78 -0
  41. package/dist/components/app-pagination.js +57 -0
  42. package/dist/components/app-panel.d.ts +399 -0
  43. package/dist/components/app-panel.js +213 -0
  44. package/dist/components/app-section.d.ts +68 -0
  45. package/dist/components/app-section.js +28 -0
  46. package/dist/components/app-steps.d.ts +115 -0
  47. package/dist/components/app-steps.js +108 -0
  48. package/dist/components/app-table.d.ts +415 -0
  49. package/dist/components/app-table.js +479 -0
  50. package/dist/components/app-tabs.d.ts +58 -0
  51. package/dist/components/app-tabs.js +54 -0
  52. package/dist/components/app-tree.d.ts +273 -0
  53. package/dist/components/app-tree.js +222 -0
  54. package/dist/components/components.css +597 -0
  55. package/dist/composables/use-client-pagination.d.ts +39 -0
  56. package/dist/composables/use-client-pagination.js +29 -0
  57. package/dist/entry/create-query-client.d.ts +8 -0
  58. package/dist/entry/create-query-client.js +14 -0
  59. package/dist/entry/create-sub-app.d.ts +45 -0
  60. package/dist/entry/create-sub-app.js +110 -0
  61. package/dist/entry/index.d.ts +4 -0
  62. package/dist/entry/index.js +12 -0
  63. package/dist/entry/initial-guard.d.ts +12 -0
  64. package/dist/entry/initial-guard.js +23 -0
  65. package/dist/index.d.ts +37 -0
  66. package/dist/index.js +67 -0
  67. package/dist/providers/setup-i18n.d.ts +19 -0
  68. package/dist/providers/setup-i18n.js +24 -0
  69. package/dist/services/app-container.d.ts +15 -0
  70. package/dist/services/app-container.js +7 -0
  71. package/dist/services/app-context.d.ts +4 -0
  72. package/dist/services/app-context.js +17 -0
  73. package/dist/services/index.d.ts +5 -0
  74. package/dist/services/loading.d.ts +16 -0
  75. package/dist/services/loading.js +27 -0
  76. package/dist/services/message-box.d.ts +84 -0
  77. package/dist/services/message-box.js +65 -0
  78. package/dist/services/modal.d.ts +51 -0
  79. package/dist/services/modal.js +51 -0
  80. package/dist/services/notify.d.ts +24 -0
  81. package/dist/services/notify.js +22 -0
  82. package/dist/shell/AppShell.d.ts +139 -0
  83. package/dist/shell/AppShell.js +171 -0
  84. package/dist/shell/shell.css +153 -0
  85. package/dist/styles/farris-bridge.css +129 -0
  86. package/dist/styles/index.css +16 -0
  87. package/dist/styles/markdown.css +85 -0
  88. package/dist/styles/reset.css +74 -0
  89. package/dist/styles/tokens.css +111 -0
  90. package/lint/__tests__/fixtures/clean-src/styles.css +14 -0
  91. package/lint/__tests__/fixtures/clean-src/views/good-page.tsx +15 -0
  92. package/lint/__tests__/fixtures/violations-src/styles.css +18 -0
  93. package/lint/__tests__/fixtures/violations-src/views/bad-page.tsx +38 -0
  94. package/lint/__tests__/fixtures/violations-src/views/no-shell-page.tsx +4 -0
  95. package/lint/__tests__/guardrails.spec.mjs +144 -0
  96. package/lint/component-audit.mjs +131 -0
  97. package/lint/guardrails.config.schema.json +74 -0
  98. package/lint/pre-commit.sample +26 -0
  99. package/lint/run-all.mjs +59 -0
  100. package/lint/shared.mjs +347 -0
  101. package/lint/structure-audit.mjs +254 -0
  102. package/lint/style-audit.mjs +200 -0
  103. package/package.json +78 -0
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * structure-audit —— 页面结构护栏(S1–S7 + 自绘竖直边框检查)。
4
+ *
5
+ * S1 路由级页面必须用 AppShell
6
+ * S2 AppShell.Header 必须传 title(或登记为自定义页头白名单)
7
+ * S3 AppShell.Body 必须显式声明 mode
8
+ * S4 禁原生 button/input/select/textarea 带 class 承载外观
9
+ * S5 禁 h-screen / 100vh
10
+ * S6 禁自绘页头与面板头类名(*titlebar* / *page-header* / *panel-head* / *nav-head*)
11
+ * S7 面板 toolbar 表单控件 >3 → warn(建议上提 AppShell.Filter);AppShell.Filter 与面板 toolbar 同时放字段 → warn
12
+ * S8 用了 AppShell.Split 的应用,其 CSS 不应再出现竖直边框(分隔线由 Split 提供)→ warn
13
+ * S9 分页与承载表格的容器错位(文件里有 AppPanel 却把 AppPagination 放 AppShell.Footer)→ warn
14
+ *
15
+ * 用法:node node_modules/@manohub/app-kit/lint/structure-audit.mjs [--json] [--changed]
16
+ */
17
+ import { readFileSync } from 'node:fs'
18
+ import { join } from 'node:path'
19
+ import {
20
+ loadConfig,
21
+ parseArgs,
22
+ collectFiles,
23
+ changedFiles,
24
+ stripComments,
25
+ displayPath,
26
+ makeViolation,
27
+ report,
28
+ } from './shared.mjs'
29
+
30
+ const args = parseArgs()
31
+ const cwd = args.cwd
32
+ const config = loadConfig(cwd)
33
+ const changed = args.changed ? changedFiles(cwd) : null
34
+
35
+ /** 路由级页面的判定:位于 views/pages 目录下的 tsx(弹窗组件在 components 下,不受 S1 约束)。 */
36
+ const ROUTE_PAGE_RE = /(?:^|\/)(?:views|pages)\//
37
+ /** 表单控件(S7 计数用)。 */
38
+ const FORM_CONTROLS = /<(AppInput|AppSearchBox|AppSelect|AppDatePicker|AppTextarea|AppNumber)\b/g
39
+ /** 自绘页头/面板头类名(S6)。 */
40
+ const SELF_HEADER_RE = /(?:titlebar|title-bar|page-header|page-title|panel-head|panel-title|nav-head|nav-caption|grid-card|grid-wrap)/i
41
+ /**
42
+ * 原生控件带 class(S4)。
43
+ *
44
+ * 含**无 `href` 的锚点**:`<a class=… onClick=…>` 是「按钮的伪装」(本仓踩过:行内操作 4 处、
45
+ * 移除列 3 处都靠它逃过检查)。带 `href` 的锚点是真链接(导航语义),不在本条范围内。
46
+ */
47
+ const NATIVE_CONTROL_RE = /<(button|input|select|textarea)\b[^>]*\bclass\s*=|<a\b(?![^>]*\bhref\b)[^>]*\bclass\s*=/
48
+
49
+ function violation(app, file, rule, line, message, snippet, severity = 'error') {
50
+ return makeViolation({ app, file: displayPath(cwd, file), rule, line, severity, message, snippet })
51
+ }
52
+
53
+ /** 取 `name={<...>}` / `name={render()}` 的整段文本(括号配平),用于计数面板 toolbar 里的控件。 */
54
+ function extractAttrBlock(source, index, attrName) {
55
+ const at = source.indexOf(`${attrName}=`, index)
56
+ if (at < 0) return ''
57
+ let i = source.indexOf('{', at)
58
+ if (i < 0) return ''
59
+ let depth = 0
60
+ const start = i
61
+ for (; i < source.length; i++) {
62
+ if (source[i] === '{') depth += 1
63
+ else if (source[i] === '}') {
64
+ depth -= 1
65
+ if (depth === 0) return source.slice(start, i + 1)
66
+ }
67
+ }
68
+ return source.slice(start)
69
+ }
70
+
71
+ const violations = []
72
+ for (const app of config.apps) {
73
+ if (args.app && app.name !== args.app && app.dir !== args.app) continue
74
+ const root = join(cwd, app.dir)
75
+ const srcFiles = collectFiles(root, config, ['.ts', '.tsx'], config.srcGlobs)
76
+ let usesSplit = false
77
+ for (const file of srcFiles) {
78
+ if (changed) {
79
+ const rel = displayPath(cwd, file)
80
+ if (![...changed].some((c) => c === rel || c.endsWith(rel))) continue
81
+ }
82
+ const source = stripComments(readFileSync(file, 'utf8'))
83
+ const rel = displayPath(cwd, file)
84
+ const lines = source.split('\n')
85
+ const hasShell = /<AppShell\b/.test(source)
86
+ if (/<AppShell\.Split\b/.test(source)) usesSplit = true
87
+
88
+ // S1:路由级页面必须用 AppShell
89
+ if (ROUTE_PAGE_RE.test(rel) && file.endsWith('.tsx') && !hasShell) {
90
+ violations.push(violation(app.name, file, 'structure/no-shell', 1, '路由级页面必须使用 AppShell 承载结构', rel))
91
+ }
92
+ // S2 / S3
93
+ if (hasShell) {
94
+ const headerIdx = source.indexOf('<AppShell.Header')
95
+ if (headerIdx >= 0 && !/<AppShell\.Header[^>]*\btitle\s*=/.test(source)) {
96
+ violations.push(
97
+ violation(
98
+ app.name,
99
+ file,
100
+ 'structure/header-title',
101
+ source.slice(0, headerIdx).split('\n').length,
102
+ 'AppShell.Header 未传 title',
103
+ '<AppShell.Header',
104
+ ),
105
+ )
106
+ }
107
+ const bodyIdx = source.indexOf('<AppShell.Body')
108
+ if (bodyIdx >= 0 && !/<AppShell\.Body[^>]*\bmode\s*=/.test(source)) {
109
+ violations.push(
110
+ violation(
111
+ app.name,
112
+ file,
113
+ 'structure/body-mode',
114
+ source.slice(0, bodyIdx).split('\n').length,
115
+ 'AppShell.Body 未显式声明 mode',
116
+ '<AppShell.Body',
117
+ ),
118
+ )
119
+ }
120
+ }
121
+
122
+ // S7:面板 toolbar 字段数
123
+ let panelToolbarTotal = 0
124
+ let searchFrom = 0
125
+ while (true) {
126
+ const at = source.indexOf('<AppPanel', searchFrom)
127
+ if (at < 0) break
128
+ const block = extractAttrBlock(source, at, 'toolbar')
129
+ const count = (block.match(FORM_CONTROLS) ?? []).length
130
+ panelToolbarTotal += count
131
+ if (count > 3) {
132
+ violations.push(
133
+ violation(
134
+ app.name,
135
+ file,
136
+ 'structure/toolbar-fields',
137
+ source.slice(0, at).split('\n').length,
138
+ `面板 toolbar 内表单控件 ${count} 个(上限 3)`,
139
+ block.slice(0, 120),
140
+ 'warn',
141
+ ),
142
+ )
143
+ }
144
+ searchFrom = at + 10
145
+ }
146
+ if (panelToolbarTotal > 0 && /<AppShell\.Filter\b/.test(source)) {
147
+ violations.push(
148
+ violation(
149
+ app.name,
150
+ file,
151
+ 'structure/toolbar-fields',
152
+ 1,
153
+ '页面级 AppShell.Filter 与面板 toolbar 同时放了字段,请核对是否重复筛选',
154
+ rel,
155
+ 'warn',
156
+ ),
157
+ )
158
+ }
159
+
160
+ // S4:原生控件带 class(含无 href 的锚点)
161
+ //
162
+ // **按整段源码扫,不逐行扫**:把 `<input` 与其 `class=` 拆成两行是常见排版
163
+ // (属性多时几乎必然如此),逐行正则会静默漏掉整条规则
164
+ // —— 本仓踩过:列编辑表格的 `<input`↵`class="vm-cell-input"` 一直没被报出来。
165
+ for (const match of source.matchAll(new RegExp(NATIVE_CONTROL_RE.source, 'g'))) {
166
+ const lineNo = source.slice(0, match.index).split('\n').length
167
+ violations.push(
168
+ violation(
169
+ app.name,
170
+ file,
171
+ 'structure/native-control',
172
+ lineNo,
173
+ '不得用原生控件(含无 href 的锚点)承载外观/交互,控件一律用组件',
174
+ lines[lineNo - 1],
175
+ ),
176
+ )
177
+ }
178
+
179
+ // S5 / S6
180
+ for (let i = 0; i < lines.length; i++) {
181
+ const line = lines[i]
182
+ const lineNo = i + 1
183
+ if (/h-screen|100vh/.test(line)) {
184
+ violations.push(violation(app.name, file, 'structure/viewport-height', lineNo, '禁用 h-screen / 100vh', line))
185
+ }
186
+ const cls = /(?:class|className)\s*=\s*["'`]([^"'`]*)["'`]/.exec(line)
187
+ if (cls && SELF_HEADER_RE.test(cls[1])) {
188
+ violations.push(
189
+ violation(
190
+ app.name,
191
+ file,
192
+ 'structure/self-header',
193
+ lineNo,
194
+ `疑似自绘页头/面板头/表格框类名:${cls[1].trim()}(页头用 AppShell.Header、面板头用 AppPanel、表格框用 AppTable framed)`,
195
+ line,
196
+ ),
197
+ )
198
+ }
199
+ }
200
+
201
+ // S9:分页与承载表格的容器错位
202
+ // 文件里既有 AppPanel(说明存在区域级容器)又在 AppShell.Footer 里放 AppPagination
203
+ // → 提示核对:表格若在面板内,分页应放 AppPanel.Footer(表格与分页必须同容器)
204
+ if (/<AppPanel[\s>]/.test(source) && source.includes('AppShell.Footer')) {
205
+ let insideShellFooter = false
206
+ for (let i = 0; i < lines.length; i++) {
207
+ if (/<AppShell\.Footer/.test(lines[i])) insideShellFooter = true
208
+ if (insideShellFooter && /\bAppPagination\b/.test(lines[i])) {
209
+ violations.push(
210
+ violation(
211
+ app.name,
212
+ file,
213
+ 'structure/pagination-scope',
214
+ i + 1,
215
+ '分页应与承载表格的容器同层:表格在 AppPanel 内时分页放 AppPanel.Footer,不要把区域列表的分页提到页面级 AppShell.Footer',
216
+ lines[i],
217
+ 'warn',
218
+ ),
219
+ )
220
+ break
221
+ }
222
+ if (/<\/AppShell\.Footer>/.test(lines[i])) insideShellFooter = false
223
+ }
224
+ }
225
+ }
226
+
227
+ // S8:用了 Split 的应用不该再写竖直边框
228
+ if (usesSplit) {
229
+ for (const cssFile of collectFiles(root, config, ['.css'], config.styleGlobs)) {
230
+ if (changed) {
231
+ const rel = displayPath(cwd, cssFile)
232
+ if (![...changed].some((c) => c === rel || c.endsWith(rel))) continue
233
+ }
234
+ const lines = stripComments(readFileSync(cssFile, 'utf8')).split('\n')
235
+ for (let i = 0; i < lines.length; i++) {
236
+ if (/border-(?:right|left)\s*:/.test(lines[i])) {
237
+ violations.push(
238
+ violation(
239
+ app.name,
240
+ cssFile,
241
+ 'structure/self-border',
242
+ i + 1,
243
+ '使用了 AppShell.Split,不应再自绘竖直边框(分隔线由 Split 提供)',
244
+ lines[i],
245
+ 'warn',
246
+ ),
247
+ )
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ process.exit(report('structure-audit(页面结构护栏)', violations, args, config))
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * style-audit —— 样式护栏(应用侧只允许布局属性)。
4
+ *
5
+ * 规则(编号沿用方案 §16,新增 12):
6
+ * 4 禁 `@import "tailwindcss..."` / `@tailwindcss*`
7
+ * 5 禁在应用侧定义令牌(`--*:`)与 `@theme`
8
+ * 6 禁视觉属性(color / background* / border-radius / box-shadow / font-* / fill / stroke)
9
+ * 7 禁 `!important`(唯一允许出现的位置是包内 farris-bridge.css,不在本审计范围内)
10
+ * 8 禁裸元素选择器与 `*`
11
+ * 9 选择器首段必须匹配登记前缀或 `.app-container`
12
+ * 10 禁书写 `.ak-*` 选择器(ak- 归 app-kit 所有)
13
+ * 11 禁 tsx 的 `class=` 里出现工具类形态字符串
14
+ * 12 禁自写 rem 尺寸(全仓统一 px 令牌,不跟随根字号 —— CONTRACT.md §9)
15
+ *
16
+ * 豁免:文件头含 `@figma-fidelity` 且登记在豁免注册表时,跳过规则 6 / 8 / 9
17
+ * (规则 4/5/7/10/11/12 仍然生效)。
18
+ *
19
+ * 用法:
20
+ * node node_modules/@manohub/app-kit/lint/style-audit.mjs [--json] [--strict] [--changed] [--app=<name>]
21
+ * 配置:应用包根 appkit-guardrails.config.json
22
+ */
23
+ import { readFileSync, existsSync } from 'node:fs'
24
+ import { join } from 'node:path'
25
+ import {
26
+ loadConfig,
27
+ parseArgs,
28
+ collectFiles,
29
+ changedFiles,
30
+ stripComments,
31
+ displayPath,
32
+ makeViolation,
33
+ report,
34
+ } from './shared.mjs'
35
+
36
+ const args = parseArgs()
37
+ const cwd = args.cwd
38
+ const config = loadConfig(cwd)
39
+ const changed = args.changed ? changedFiles(cwd) : null
40
+
41
+ /** 视觉属性(规则 6)。布局属性一律不在列表内。 */
42
+ const VISUAL_PROPS = [
43
+ 'color',
44
+ 'background',
45
+ 'background-color',
46
+ 'background-image',
47
+ 'background-position',
48
+ 'background-size',
49
+ 'background-repeat',
50
+ 'background-clip',
51
+ 'border-radius',
52
+ 'box-shadow',
53
+ 'font',
54
+ 'font-size',
55
+ 'font-weight',
56
+ 'font-family',
57
+ 'font-style',
58
+ 'line-height',
59
+ 'fill',
60
+ 'stroke',
61
+ 'text-shadow',
62
+ 'opacity',
63
+ ]
64
+
65
+ /** 带色值的边框简写(规则 6):值里出现色值字面量时违规。 */
66
+ const BORDER_SHORTHAND = /^border(?:-(?:top|right|bottom|left))?(?:-(?:width|style|color))?$/
67
+
68
+ /** 工具类形态(规则 11,窄口径,避免误伤业务类名)。 */
69
+ const UTILITY_CLASS = /(?:^|\s)(?:p|m|px|py|mx|my|pt|pb|pl|pr|w|h|text|bg|border|rounded|gap|items|justify|leading|tracking)-(?:\[|\d|xs|sm|base|lg|xl|full|auto|center|none)/
70
+
71
+ function violation(app, file, rule, line, message, snippet, severity = 'error') {
72
+ return makeViolation({ app, file: displayPath(cwd, file), rule, line, severity, message, snippet })
73
+ }
74
+
75
+ /** 该文件是否登记为设计还原豁免(跳过规则 6/8/9)。 */
76
+ function isExempt(file, source) {
77
+ if (!config.exemptionRegistry) return false
78
+ if (!/@figma-fidelity/.test(source)) return false
79
+ const registry = join(cwd, config.exemptionRegistry)
80
+ if (!existsSync(registry)) return false
81
+ const text = readFileSync(registry, 'utf8')
82
+ return text.includes(displayPath(cwd, file)) || text.includes(file.split(/[\\/]/).pop())
83
+ }
84
+
85
+ function auditCss(app, file, source, out) {
86
+ const exempt = isExempt(file, source)
87
+ const lines = stripComments(source).split('\n')
88
+ const prefixes = app.prefixes ?? []
89
+ let selector = []
90
+ for (let i = 0; i < lines.length; i++) {
91
+ const rawLine = lines[i]
92
+ const line = rawLine.trim()
93
+ if (!line) continue
94
+ const lineNo = i + 1
95
+
96
+ // 规则 4:tailwind 引入
97
+ if (/@import\s+['"]tailwindcss|@tailwindcss/.test(line)) {
98
+ out.push(violation(app.name, file, 'style/tailwind', lineNo, '禁止引入 Tailwind', line))
99
+ }
100
+ // 规则 5:@theme / 令牌定义
101
+ if (/@theme/.test(line)) {
102
+ out.push(violation(app.name, file, 'style/token', lineNo, '禁止在应用侧定义 @theme', line))
103
+ }
104
+ if (/^\s*--[\w-]+\s*:/.test(rawLine) && !/[({]/.test(line)) {
105
+ out.push(violation(app.name, file, 'style/token', lineNo, '禁止在应用侧定义 CSS 令牌', line))
106
+ }
107
+
108
+ // 选择器(以 { 结尾或本行含 {)
109
+ const braceIdx = line.indexOf('{')
110
+ if (braceIdx >= 0) {
111
+ const head = line.slice(0, braceIdx).trim()
112
+ if (head) selector.push(head)
113
+ const first = selector.join(' ').split(',')[0].trim()
114
+ if (first) {
115
+ if (!exempt) {
116
+ // 规则 8:裸元素选择器 / *
117
+ const firstSeg = first.split(/[\s>+~]+/)[0]
118
+ const isBareElement = /^[a-zA-Z][a-zA-Z0-9-]*$/.test(firstSeg) || firstSeg.startsWith('*')
119
+ if (isBareElement) {
120
+ out.push(violation(app.name, file, 'style/bare-element', lineNo, `禁止裸元素选择器 \`${firstSeg}\``, first))
121
+ }
122
+ // 规则 9:首段必须匹配登记前缀
123
+ if (
124
+ prefixes.length > 0 &&
125
+ !isBareElement &&
126
+ !prefixes.some((p) => first.includes(p)) &&
127
+ !first.includes('.app-container')
128
+ ) {
129
+ out.push(
130
+ violation(app.name, file, 'style/prefix', lineNo, `选择器首段未匹配登记前缀(${prefixes.join(' / ')})`, first),
131
+ )
132
+ }
133
+ }
134
+ // 规则 10:.ak-* 选择器
135
+ if (/\.ak-[\w-]/.test(first)) {
136
+ out.push(violation(app.name, file, 'style/ak-selector', lineNo, '禁止在应用侧书写 .ak-* 选择器', first))
137
+ }
138
+ }
139
+ if (line.includes('}')) selector = []
140
+ continue
141
+ }
142
+ if (line === '}') {
143
+ selector = []
144
+ continue
145
+ }
146
+
147
+ // 声明行
148
+ const decl = /^([\w-]+)\s*:\s*(.+?);?\s*$/.exec(line)
149
+ if (decl) {
150
+ const prop = decl[1].toLowerCase()
151
+ const value = decl[2]
152
+ if (/!important/.test(value)) {
153
+ out.push(violation(app.name, file, 'style/important', lineNo, '禁止 !important', line))
154
+ }
155
+ if (/\b\d*\.?\d+rem\b/.test(value)) {
156
+ out.push(violation(app.name, file, 'style/rem', lineNo, '禁止自写 rem 尺寸', line))
157
+ }
158
+ if (!exempt) {
159
+ // 规则 6:视觉属性(含带色值的 border 简写)一律走组件或 --ui-* 令牌
160
+ if (VISUAL_PROPS.includes(prop) || BORDER_SHORTHAND.test(prop)) {
161
+ out.push(violation(app.name, file, 'style/visual', lineNo, `禁止视觉属性 \`${prop}\``, line))
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ function auditSource(app, file, source, out) {
169
+ const lines = stripComments(source).split('\n')
170
+ for (let i = 0; i < lines.length; i++) {
171
+ const line = lines[i]
172
+ const lineNo = i + 1
173
+ if (/@import\s+['"]tailwindcss|from\s+['"]tailwindcss|@tailwindcss/.test(line)) {
174
+ out.push(violation(app.name, file, 'style/tailwind', lineNo, '禁止引入 Tailwind', line))
175
+ }
176
+ const classMatch = /(?:class|className)\s*=\s*["'`]([^"'`]*)["'`]/.exec(line)
177
+ if (classMatch && UTILITY_CLASS.test(classMatch[1])) {
178
+ out.push(violation(app.name, file, 'style/utility-class', lineNo, '禁止工具类形态字符串', line))
179
+ }
180
+ }
181
+ }
182
+
183
+ const violations = []
184
+ for (const app of config.apps) {
185
+ if (args.app && app.name !== args.app && app.dir !== args.app) continue
186
+ const root = join(cwd, app.dir)
187
+ const cssFiles = collectFiles(root, config, ['.css'], config.styleGlobs)
188
+ const srcFiles = collectFiles(root, config, ['.ts', '.tsx'], config.srcGlobs)
189
+ for (const file of [...cssFiles, ...srcFiles]) {
190
+ if (changed) {
191
+ const rel = displayPath(cwd, file)
192
+ if (![...changed].some((c) => c === rel || c.endsWith(rel))) continue
193
+ }
194
+ const source = readFileSync(file, 'utf8')
195
+ if (file.endsWith('.css')) auditCss(app, file, source, violations)
196
+ else auditSource(app, file, source, violations)
197
+ }
198
+ }
199
+
200
+ process.exit(report('style-audit(样式护栏)', violations, args, config))
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@manohub/app-kit",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "子应用统一骨架层:入口编排(createSubApp)、布局契约(AppShell)、页面组件、原子件、服务、样式底座。farris 被收敛在本包内部,对外只暴露标准 API。",
7
+ "license": "UNLICENSED",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://git.iec.io/gsp-cloud-ds/dip/ibp/aihub/frontend-infra.git",
11
+ "directory": "packages/app-kit"
12
+ },
13
+ "publishConfig": {
14
+ "registry": "https://registry.npmjs.org/",
15
+ "access": "public"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "lint",
20
+ "CONTRACT.md",
21
+ "README.md"
22
+ ],
23
+ "sideEffects": [
24
+ "**/*.css"
25
+ ],
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ },
31
+ "./entry": {
32
+ "types": "./dist/entry/index.d.ts",
33
+ "import": "./dist/entry/index.js"
34
+ },
35
+ "./styles.css": "./dist/styles/index.css",
36
+ "./reset.css": "./dist/styles/reset.css",
37
+ "./CONTRACT.md": "./CONTRACT.md",
38
+ "./lint/*": "./lint/*"
39
+ },
40
+ "main": "./dist/index.js",
41
+ "module": "./dist/index.js",
42
+ "types": "./dist/index.d.ts",
43
+ "scripts": {
44
+ "build": "node ../../scripts/clean-dist.mjs && vue-tsc -p tsconfig.build.json && vite build && node scripts/copy-styles.mjs",
45
+ "type-check": "vue-tsc --noEmit -p tsconfig.json",
46
+ "test:unit": "vitest run && node --test lint/__tests__/guardrails.spec.mjs",
47
+ "test:guardrails": "node --test lint/__tests__/guardrails.spec.mjs",
48
+ "test:watch": "vitest"
49
+ },
50
+ "peerDependencies": {
51
+ "@tanstack/vue-query": "^5.0.0",
52
+ "i18next": "^25.0.0",
53
+ "i18next-browser-languagedetector": "^8.0.0",
54
+ "i18next-vue": "^5.0.0",
55
+ "pinia": "^4.0.0",
56
+ "vue": "^3.5.0",
57
+ "vue-router": "^4.6.0"
58
+ },
59
+ "dependencies": {
60
+ "@farris/ui-vue": "^1.8.4",
61
+ "@manohub/icon": "workspace:^0.1.0"
62
+ },
63
+ "devDependencies": {
64
+ "@tanstack/vue-query": "catalog:",
65
+ "@vitejs/plugin-vue-jsx": "catalog:",
66
+ "i18next": "catalog:",
67
+ "i18next-browser-languagedetector": "catalog:",
68
+ "i18next-vue": "catalog:",
69
+ "jsdom": "^25.0.1",
70
+ "pinia": "catalog:",
71
+ "typescript": "catalog:",
72
+ "vite": "catalog:",
73
+ "vitest": "^3.2.7",
74
+ "vue": "catalog:",
75
+ "vue-router": "catalog:",
76
+ "vue-tsc": "catalog:"
77
+ }
78
+ }