@dsh-workbench/module-tools 0.3.2 → 0.3.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.
Files changed (37) hide show
  1. package/README.md +134 -7
  2. package/bin/dsh-wb-module.mjs +53 -4
  3. package/package.json +11 -3
  4. package/src/arch.mjs +256 -0
  5. package/src/build.mjs +4 -1
  6. package/src/check-flags.mjs +22 -0
  7. package/src/check.mjs +23 -2
  8. package/src/checks/arch.mjs +127 -0
  9. package/src/checks/kit.mjs +68 -19
  10. package/src/checks/lint.mjs +351 -0
  11. package/src/checks/primitives.mjs +239 -5
  12. package/src/design.mjs +47 -5
  13. package/src/discover.mjs +9 -1
  14. package/src/init.mjs +33 -0
  15. package/src/lint.mjs +309 -0
  16. package/src/primitives-surface.mjs +120 -0
  17. package/src/shell-type-files.mjs +103 -0
  18. package/template/README.md.tpl +18 -3
  19. package/template/arch.config.mjs +2 -0
  20. package/template/eslint.config.mjs +2 -0
  21. package/template/package.json.tpl +4 -1
  22. package/template/src/client.tsx.tpl +4 -18
  23. package/template/src/components/HelloCard.tsx.tpl +28 -0
  24. package/template-diy/README.md.tpl +44 -19
  25. package/template-diy/arch.config.mjs +2 -0
  26. package/template-diy/design.json.tpl +1 -0
  27. package/template-diy/eslint.config.mjs +2 -0
  28. package/template-diy/package.json.tpl +4 -1
  29. package/template-diy/src/client.tsx.tpl +10 -149
  30. package/template-diy/src/components/OverviewCard.module.css.tpl +5 -0
  31. package/template-diy/src/components/OverviewCard.test.ts.tpl +118 -0
  32. package/template-diy/src/components/OverviewCard.tsx.tpl +92 -0
  33. package/template-diy/src/host.ts.tpl +1 -14
  34. package/template-diy/src/model/overview.test.ts.tpl +49 -0
  35. package/template-diy/src/model/overview.ts.tpl +61 -0
  36. package/template-diy/src/plugin.test.ts.tpl +4 -140
  37. package/template-diy/types/styles.d.ts +14 -0
package/src/check.mjs CHANGED
@@ -19,6 +19,15 @@
19
19
  // 不适用 ○ <title>:(不适用)<notes>
20
20
  // 失败 ✖ <title> <N> 条不过: 然后逐条 ` · <problem>`
21
21
  // 最后一行是汇总;有任何 fail 就 exit 1。
22
+ //
23
+ // ── 两条加法(2026-10-19,dsh-primitives-face)────────────────────────────────
24
+ //
25
+ // ① 开关透传:聚合层只认 `--only` / `--cwd`,其余开关(`--all` / `--json`)原样经
26
+ // `DSH_WB_ONLY_ARGS`(JSON 数组)交给**单跑的那一条**。为什么走 env 而不是给 `run()`
27
+ // 加参数:所有判据的形状都是 `run()` 无参,加参数会让"统一形状"这条纪律多一个例外。
28
+ // ② 可选 `payload`:判据可以额外返回 `payload`;带着 `--json` 跑时,聚合层打它、**跳过**
29
+ // 人话版 report 与最后的汇总行(stdout 只有一份 JSON)。老判据不返回它,行为一字不变 ——
30
+ // 这是加法,不是新形状。
22
31
 
23
32
  // 名字与 title 一字不变 —— selfcheck 会把它与各判据模块导出的 `title` 逐个对一遍。
