@manohub/app-kit 0.2.1 → 0.2.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/CONTRACT.md +208 -9
- package/README.md +9 -5
- package/bin/appkit.mjs +121 -0
- package/dist/components/app-drawer.d.ts +139 -0
- package/dist/components/app-drawer.js +104 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/styles/farris-bridge.css +8 -0
- package/lint/__tests__/guardrails.spec.mjs +154 -0
- package/lint/component-audit.mjs +1 -1
- package/lint/guardrails.config.schema.json +42 -0
- package/lint/pre-commit.sample +38 -26
- package/lint/run-all.mjs +63 -59
- package/lint/shared.mjs +462 -347
- package/lint/structure-audit.mjs +1 -1
- package/lint/style-audit.mjs +1 -1
- package/package.json +7 -3
- package/skills/README.md +32 -10
- package/skills/app-kit/SKILL.md +19 -11
- package/skills/app-kit/references/adoption.md +76 -6
- package/skills/app-kit/references/contract-index.md +7 -2
- package/skills/app-kit-dev/SKILL.md +40 -8
- package/skills/app-kit-dev/references/page-recipes.md +53 -5
- package/skills/app-kit-dev/references/style-rules.md +2 -2
- package/skills/app-kit-migrate/SKILL.md +30 -11
- package/skills/app-kit-migrate/references/migration-playbook.md +53 -4
- package/skills/install.mjs +298 -222
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export { AppTable, type AppTableColumn, type AppTableProps, type AppTableSelecti
|
|
|
24
24
|
export { AppPagination } from './components/app-pagination';
|
|
25
25
|
export { AppBadge, type AppBadgeTone, type AppBadgeShape, type AppBadgeSize, type AppBadgeDot } from './components/app-badge';
|
|
26
26
|
export { AppDialog } from './components/app-dialog';
|
|
27
|
+
export { AppDrawer } from './components/app-drawer';
|
|
27
28
|
export { AppTabs, type AppTabItem } from './components/app-tabs';
|
|
28
29
|
export { AppForm, type AppFormProps, type AppFormItemProps, type AppFormLabelAlign, type AppFormCompound, } from './components/app-form';
|
|
29
30
|
export { AppSection, type AppSectionProps } from './components/app-section';
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { AppTable } from "./components/app-table.js";
|
|
|
18
18
|
import { AppPagination } from "./components/app-pagination.js";
|
|
19
19
|
import { AppBadge } from "./components/app-badge.js";
|
|
20
20
|
import { AppDialog } from "./components/app-dialog.js";
|
|
21
|
+
import { AppDrawer } from "./components/app-drawer.js";
|
|
21
22
|
import { AppTabs } from "./components/app-tabs.js";
|
|
22
23
|
import { AppForm } from "./components/app-form.js";
|
|
23
24
|
import { AppSection } from "./components/app-section.js";
|
|
@@ -34,6 +35,7 @@ export {
|
|
|
34
35
|
AppButton,
|
|
35
36
|
AppCheckbox,
|
|
36
37
|
AppDialog,
|
|
38
|
+
AppDrawer,
|
|
37
39
|
AppFilter,
|
|
38
40
|
AppForm,
|
|
39
41
|
AppIconButton,
|
|
@@ -110,6 +110,14 @@ body > .fv-tooltip.tooltip {
|
|
|
110
110
|
min-height: 0;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/* AppDrawer:抽屉被 Teleport 进 `.app-container`(拿令牌与应用侧样式)后,上游会给根节点加
|
|
114
|
+
* `f-drawer-inline` 把它从 `position: fixed` 改成 `absolute` —— 而微前端里绝对定位的包含块
|
|
115
|
+
* 不可控(`.app-container` 自身未定位,宿主层级随门户而变),抽屉与遮罩会错位。
|
|
116
|
+
* 这里把包含块语义压回视口,覆盖范围与改造前 `host: "body"` 时完全一致。 */
|
|
117
|
+
.app-container .f-drawer.f-drawer-inline {
|
|
118
|
+
position: fixed;
|
|
119
|
+
}
|
|
120
|
+
|
|
113
121
|
/* ===== AppSteps 只读档(clickable=false)的光标 =====
|
|
114
122
|
* 上游 FStep 的 `clickable` prop 在 1.8.4 里**完全没被使用**,`li.clickable` 只由单个 step 的
|
|
115
123
|
* `disabled` 决定 ⇒ 整条只读时上游仍给非禁用项手型光标。拦截已由 AppSteps 的门控承担,
|
|
@@ -41,6 +41,28 @@ function runAudit(script, cwd) {
|
|
|
41
41
|
return { code: res.status, out: (res.stdout ?? '') + (res.stderr ?? '') }
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** 同上,但可追加参数(如 --strict / --json) */
|
|
45
|
+
function runAuditWith(script, cwd, extraArgs = []) {
|
|
46
|
+
const res = spawnSync(process.execPath, [join(LINT, script), '--cwd=' + cwd, ...extraArgs], {
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
})
|
|
49
|
+
return { code: res.status, out: (res.stdout ?? '') + (res.stderr ?? '') }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 改写临时消费仓的护栏配置(其余字段保持默认) */
|
|
53
|
+
function writeConfig(root, apps, extra = {}) {
|
|
54
|
+
writeFileSync(
|
|
55
|
+
join(root, 'appkit-guardrails.config.json'),
|
|
56
|
+
JSON.stringify({
|
|
57
|
+
apps,
|
|
58
|
+
srcGlobs: ['src/**/*.ts', 'src/**/*.tsx'],
|
|
59
|
+
styleGlobs: ['src/**/*.css'],
|
|
60
|
+
ignore: ['node_modules', 'dist'],
|
|
61
|
+
...extra,
|
|
62
|
+
}),
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
44
66
|
test('glob:嵌套路径必须匹配(防「只扫顶层文件」的静默漏扫)', async () => {
|
|
45
67
|
const { globToRegExp } = await import('../shared.mjs')
|
|
46
68
|
const src = globToRegExp('src/**/*.tsx')
|
|
@@ -52,6 +74,33 @@ test('glob:嵌套路径必须匹配(防「只扫顶层文件」的静默漏
|
|
|
52
74
|
assert.ok(css.test('src/features/x/y.css'), '深层 css 应匹配')
|
|
53
75
|
})
|
|
54
76
|
|
|
77
|
+
test('changedFiles:未暂存的新增文件必须纳入(否则「新增文件必须归零」会假绿)', async () => {
|
|
78
|
+
const { changedFiles } = await import('../shared.mjs')
|
|
79
|
+
const root = mkdtempSync(join(tmpdir(), 'appkit-changed-'))
|
|
80
|
+
const git = (...args) => spawnSync('git', args, { cwd: root, encoding: 'utf8' })
|
|
81
|
+
try {
|
|
82
|
+
if (git('init', '-q', '.').status !== 0) return // 无 git:跳过(本机的护栏自测仍覆盖其余规则)
|
|
83
|
+
git('config', 'user.email', 'guardrails@test')
|
|
84
|
+
git('config', 'user.name', 'guardrails')
|
|
85
|
+
writeFileSync(join(root, 'tracked.ts'), 'export const a = 1\n')
|
|
86
|
+
assert.equal(git('add', 'tracked.ts').status, 0)
|
|
87
|
+
if (git('commit', '-qm', 'init').status !== 0) return // 提交失败(无身份/无 hooks 环境):跳过
|
|
88
|
+
|
|
89
|
+
writeFileSync(join(root, 'untracked-new.ts'), 'export const b = 2\n')
|
|
90
|
+
const changed = changedFiles(root)
|
|
91
|
+
assert.ok(changed, '仓库内应返回改动集合(null 表示非 git 环境)')
|
|
92
|
+
assert.ok(
|
|
93
|
+
changed.has('untracked-new.ts'),
|
|
94
|
+
`未 git add 的新增文件必须进集合,实际:${[...changed].join(', ')}`,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
writeFileSync(join(root, 'tracked.ts'), 'export const a = 2\n')
|
|
98
|
+
assert.ok(changedFiles(root).has('tracked.ts'), '已跟踪文件的改动仍须进集合')
|
|
99
|
+
} finally {
|
|
100
|
+
rmSync(root, { recursive: true, force: true })
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
55
104
|
test('违规样本:三条护栏都必须报错,且每条违规都带 correction', () => {
|
|
56
105
|
const root = makeConsumer('violations-src')
|
|
57
106
|
try {
|
|
@@ -133,6 +182,111 @@ test('pending 应用:照常报告但不计入退出码', () => {
|
|
|
133
182
|
}
|
|
134
183
|
})
|
|
135
184
|
|
|
185
|
+
test('规则级豁免:只迁骨架(Shell-only)时,结构类纳入门禁、组件与样式类登记豁免', () => {
|
|
186
|
+
const root = makeConsumer('violations-src')
|
|
187
|
+
try {
|
|
188
|
+
// 只迁骨架:结构类立刻要合规(本 fixture 结构违规很多 → 必须仍拦断),
|
|
189
|
+
// 组件直连与样式类先登记豁免。
|
|
190
|
+
writeConfig(root, [
|
|
191
|
+
{
|
|
192
|
+
dir: 'app',
|
|
193
|
+
name: 'fixture-app',
|
|
194
|
+
prefixes: ['other-'],
|
|
195
|
+
pending: false,
|
|
196
|
+
waivedRules: [
|
|
197
|
+
{ rule: 'api/*', reason: 'Shell-only:组件替换在下一批次', since: '2026-09-18' },
|
|
198
|
+
{ rule: 'style/*', reason: '同上:样式收口等骨架稳定后', since: '2026-09-18' },
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
])
|
|
202
|
+
|
|
203
|
+
// component-audit:违规只剩豁免项 → 退出码 0,但**照常报告**(技术债必须可见)
|
|
204
|
+
const component = runAudit('component-audit.mjs', root)
|
|
205
|
+
assert.equal(component.code, 0, '豁免后不应拦断')
|
|
206
|
+
assert.ok(component.out.includes('api/farris-import'), '豁免项仍必须出现在报告里')
|
|
207
|
+
assert.ok(component.out.includes('豁免:已登记'), '豁免项要标明「已登记」')
|
|
208
|
+
assert.ok(component.out.includes('条属已登记豁免'), '汇总行要给出豁免条数')
|
|
209
|
+
|
|
210
|
+
// style-audit:同理(样式类整类豁免)
|
|
211
|
+
const style = runAudit('style-audit.mjs', root)
|
|
212
|
+
assert.equal(style.code, 0)
|
|
213
|
+
assert.ok(style.out.includes('style/visual'))
|
|
214
|
+
|
|
215
|
+
// structure-audit:没有豁免 → 结构类违规仍拦断(这才是「只迁骨架」的门禁)
|
|
216
|
+
const structure = runAudit('structure-audit.mjs', root)
|
|
217
|
+
assert.equal(structure.code, 1, '结构类规则未豁免,必须仍然拦断')
|
|
218
|
+
assert.ok(structure.out.includes('structure/no-shell'))
|
|
219
|
+
} finally {
|
|
220
|
+
rmSync(root, { recursive: true, force: true })
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test('规则级豁免:--strict 时豁免一律失效(收口验收口径)', () => {
|
|
225
|
+
const root = makeConsumer('violations-src')
|
|
226
|
+
try {
|
|
227
|
+
writeConfig(root, [
|
|
228
|
+
{
|
|
229
|
+
dir: 'app',
|
|
230
|
+
name: 'fixture-app',
|
|
231
|
+
prefixes: ['other-'],
|
|
232
|
+
pending: true,
|
|
233
|
+
waivedRules: [{ rule: 'style/*', reason: '分阶段迁移', since: '2026-09-18' }],
|
|
234
|
+
},
|
|
235
|
+
])
|
|
236
|
+
assert.equal(runAudit('style-audit.mjs', root).code, 0, '平时:豁免 + 挂起 → 不拦断')
|
|
237
|
+
assert.equal(runAuditWith('style-audit.mjs', root, ['--strict']).code, 1, '--strict:豁免与挂起都失效')
|
|
238
|
+
} finally {
|
|
239
|
+
rmSync(root, { recursive: true, force: true })
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('规则级豁免:0 条命中时提示可撤销(豁免只该在「还没迁到那层」期间存在)', () => {
|
|
244
|
+
const root = makeConsumer('clean-src')
|
|
245
|
+
try {
|
|
246
|
+
writeConfig(root, [
|
|
247
|
+
{
|
|
248
|
+
dir: 'app',
|
|
249
|
+
name: 'fixture-app',
|
|
250
|
+
prefixes: ['other-'],
|
|
251
|
+
pending: false,
|
|
252
|
+
waivedRules: [{ rule: 'api/*', reason: '上一批次留下的', since: '2026-09-01' }],
|
|
253
|
+
},
|
|
254
|
+
])
|
|
255
|
+
const { code, out } = runAudit('component-audit.mjs', root)
|
|
256
|
+
assert.equal(code, 0)
|
|
257
|
+
assert.ok(out.includes('可以撤销'), `0 条命中应提示撤销,实际输出:${out}`)
|
|
258
|
+
assert.ok(out.includes('api/*'))
|
|
259
|
+
} finally {
|
|
260
|
+
rmSync(root, { recursive: true, force: true })
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('规则级豁免:配置缺 reason 时直接报错(不留静音键)', () => {
|
|
265
|
+
const root = makeConsumer('clean-src')
|
|
266
|
+
try {
|
|
267
|
+
writeConfig(root, [
|
|
268
|
+
{ dir: 'app', name: 'fixture-app', prefixes: ['other-'], waivedRules: [{ rule: 'api/*' }] },
|
|
269
|
+
])
|
|
270
|
+
const { code, out } = runAudit('component-audit.mjs', root)
|
|
271
|
+
assert.equal(code, 2, '配置错误应走「环境问题」退出码,而不是静默通过')
|
|
272
|
+
assert.ok(out.includes('缺少 reason'))
|
|
273
|
+
} finally {
|
|
274
|
+
rmSync(root, { recursive: true, force: true })
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
test('matchRule:精确名与整类通配各按各的匹配,不做中间通配', async () => {
|
|
279
|
+
const { matchRule } = await import('../shared.mjs')
|
|
280
|
+
assert.equal(matchRule('api/farris-import', 'api/farris-import'), true)
|
|
281
|
+
assert.equal(matchRule('api/*', 'api/legacy-prop'), true)
|
|
282
|
+
assert.equal(matchRule('api/*', 'style/visual'), false)
|
|
283
|
+
assert.equal(matchRule('structure/*', 'structure/no-shell'), true)
|
|
284
|
+
// 中间通配不实现:写出来会让人以为能精确匹配
|
|
285
|
+
assert.equal(matchRule('style/*-element', 'style/bare-element'), false)
|
|
286
|
+
assert.equal(matchRule('', 'api/legacy-prop'), false)
|
|
287
|
+
assert.equal(matchRule(undefined, 'api/legacy-prop'), false)
|
|
288
|
+
})
|
|
289
|
+
|
|
136
290
|
test('--changed:非 git 环境下不裁剪(宁可多检查)', () => {
|
|
137
291
|
const root = makeConsumer('violations-src')
|
|
138
292
|
try {
|
package/lint/component-audit.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* `selection={{…}}` 是 app-kit 自己的结构化契约,**合法**;
|
|
14
14
|
* 而 `rowOption` / `columnOption` / `enableSelectRow` / `enabelSelectRow` 等属于底层,一律禁止。
|
|
15
15
|
*
|
|
16
|
-
* 用法:
|
|
16
|
+
* 用法:pnpm exec appkit lint:component [--json] [--changed]
|
|
17
17
|
*/
|
|
18
18
|
import { readFileSync } from 'node:fs'
|
|
19
19
|
import { join } from 'node:path'
|
|
@@ -30,6 +30,34 @@
|
|
|
30
30
|
"type": "boolean",
|
|
31
31
|
"default": false,
|
|
32
32
|
"description": "存量挂起:为 true 时该应用仅产出基线报告,不计入退出码(迁移完成后置 false 即自动纳入门禁)。"
|
|
33
|
+
},
|
|
34
|
+
"waivedRules": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"description": "规则级豁免(**分阶段迁移**用,典型是「只迁骨架 / Shell-only」):登记的规则照常报告但不计入退出码。要收口时跑 `--strict` 验证,并在撤销豁免后纳入门禁。规则名支持 `域/*` 整类通配(如 structure/*);每条必须写 reason。",
|
|
37
|
+
"items": {
|
|
38
|
+
"oneOf": [
|
|
39
|
+
{ "type": "string", "description": "简写形式(不推荐:没有理由,无法审计)" },
|
|
40
|
+
{
|
|
41
|
+
"type": "object",
|
|
42
|
+
"required": ["rule", "reason"],
|
|
43
|
+
"properties": {
|
|
44
|
+
"rule": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "规则名(如 api/farris-import)或整类通配(如 style/*)。"
|
|
47
|
+
},
|
|
48
|
+
"reason": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "为什么暂时不查 + 什么时候收口(如「只迁骨架,组件替换在下一批次」)。必填。"
|
|
51
|
+
},
|
|
52
|
+
"since": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "登记日期(ISO 8601,如 2026-09-18)。"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"additionalProperties": false
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
33
61
|
}
|
|
34
62
|
}
|
|
35
63
|
}
|
|
@@ -69,6 +97,20 @@
|
|
|
69
97
|
"exemptionRegistry": "docs/appkit-exemptions.md",
|
|
70
98
|
"contractDoc": "node_modules/@manohub/app-kit/CONTRACT.md",
|
|
71
99
|
"ignore": ["node_modules", "dist"]
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"apps": [
|
|
103
|
+
{
|
|
104
|
+
"dir": ".",
|
|
105
|
+
"name": "shell-only-app",
|
|
106
|
+
"prefixes": ["so-"],
|
|
107
|
+
"pending": false,
|
|
108
|
+
"waivedRules": [
|
|
109
|
+
{ "rule": "api/*", "reason": "只迁骨架(Shell-only):组件替换在下一批次", "since": "2026-09-18" },
|
|
110
|
+
{ "rule": "style/*", "reason": "同上:样式收口等骨架稳定后再做", "since": "2026-09-18" }
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
]
|
|
72
114
|
}
|
|
73
115
|
]
|
|
74
116
|
}
|
package/lint/pre-commit.sample
CHANGED
|
@@ -1,26 +1,38 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# app-kit 护栏 pre-commit
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# app-kit 护栏 pre-commit 钩子样本:只检查**已改动文件**(含新增但未提交的文件),目标 < 3 秒
|
|
3
|
+
# (否则人会在本机把它关掉)。
|
|
4
|
+
#
|
|
5
|
+
# 启用(不需要改 git 配置,默认 hooks 目录即可):
|
|
6
|
+
# cp node_modules/@manohub/app-kit/lint/pre-commit.sample .git/hooks/pre-commit
|
|
7
|
+
# chmod +x .git/hooks/pre-commit
|
|
8
|
+
#
|
|
9
|
+
# 应用包不是仓库根时(monorepo,如 apps/sub-app):**必须**用环境变量指出包目录,
|
|
10
|
+
# 因为钩子的 cwd 恒为仓库根,而本包通常只装在应用包自己的 node_modules 下:
|
|
11
|
+
# APPKIT_APP_DIR=apps/sub-app (推荐在钩子文件里写死一行,或提交前 export)
|
|
12
|
+
#
|
|
13
|
+
# 说明:护栏以「应用包根」为 cwd 读取 appkit-guardrails.config.json;找不到脚本或配置时
|
|
14
|
+
# 会明确告知并**跳过**本次检查(不阻塞提交)—— 静默通过的假绿比报错更危险。
|
|
15
|
+
set -e
|
|
16
|
+
|
|
17
|
+
# 应用包目录(缺省仓库根)
|
|
18
|
+
APP_DIR="${APPKIT_APP_DIR:-.}"
|
|
19
|
+
|
|
20
|
+
# 护栏脚本按「应用包自己的 node_modules → 仓库根 node_modules」依次找
|
|
21
|
+
if [ -f "$APP_DIR/node_modules/@manohub/app-kit/lint/run-all.mjs" ]; then
|
|
22
|
+
AUDIT="$APP_DIR/node_modules/@manohub/app-kit/lint/run-all.mjs"
|
|
23
|
+
elif [ -f "node_modules/@manohub/app-kit/lint/run-all.mjs" ]; then
|
|
24
|
+
AUDIT="node_modules/@manohub/app-kit/lint/run-all.mjs"
|
|
25
|
+
else
|
|
26
|
+
echo "[guardrails] 未找到护栏脚本(已查 $APP_DIR/node_modules 与 node_modules)。"
|
|
27
|
+
echo "[guardrails] monorepo 请在钩子里设置 APPKIT_APP_DIR=<应用包目录>(如 apps/sub-app),"
|
|
28
|
+
echo "[guardrails] 否则本次提交不检查(不阻塞,但请尽快修好这条路径)。"
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
if [ ! -f "$APP_DIR/appkit-guardrails.config.json" ]; then
|
|
33
|
+
echo "[guardrails] $APP_DIR 下缺少 appkit-guardrails.config.json,无法确定被审计的应用。"
|
|
34
|
+
echo "[guardrails] 配置片段见 node_modules/@manohub/app-kit/CONTRACT.md §8。"
|
|
35
|
+
exit 0
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
exec node "$AUDIT" --changed --cwd="$APP_DIR"
|
package/lint/run-all.mjs
CHANGED
|
@@ -1,59 +1,63 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* run-all —— 三条护栏的统一入口(接入方只挂这一条,见 CONTRACT.md §8)。
|
|
4
|
-
*
|
|
5
|
-
* 并发跑 style / component / structure,汇总输出与退出码;支持 `--changed`(只跑改动文件,供 pre-commit)。
|
|
6
|
-
* 任一脚本报 error 即整体失败;`--json` 时输出聚合结果。
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* run-all —— 三条护栏的统一入口(接入方只挂这一条,见 CONTRACT.md §8)。
|
|
4
|
+
*
|
|
5
|
+
* 并发跑 style / component / structure,汇总输出与退出码;支持 `--changed`(只跑改动文件,供 pre-commit)。
|
|
6
|
+
* 任一脚本报 error 即整体失败;`--json` 时输出聚合结果。
|
|
7
|
+
*
|
|
8
|
+
* 用法(经包的命令入口;接入方的 `package.json` 只挂前两条,其余是排查时才用的):
|
|
9
|
+
* pnpm exec appkit lint # 全量(挂成 "lint")
|
|
10
|
+
* pnpm exec appkit lint --changed # 只跑改动文件(挂成 "lint:changed")
|
|
11
|
+
* pnpm exec appkit lint --strict # 收口验收:存量挂起与规则豁免一律失效
|
|
12
|
+
* pnpm exec appkit lint:style # 单条排查:style / component / structure
|
|
13
|
+
* pnpm exec appkit lint --json
|
|
14
|
+
*
|
|
15
|
+
* 直接跑本脚本(`node node_modules/@manohub/app-kit/lint/run-all.mjs`)与上面完全等价 ——
|
|
16
|
+
* CLI 只是薄壳,已有钩子/脚本不必改。
|
|
17
|
+
*/
|
|
18
|
+
import { spawn } from 'node:child_process'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
import { dirname, join } from 'node:path'
|
|
21
|
+
import { parseArgs } from './shared.mjs'
|
|
22
|
+
|
|
23
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
24
|
+
const AUDITS = ['style-audit.mjs', 'component-audit.mjs', 'structure-audit.mjs']
|
|
25
|
+
|
|
26
|
+
const args = parseArgs()
|
|
27
|
+
const passthrough = process.argv.slice(2).filter((a) => a.startsWith('--app=') || a.startsWith('--cwd='))
|
|
28
|
+
if (args.changed) passthrough.push('--changed')
|
|
29
|
+
if (args.json) passthrough.push('--json')
|
|
30
|
+
|
|
31
|
+
const runOne = (script) =>
|
|
32
|
+
new Promise((resolve) => {
|
|
33
|
+
const child = spawn(process.execPath, [join(HERE, script), ...passthrough], {
|
|
34
|
+
cwd: args.cwd,
|
|
35
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
36
|
+
})
|
|
37
|
+
let stdout = ''
|
|
38
|
+
let stderr = ''
|
|
39
|
+
child.stdout.on('data', (d) => (stdout += d))
|
|
40
|
+
child.stderr.on('data', (d) => (stderr += d))
|
|
41
|
+
child.on('close', (code) => resolve({ script, code, stdout, stderr }))
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const started = Date.now()
|
|
45
|
+
const results = await Promise.all(AUDITS.map(runOne))
|
|
46
|
+
const elapsed = ((Date.now() - started) / 1000).toFixed(2)
|
|
47
|
+
|
|
48
|
+
if (args.json) {
|
|
49
|
+
process.stdout.write(JSON.stringify({ audits: results.map((r) => ({ script: r.script, code: r.code })), elapsed }, null, 2) + '\n')
|
|
50
|
+
} else {
|
|
51
|
+
for (const r of results) {
|
|
52
|
+
if (r.stdout) process.stdout.write(r.stdout)
|
|
53
|
+
if (r.stderr) process.stderr.write(r.stderr)
|
|
54
|
+
}
|
|
55
|
+
const failed = results.filter((r) => r.code !== 0)
|
|
56
|
+
process.stdout.write(
|
|
57
|
+
failed.length === 0
|
|
58
|
+
? `✓ 护栏(三条):全绿(${elapsed}s)\n`
|
|
59
|
+
: `✗ 护栏(三条):${failed.map((f) => f.script).join(' / ')} 未通过(${elapsed}s)\n`,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
process.exit(results.some((r) => r.code !== 0) ? 1 : 0)
|