24
33
  export const CHECKS = {
@@ -26,6 +35,8 @@ export const CHECKS = {
26
35
  protocol: { title: '模块声明协议', load: () => import('./checks/protocol.mjs') },
27
36
  bundles: { title: '模块产物边界', load: () => import('./checks/bundles.mjs') },
28
37
  source: { title: '模块源码边界', load: () => import('./checks/source.mjs') },
38
+ arch: { title: '模块架构(分层 / 边界 / 设计系统)', load: () => import('./checks/arch.mjs') },
39
+ lint: { title: '模块代码卫生(eslint)', load: () => import('./checks/lint.mjs') },
29
40
  primitives: { title: 'dsh 原语只从模块表拿', load: () => import('./checks/primitives.mjs') },
30
41
  kit: { title: '外壳 kit 面', load: () => import('./checks/kit.mjs') },
31
42
  cards: { title: '卡片渲染', load: () => import('./checks/cards.mjs') },
@@ -56,11 +67,13 @@ function report(title, result) {
56
67
  for (const note of notes.slice(1)) console.log(` ${note}`);
57
68
  }
58
69
 
59
- export async function check({ only } = {}) {
70
+ export async function check({ only, args = [] } = {}) {
60
71
  const names = only === undefined ? Object.keys(CHECKS) : [only];
72
+ const jsonMode = args.includes('--json');
61
73
  const tally = { pass: 0, skip: 0, fail: 0 };
62
74
  const skipped = [];
63
75
  let ran = 0;
76
+ process.env.DSH_WB_ONLY_ARGS = JSON.stringify(args);
64
77
  try {
65
78
  for (const name of names) {
66
79
  const entry = CHECKS[name];
@@ -87,10 +100,18 @@ export async function check({ only } = {}) {
87
100
  }
88
101
  tally[result.status] += 1;
89
102
  if (result.status === 'skip') skipped.push(`${title}(${result.notes[0] ?? ''})`);
90
- report(title, result);
103
+ if (jsonMode && result.payload !== undefined) console.log(JSON.stringify(result.payload, null, 2));
104
+ else report(title, result);
91
105
  }
92
106
  } finally {
93
107
  process.env.DSH_WB_ONLY = '';
108
+ process.env.DSH_WB_ONLY_ARGS = '';
109
+ }
110
+
111
+ // `--json` 的 stdout 只有那几份 payload:汇总行与"不适用"那几段都是人话,不进机器档。
112
+ if (jsonMode) {
113
+ if (tally.fail > 0) process.exit(1);
114
+ return;
94
115
  }
95
116
 
96
117
  console.log(`${ran} 条判据:${tally.pass} 通过 · ${tally.skip} 不适用 · ${tally.fail} 不过`);
@@ -0,0 +1,127 @@
1
+ // 模块包的**架构判据**(`check` 的第十二条):把 arch-guard 跑在**模块自己的方言**上。
2
+ //
3
+ // 为什么模块包需要这一条:另外十一条判据全是"边界"(自述清单 / 产物 / 可达性 / 卡片渲染 / 样式归属),
4
+ // 没有一条在看**代码结构** —— 而模块包是用户自己写的、模板给的又是能一路长成几百行的起步形状。
5
+ // 结构退化(巨文件、反向依赖、宿主侧拖进组件、未登记的目录、模型层没有测试)以前只靠人自觉。
6
+ //
7
+ // 判据的口径(三条,都要守住):
8
+ // ① **只跑带 `arch.config.mjs` 的模块**:没有那份声明就 `skip`(不适用),不是红。
9
+ // 工坊里已经造好的模块(以及别的仓里已经发出去的模块)不会被这次升级炸掉 —— 补一行就上车。
10
+ // ② 方言住 `../arch.mjs`(`@dsh-workbench/module-tools/arch`),不在这条判据里 ——
11
+ // 判据只负责"跑起来 + 把结果翻成人话"。
12
+ // ③ `cache: false`:模块目录不一定有 node_modules,arch-guard 的 facts 缓存会落到模块根下
13
+ // (实测会打一句"记得加进 .gitignore")—— 门禁不该往用户的包里拉屎。
14
+ //
15
+ // 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
16
+ // 打印与退出码归 `../check.mjs` 那一层。
17
+
18
+ import { existsSync } from 'node:fs';
19
+ import { join } from 'node:path';
20
+ import { builtinSourceForms, runGuard } from '@arch-guard/core';
21
+ import { discoverModulePackages } from '../discover.mjs';
22
+
23
+ export const title = '模块架构(分层 / 边界 / 设计系统)';
24
+
25
+ const CONFIG_FILE = 'arch.config.mjs';
26
+
27
+ // "怎么上车"那句话只写一遍:skip 的提示与 fail 的提示都要它。
28
+ const HOW_TO_JOIN =
29
+ `在模块包里放一份 ${CONFIG_FILE},内容一行就够:\n` +
30
+ ` import { moduleArch } from '@dsh-workbench/module-tools/arch';\n` +
31
+ ' export default moduleArch();\n' +
32
+ ' 形状(层序表 / 两个运行时 / 阈值)见 packages/module-tools/src/arch.mjs;' +
33
+ '要开口子就从 moduleArch() 的参数进(deps / layers / roles / exceptions)。';
34
+
35
+ const severityIndex = (rules) => new Map(rules.map((rule) => [rule.id, rule.severity]));
36
+
37
+ const severityOf = (finding, index) => finding.severity ?? index.get(finding.rule) ?? 'error';
38
+
39
+ /** 一条 finding 翻成一行人话:位置 → 说明 → 规则与修法。 */
40
+ function lineOf(name, finding) {
41
+ const where = `${name}: ${finding.file}:${String(finding.line)}`;
42
+ const hint = typeof finding.hint === 'string' && finding.hint !== '' ? ` —— ${finding.hint}` : '';
43
+ return `${where} ${finding.text}(${finding.rule})${hint}`;
44
+ }
45
+
46
+ export async function run() {
47
+ const modulePackages = discoverModulePackages({ root: process.cwd() });
48
+ if (modulePackages.length === 0) {
49
+ return {
50
+ status: 'skip',
51
+ notes: ['没有模块包(没有声明 dsh.workbench 的包)—— 没有源码要判结构'],
52
+ problems: [],
53
+ };
54
+ }
55
+
56
+ const declared = modulePackages.filter((pkg) => existsSync(join(pkg.dir, CONFIG_FILE)));
57
+ if (declared.length === 0) {
58
+ return {
59
+ status: 'skip',
60
+ notes: [
61
+ `${modulePackages.length} 个模块包都没有 ${CONFIG_FILE} —— 没有架构声明就无从判定(不是"没问题")。` +
62
+ `\n ${HOW_TO_JOIN}`,
63
+ ],
64
+ problems: [],
65
+ };
66
+ }
67
+
68
+ const problems = [];
69
+ let files = 0;
70
+ let warnings = 0;
71
+ // 真的跑过的规则 id(与 arch-guard 报告头那句"规则 58/107"同一个口径:`stats` 是逐规则执行统计)。
72
+ const ranRules = new Set();
73
+
74
+ for (const pkg of declared) {
75
+ const name = pkg.manifest.name;
76
+ let result;
77
+ try {
78
+ result = await runGuard({
79
+ cwd: pkg.dir,
80
+ // **相对**路径:arch-guard 把 configPath 拼在 cwd 后面(实测传绝对路径会拼成
81
+ // `<cwd>/<绝对路径>`,再报"找不到配置文件")。
82
+ configPath: CONFIG_FILE,
83
+ sourceForms: builtinSourceForms,
84
+ cache: false,
85
+ quiet: true,
86
+ format: 'json',
87
+ });
88
+ } catch (error) {
89
+ const message = error instanceof Error ? error.message : String(error);
90
+ problems.push(
91
+ `${name}: ${CONFIG_FILE} 跑不起来 —— ${message}(先确认这份配置能 import、没有语法错,` +
92
+ '再确认它 import 的东西在工坊里解析得到)',
93
+ );
94
+ continue;
95
+ }
96
+
97
+ // `scopeFiles` 在 full 档是空的(实测)—— 解析过的文件数从 facts 缓存那份统计里拿
98
+ // (`cache: false` 时 `misses` 就是这一轮解析了几个文件,见 arch-guard 的 RunResult 注释)。
99
+ files += result.cache.misses;
100
+ for (const stat of result.stats) ranRules.add(stat.rule);
101
+ const index = severityIndex(result.config.rules);
102
+ for (const finding of result.active) {
103
+ if (severityOf(finding, index) === 'error') problems.push(lineOf(name, finding));
104
+ else warnings += 1;
105
+ }
106
+ }
107
+
108
+ if (problems.length > 0) {
109
+ return {
110
+ status: 'fail',
111
+ // 提示放在 notes 里,判据层照旧只报"哪几条不过"。
112
+ notes: [`红了怎么修:先看位置与规则号,再决定"改代码"还是"在 ${CONFIG_FILE} 里开口子"。`],
113
+ problems,
114
+ };
115
+ }
116
+
117
+ return {
118
+ status: 'pass',
119
+ notes: [
120
+ `${declared.length}/${modulePackages.length} 个模块包带 ${CONFIG_FILE} · ${files} 个源文件 · ` +
121
+ `${ranRules.size} 条规则在跑` +
122
+ (warnings === 0 ? '' : ` · ${warnings} 条 warn(不挡门禁)`) +
123
+ (declared.length === modulePackages.length ? '' : `;其余 ${modulePackages.length - declared.length} 个没声明(不适用)`),
124
+ ],
125
+ problems: [],
126
+ };
127
+ }
@@ -22,7 +22,9 @@
22
22
 
23
23
  import { existsSync, readFileSync } from 'node:fs';
24
24
  import { join } from 'node:path';
25
+ import { flagValue, flagsOf } from '../check-flags.mjs';
25
26
  import { discoverModulePackages } from '../discover.mjs';
27
+ import { memberFilesOf } from '../shell-type-files.mjs';
26
28
  import { shellClientPathOf, shellKitOf } from './shell-artifact.mjs';
27
29
 
28
30
  const SHELL = '@dsh-workbench/core';
@@ -82,14 +84,19 @@ function below(a, b) {
82
84
  // 只在单跑时打:外壳把整份 `@dsh-workbench/shared` barrel 摊进了每个桶,所以这份名单
83
85
  // 有一百多个名字 —— 聚合跑时它只是噪音,查询时才值钱。
84
86
  //
85
- // 返回的是若干 `notes`(每条一行,交给聚合层加缩进),而不是打印 —— 打印归 check.mjs。
87
+ // 名字还要能落到**文件**("想问哪个直接去看"):默认打两棵类型树的绝对根 + 取法,
88
+ // `--files` 打逐条 `名字 → 绝对路径`(190 条 / ~9k 字符,会被 tool-result-pruner 裁中间,
89
+ // 所以它得配 `| grep <名字>` 用,默认档不内联)。`--json` 给机器档。
90
+ //
91
+ // 返回的是 `{ notes, payload }`(notes 每条一行,交给聚合层加缩进;payload 归 `--json`)——
92
+ // 打印归 check.mjs。
86
93
  function faceNotes(asked, shells) {
87
94
  const faceShell = shells[shells.length - 1] ?? shellClientPathOf(process.cwd());
88
95
  if (!asked) {
89
- return ['(想知道这份外壳的 kit 里有什么:dsh-wb-module check --only kit)'];
96
+ return { notes: ['(想知道这份外壳的 kit 里有什么:dsh-wb-module check --only kit)'] };
90
97
  }
91
98
  if (faceShell === undefined || !existsSync(faceShell)) {
92
- return ['(没找到外壳的 client.js,列不出 kit 面:先确认 node_modules 里装着 @dsh-workbench/core)'];
99
+ return { notes: ['(没找到外壳的 client.js,列不出 kit 面:先确认 node_modules 里装着 @dsh-workbench/core)'] };
93
100
  }
94
101
  try {
95
102
  const kit = shellKitOf(faceShell);
@@ -97,23 +104,59 @@ function faceNotes(asked, shells) {
97
104
  .filter((bucket) => typeof kit[bucket] === 'object' && kit[bucket] !== null)
98
105
  .sort();
99
106
  if (buckets.length === 0) {
100
- return [`(${faceShell} 里没取出 kit:外壳版本可能太旧)`];
107
+ return { notes: [`(${faceShell} 里没取出 kit:外壳版本可能太旧)`] };
101
108
  }
102
109
  const every = new Map();
103
110
  for (const bucket of buckets) {
104
111
  for (const name of Object.keys(kit[bucket])) every.set(name, (every.get(name) ?? 0) + 1);
105
112
  }
106
- const notes = [`这份外壳(${faceShell})的 kit 面 —— 只能用这些,别自己造:`];
107
- for (const bucket of buckets) {
108
- const names = Object.keys(kit[bucket]);
109
- const only = names.filter((name) => every.get(name) === 1).sort();
110
- notes.push(` ${bucket}(${names.length}):${only.length === 0 ? '(全部来自下面的共用面)' : only.join(', ')}`);
113
+ const flags = flagsOf();
114
+ const one = flagValue(flags, '--file');
115
+ const { roots, files, missing } = memberFilesOf(faceShell, [...every.keys()]);
116
+ const rootLine =
117
+ Object.entries(roots).length === 0
118
+ ? ' 类型面:读不到(外壳包里没有 dist/types,@dsh-workbench/shared 也没装)'
119
+ : ` 类型面(绝对路径):${Object.entries(roots).map(([pkg, dir]) => `${pkg} → ${dir}`).join(' | ')}` +
120
+ ` (某个名字的文件:--file <名字>,或 --files | grep <名字>)`;
121
+
122
+ const notes = [`这份外壳(${faceShell})的 kit 面 —— 只能用这些,别自己造:`, rootLine];
123
+ if (one !== undefined) {
124
+ const file = files.get(one);
125
+ notes.push(
126
+ file === undefined
127
+ ? ` --file ${one}:不在这份清单里(用 --files | grep ${one} 找找近似的名字)`
128
+ : ` --file ${one}:${file} (打开看它的 JSDoc 与 prop:sed -n '1,60p' 那一份)`,
129
+ );
130
+ }
131
+ if (flags.includes('--files')) {
132
+ notes.push(
133
+ ` 名字 → 类型文件(绝对路径,全表 ${every.size} 条、会被 tool-result-pruner 裁中间:用 --files | grep <名字> 取一件)`,
134
+ );
135
+ for (const name of [...every.keys()].sort()) {
136
+ notes.push(` ${name} → ${files.get(name) ?? '(没扫到,见上面那两棵根)'}`);
137
+ }
138
+ if (missing.length > 0) notes.push(` ${missing.length} 个没扫到:${missing.join('、')}`);
139
+ } else if (one === undefined) {
140
+ for (const bucket of buckets) {
141
+ const names = Object.keys(kit[bucket]);
142
+ const only = names.filter((name) => every.get(name) === 1).sort();
143
+ notes.push(` ${bucket}(${names.length}):${only.length === 0 ? '(全部来自下面的共用面)' : only.join(', ')}`);
144
+ }
145
+ const shared = [...every.entries()].filter(([, count]) => count > 1).map(([name]) => name).sort();
146
+ notes.push(` 共用面(${shared.length}):每个桶里都有 —— ${shared.join(', ')}`);
111
147
  }
112
- const shared = [...every.entries()].filter(([, count]) => count > 1).map(([name]) => name).sort();
113
- notes.push(` 共用面(${shared.length}):每个桶里都有 —— ${shared.join(', ')}`);
114
- return notes;
148
+ return {
149
+ notes,
150
+ payload: {
151
+ shell: faceShell,
152
+ roots,
153
+ buckets: Object.fromEntries(buckets.map((bucket) => [bucket, Object.keys(kit[bucket])])),
154
+ files: Object.fromEntries([...every.keys()].sort().map((name) => [name, files.get(name) ?? null])),
155
+ missing,
156
+ },
157
+ };
115
158
  } catch (error) {
116
- return [`(取不出这份外壳的 kit 面:${error instanceof Error ? error.message : String(error)})`];
159
+ return { notes: [`(取不出这份外壳的 kit 面:${error instanceof Error ? error.message : String(error)})`] };
117
160
  }
118
161
  }
119
162
 
@@ -183,19 +226,25 @@ export async function run() {
183
226
  }
184
227
  }
185
228
 
186
- if (problems.length > 0) {
187
- return { status: 'fail', notes: [], problems };
188
- }
189
-
190
229
  const asked = process.env.DSH_WB_ONLY === 'kit';
191
230
  const face = faceNotes(asked, shells);
231
+ const payload = face.payload === undefined ? {} : { payload: face.payload };
232
+
233
+ if (problems.length > 0) {
234
+ // 清单是**事实**、红是**判据**:判据红不该把清单一起扣下。`--json` 的消费者(工坊那把
235
+ // `workbench_diy_components`)要靠这份机器档回答"这份外壳有哪些件",而判据红经常是**别的
236
+ // 模块**的事 —— 2026-10-20 真机:三个模块用了 MockStrip / useFixturePick,工具整条查询直接
237
+ // 报错、agent 什么也没拿到。退出码照旧 1(红没被藏起来),problems 也一起进机器档。
238
+ const extra = face.payload === undefined ? {} : { payload: { ...face.payload, problems } };
239
+ return { status: 'fail', notes: [], problems, ...extra };
240
+ }
192
241
 
193
242
  if (inspected === 0) {
194
243
  const why =
195
244
  modulePackages.length === 0
196
245
  ? '没有模块包(没有声明 dsh.workbench 的包)—— 没有产物可对'
197
246
  : `${noArtifact} 个还没建产物(跳过)`;
198
- return { status: 'skip', notes: [why, ...face], problems: [] };
247
+ return { status: 'skip', notes: [why, ...face.notes], problems: [], ...payload };
199
248
  }
200
249
 
201
250
  const notes = [
@@ -204,5 +253,5 @@ export async function run() {
204
253
  ];
205
254
  if (noArtifact > 0) notes.push(`${noArtifact} 个还没建产物(跳过)`);
206
255
  if (noShell > 0) notes.push(`${noShell} 个没装外壳(跳过面差)`);
207
- return { status: 'pass', notes: [...notes, ...face], problems: [] };
256
+ return { status: 'pass', notes: [...notes, ...face.notes], problems: [], ...payload };
208
257
  }