@dsh-workbench/module-tools 0.3.11 → 1.0.0-beta.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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -177
  3. package/bin/dsh-wb-module.mjs +89 -34
  4. package/package.json +18 -3
  5. package/src/arch.mjs +158 -129
  6. package/src/build.mjs +6 -30
  7. package/src/changelog.mjs +167 -0
  8. package/src/check-flags.mjs +0 -7
  9. package/src/check.mjs +115 -53
  10. package/src/checks/alias.mjs +0 -9
  11. package/src/checks/arch.mjs +3 -25
  12. package/src/checks/bundles.mjs +2 -34
  13. package/src/checks/cards.mjs +26 -30
  14. package/src/checks/contracts.mjs +54 -219
  15. package/src/checks/kit.mjs +2 -42
  16. package/src/checks/lint.mjs +9 -82
  17. package/src/checks/markers.mjs +48 -17
  18. package/src/checks/primitives.mjs +0 -39
  19. package/src/checks/protocol.mjs +0 -18
  20. package/src/checks/source.mjs +103 -51
  21. package/src/checks/standalone.mjs +4 -14
  22. package/src/checks/style.mjs +71 -241
  23. package/src/css-injection.mjs +0 -11
  24. package/src/css-shell.mjs +3 -31
  25. package/src/datasource-file.mjs +104 -0
  26. package/src/declared-tools.mjs +0 -24
  27. package/src/design.mjs +116 -38
  28. package/src/discover.mjs +2 -10
  29. package/src/font-scale.mjs +1 -61
  30. package/src/init.mjs +2 -17
  31. package/src/lint.mjs +24 -144
  32. package/src/market-index.mjs +569 -0
  33. package/src/market-init.mjs +80 -0
  34. package/src/primitives-surface.mjs +2 -28
  35. package/src/publish.mjs +6 -24
  36. package/src/release-changelog.mjs +29 -0
  37. package/src/release-journal.mjs +131 -0
  38. package/src/release-report.mjs +99 -0
  39. package/src/release-util.mjs +80 -0
  40. package/src/release.mjs +1251 -0
  41. package/src/seed.mjs +0 -25
  42. package/src/shell-type-files.mjs +3 -30
  43. package/src/sync.mjs +41 -24
  44. package/template/AGENTS.md.tpl +33 -0
  45. package/template/CHANGELOG.md.tpl +7 -0
  46. package/template/README.md.tpl +6 -33
  47. package/template/package.json.tpl +2 -2
  48. package/template/src/client.tsx.tpl +1 -18
  49. package/template/src/host.ts.tpl +2 -15
  50. package/template/types/globals.d.ts +1 -14
  51. package/template-diy/AGENTS.md.tpl +33 -0
  52. package/template-diy/CHANGELOG.md.tpl +7 -0
  53. package/template-diy/README.md.tpl +4 -133
  54. package/template-diy/package.json.tpl +2 -2
  55. package/template-diy/src/client.tsx.tpl +1 -29
  56. package/template-diy/src/host.ts.tpl +1 -15
  57. package/template-diy/src/plugin.test.ts.tpl +1 -1
  58. package/template-diy/types/globals.d.ts +1 -14
  59. package/template-diy/types/styles.d.ts +0 -10
package/src/build.mjs CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
2
  import { createHash } from 'node:crypto';
4
3
  import { builtinModules } from 'node:module';
@@ -11,9 +10,6 @@ const SHELL_CLIENT = '@dsh-workbench/core/client';
11
10
  const RESOLVE_EXTENSIONS = ['.ts', '.tsx', '.js', '.json'];
12
11
  const SINGLETON = /^@\/shared\/(hooks|components)\//;
13
12
 
14
- // dsh 的客户端模块表里"天生就有"的包(seed 词)。模块的浏览器产物**默认**把它们标成外部:
15
- // 这些是外壳那一份实例,内联进来就是第二份 —— 两份 React 组件树、两套样式、hover 与浮层失联。
16
- // 名单在 `../seed.mjs`(唯一真相);判据侧由 `checks/source.mjs` 与 `checks/primitives.mjs` 守。
17
13
  const SEED_EXTERNAL = SEED_SPECIFIERS;
18
14
 
19
15
  function pickEntry(root, names) {
@@ -48,8 +44,7 @@ function aliasResolver({ root, unit }) {
48
44
  `模块自己的文件用 @/modules/${unit ?? '<unit>'}/… 或相对路径`,
49
45
  );
50
46
  }
51
- // `@/…/X.module.css` 这类:把解析结果直接交给 CSS 落点(namespace 由 cssInjector 接管),
52
- // 不要按 .ts/.tsx 去找 —— 否则别名插件的候选表把样式文件挡在外面。
47
+
53
48
  if (base.endsWith('.css') && existsSync(base)) {
54
49
  return { path: base, namespace: 'dsh-wb-css' };
55
50
  }
@@ -71,15 +66,7 @@ function cssInjector({ root }) {
71
66
  namespace: 'dsh-wb-css',
72
67
  }));
73
68
  context.onLoad({ filter: /.*/, namespace: 'dsh-wb-css' }, (args) => {
74
- // 来自被 external 的那份 kit(@dsh-workbench/shared)的样式一律不收:外壳运行时自己会注入
75
- // 那一份,产物里再带一份就变成"两份 kit 样式按选择器混合"(2026-10-18 实测:模块内联了 145
76
- // 条外壳规则、冻结构建时的版本,与运行的外壳混用 ⇒ 折行 / 塌列 / 标签压值)。见 ADR-0028 §5.1。
77
- // 两条路径都要认:
78
- // · 装了依赖的模块仓 —— `node_modules/@dsh-workbench/{shared,core}/…`(pnpm 的 store 里也含这段);
79
- // · 本仓自己开发时 —— `packages/{shared,workbench}/dist/…`。**是 dist 不是 src**:esbuild 默认
80
- // 跟随软链(`preserveSymlinks:false`),workspace 的 `node_modules/@dsh-workbench/shared`
81
- // 软链会被解成真实源码包路径,而 `exports."."` 指的是编译产物 `dist/index.js`,组件样式
82
- // 与它同住 `dist/components/**`(只写 `src` 会在本仓场景漏收:实测 148 条规则照进产物)。
69
+
83
70
  const outsourcedKitCss =
84
71
  /node_modules\/@dsh-workbench\/(shared|core)\//.test(args.path) ||
85
72
  /\/packages\/(shared|workbench)\/(src|dist)\//.test(args.path);
@@ -87,16 +74,12 @@ function cssInjector({ root }) {
87
74
  const raw = readFileSync(args.path, 'utf8');
88
75
  const key = relative(root, args.path).split('/').reverse()[0] + ':' + args.path;
89
76
  const scoped = /\.module\.css$/.test(args.path);
90
- // `*.module.css` 按 CSS Modules 处理:类名哈希后写回 CSS,同时把映射交给调用点
91
- // (`import styles from './X.module.css'` → `styles.foo`)。哈希取"模块内相对路径",
92
- // 同一个类在同一次构建里稳定,跨模块不同名。非 module 的 .css 原样注入(依赖里的就是这种)。
77
+
93
78
  let css = raw;
94
79
  let map = '{}';
95
80
  if (scoped) {
96
81
  const hash = createHash('sha1').update(relative(root, args.path)).digest('hex').slice(0, 6);
97
- // `:global(...)` 里的类名**不许哈希** —— 那是外壳的契约类(比如 `.wb-iconButton`),
98
- // 模块借用它时只能这样写:`.wb-envRefresh:global(.wb-iconButton)`。先把这些片段挖出来存着,
99
- // 哈希完再放回去。
82
+
100
83
  const globals = [];
101
84
  const guarded = raw.replace(/:global\([^)]*\)/g, (piece) => {
102
85
  globals.push(piece.slice(':global('.length, -1));
@@ -105,9 +88,7 @@ function cssInjector({ root }) {
105
88
  const names = [
106
89
  ...new Set([...guarded.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)].map((match) => match[1])),
107
90
  ].sort((a, b) => b.length - a.length);
108
- // 类名**不能以数字开头**:哈希是十六进制,直接接在 `.` 后面会写出 `.3a0f86_x` 这种
109
- // 非法选择器 —— 浏览器会把整条规则丢掉(实测:20 条规则只解析出 1 条,界面整屏掉样式)。
110
- // 前面加一个下划线,保持 CSS Modules 的老习惯(`._3a0f86_x`)。
91
+
111
92
  const pairs = names.map((name) => [name, `_${hash}_${name}`]);
112
93
  css = guarded;
113
94
  for (const [from, to] of pairs) css = css.split(`.${from}`).join(`.${to}`);
@@ -169,9 +150,6 @@ export async function build({ root }) {
169
150
  logLevel: 'warning',
170
151
  });
171
152
 
172
- // 物证:metafile 列出**这次真的被打进包里**的每个输入文件。dsh 的包只要出现,
173
- // 就说明它没走外部化(声明漏了 / 名字写错 / 从 shared 内联进来)—— 那正是"第二份实例"。
174
- // 不靠产物里的字符串去猜(tree-shaking 之后标记可能根本不进包),也不靠它自己报错。
175
153
  const inlinedDsh = Object.keys(client.metafile.inputs).filter((path) => /@deepseek-ai[\\/]/.test(path));
176
154
  if (inlinedDsh.length > 0) {
177
155
  throw new Error(
@@ -188,8 +166,6 @@ export async function build({ root }) {
188
166
  const wrapped = `// ⚠️ 本文件由 dsh-wb-module build 生成,请勿手改。改 src/ 后跑:dsh-wb-module build\nwindow.__ModuleLoader__.load({\n id: ${JSON.stringify(id)},\n factory(require) {\n const module = { exports: {} };\n const exports = module.exports;\n${indented}\n return module.exports;\n },\n});\n`;
189
167
 
190
168
  writeFileSync(join(root, 'client.js'), wrapped);
191
- // `wrapped.length` 是 **UTF-16 码元数**(字符数),不是字节数 —— 产物里有中文头注释与模块自己的
192
- // 中文文案,报出来会比真实文件小(实测 `module-hello/client.js`:3095 字符 / 3131 字节)。
193
- // 报字节就按字节算。
169
+
194
170
  console.log(`${id}:client.js 已更新(${Buffer.byteLength(wrapped, 'utf8')} 字节)`);
195
171
  }
@@ -0,0 +1,167 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ import {
5
+ CHANGELOG_FILE,
6
+ CHANGELOG_KINDS,
7
+ appendChangelogEntry,
8
+ changelogDate,
9
+ changelogHasEntry,
10
+ changelogSkeleton,
11
+ changelogVersion,
12
+ } from '@dsh-workbench/shared/host';
13
+
14
+ const SEMVER = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
15
+
16
+ const USAGE = `用法:
17
+ changelog init 建一份 ${CHANGELOG_FILE}(已有就不动)
18
+ changelog add <版本> [--type ${CHANGELOG_KINDS.join('|')}] [--note <文字>] [--date <日期>]
19
+ changelog check [版本] 查这一版有没有记录(省略版本用 package.json 的)
20
+ 版本也可以省略:用 package.json 里的 version。发布时这一版必须有记录,否则拦下。`;
21
+
22
+ function argValue(args, flag) {
23
+ const at = args.indexOf(flag);
24
+ return at === -1 ? undefined : args[at + 1];
25
+ }
26
+
27
+ function positional(args, at) {
28
+ const value = args[at];
29
+ return value === undefined || value.startsWith('--') ? undefined : value;
30
+ }
31
+
32
+ export function changelogFileOf(root) {
33
+ return join(root, CHANGELOG_FILE);
34
+ }
35
+
36
+ export function readChangelogText(root) {
37
+ try {
38
+ return readFileSync(changelogFileOf(root), 'utf8');
39
+ } catch {
40
+ return '';
41
+ }
42
+ }
43
+
44
+ export function manifestVersionOf(root) {
45
+ try {
46
+ const version = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version;
47
+ return typeof version === 'string' ? version.trim() : '';
48
+ } catch {
49
+ return '';
50
+ }
51
+ }
52
+
53
+ export function changelogProblemOf(root, version) {
54
+ const text = readChangelogText(root);
55
+ if (text.trim() === '') {
56
+ return {
57
+ reason: `包里还没有 ${CHANGELOG_FILE} —— 更新日志要随包发布`,
58
+ command: 'dsh-wb-module changelog init',
59
+ };
60
+ }
61
+ if (!changelogHasEntry(text, version)) {
62
+ return {
63
+ reason: `${CHANGELOG_FILE} 里没有 ${version} 这一版的内容 —— 发布前先把这一版写进去`,
64
+ command: `dsh-wb-module changelog add ${version} --note "这一版改了什么"`,
65
+ };
66
+ }
67
+ return undefined;
68
+ }
69
+
70
+ export function initChangelog(root) {
71
+ const file = changelogFileOf(root);
72
+ if (existsSync(file)) return { file, created: false };
73
+ writeFileSync(file, changelogSkeleton());
74
+ return { file, created: true };
75
+ }
76
+
77
+ export class ChangelogError extends Error {
78
+ constructor(message, code = 2) {
79
+ super(message);
80
+ this.name = 'ChangelogError';
81
+ this.code = code;
82
+ }
83
+ }
84
+
85
+ export function addChangelog(root, { version, kind = 'changed', note, date }) {
86
+ const text = (note ?? '').trim();
87
+ if (text === '') throw new ChangelogError('changelog add 要一句 --note:这一版改了什么');
88
+ if (!CHANGELOG_KINDS.includes(kind)) {
89
+ throw new ChangelogError(`--type 只认 ${CHANGELOG_KINDS.join(' / ')}(收到「${kind}」)`);
90
+ }
91
+ const wanted = changelogVersion(version === undefined || version === '' ? manifestVersionOf(root) : version);
92
+ if (!SEMVER.test(wanted)) {
93
+ throw new ChangelogError(`版本要写成 x.y.z(收到「${String(version)}」)—— 也可以省略,用 package.json 里的`);
94
+ }
95
+ const file = changelogFileOf(root);
96
+ const body = appendChangelogEntry(readChangelogText(root), { version: wanted, kind, note: text, date });
97
+ writeFileSync(file, body);
98
+ return { file, version: wanted, kind };
99
+ }
100
+
101
+ function runInit(root, args) {
102
+ const result = initChangelog(root);
103
+ if (args.includes('--json')) {
104
+ console.log(JSON.stringify({ ok: true, ...result }));
105
+ return;
106
+ }
107
+ if (!result.created) {
108
+ console.log(`${CHANGELOG_FILE} 已经在:${result.file} —— 没动它`);
109
+ return;
110
+ }
111
+ console.log(`✔ 建好了 ${result.file}`);
112
+ console.log(` 下一步:dsh-wb-module changelog add ${manifestVersionOf(root) || '<版本>'} --note "这一版改了什么"`);
113
+ }
114
+
115
+ function runAdd(root, args) {
116
+ const version = positional(args, 1) ?? manifestVersionOf(root);
117
+ const kind = argValue(args, '--type') ?? 'changed';
118
+ const note = argValue(args, '--note') ?? '';
119
+ const date = (argValue(args, '--date') ?? changelogDate()).trim();
120
+ let result;
121
+ try {
122
+ result = addChangelog(root, { version, kind, note, date });
123
+ } catch (error) {
124
+ if (!(error instanceof ChangelogError)) throw error;
125
+ console.error(error.message);
126
+ process.exit(error.code);
127
+ }
128
+ if (args.includes('--json')) {
129
+ console.log(JSON.stringify({ ok: true, ...result, date }));
130
+ return;
131
+ }
132
+ console.log(`✔ ${CHANGELOG_FILE} 已记下 ${result.version}(${result.kind})`);
133
+ }
134
+
135
+ function runCheck(root, args) {
136
+ const version = changelogVersion(positional(args, 1) ?? manifestVersionOf(root));
137
+ if (!SEMVER.test(version)) {
138
+ console.error('changelog check 要知道查哪一版:给一个 x.y.z,或先让 package.json 有 version');
139
+ process.exit(2);
140
+ }
141
+ const problem = changelogProblemOf(root, version);
142
+ if (args.includes('--json')) {
143
+ console.log(
144
+ JSON.stringify(
145
+ problem === undefined
146
+ ? { ok: true, version }
147
+ : { ok: false, version, reason: problem.reason, command: problem.command },
148
+ ),
149
+ );
150
+ } else if (problem === undefined) {
151
+ console.log(`✔ ${CHANGELOG_FILE} 里有 ${version} 的更新日志`);
152
+ } else {
153
+ console.error(`✖ ${problem.reason}`);
154
+ console.error(` 补上:${problem.command}`);
155
+ }
156
+ if (problem !== undefined) process.exit(1);
157
+ }
158
+
159
+ export async function changelog({ root, args = [] }) {
160
+ const sub = args[0];
161
+ if (sub === 'init') return runInit(root, args);
162
+ if (sub === 'add') return runAdd(root, args);
163
+ if (sub === 'check') return runCheck(root, args);
164
+ console.error(`不认识的 changelog 子命令:${String(sub)}`);
165
+ console.error(USAGE);
166
+ process.exit(2);
167
+ }
@@ -1,9 +1,3 @@
1
- // `dsh-wb-module check --only <名字>` 单跑时透传的开关。
2
- //
3
- // 聚合层只认 `--only` / `--cwd`,其余开关(`--all` / `--files` / `--json`)原样经
4
- // `DSH_WB_ONLY_ARGS`(JSON 数组)交给**单跑的那一条** —— 这是 `check.mjs` 的加法之一
5
- // (给 `run()` 加参数会让"所有判据都是无参 `run()`"这条统一形状多一个例外)。
6
- // 名字只有这一处写,别把环境变量名抄成第二份真相。
7
1
  export function flagsOf() {
8
2
  try {
9
3
  const parsed = JSON.parse(process.env.DSH_WB_ONLY_ARGS ?? '[]');
@@ -13,7 +7,6 @@ export function flagsOf() {
13
7
  }
14
8
  }
15
9
 
16
- /** `--file <名字>` 那种带值的开关取它的值(没给值返回 undefined)。 */
17
10
  export function flagValue(flags, flag) {
18
11
  const at = flags.indexOf(flag);
19
12
  if (at === -1) return undefined;
package/src/check.mjs CHANGED
@@ -1,55 +1,116 @@
1
- // 判据的**聚合层**:跑、收集三态、打印、决定退出码。有几条以 `CHECKS` 表为准。
2
- // ── 为什么判据改成"导出函数",打印与退出码都搬到这里 ──────────────────────────
3
- //
4
- // 在这之前,每条 `checks/*.mjs` 都是"import 即执行 + 自己 console.log + 自己 process.exit"
5
- // 的脚本。那样"不适用"就没有一等形状 —— 只能靠"这条判据不许 exit(0)"这条**纪律**维持:
6
- // 谁哪天写个 `process.exit(0)`,排在它后面的判据就静默不跑,而屏幕看着还是全绿
7
- // (standalone 在仓外工作区就是这么吞掉 markers / style / alias 三条的,见
8
- // `.scratch/workbench-diy/spec.md` §20)。
9
- //
10
- // 另一件后果是聚合层拿不到"跑了哪些、跳了哪些、为什么跳",只能看判据自己打的字。
11
- //
12
- // 新形状:每条判据导出 `run()`,返回
13
- // { status: 'pass' | 'fail' | 'skip', notes: string[], problems: string[] }
14
- // ——"不适用"于是是一等结果,而**截断在形状上就不可能**:
15
- // 判据手里没有 exit,退出码只有这一层按汇总给。
16
- //
17
- // 打印形状(三态,稳定):
18
- // 通过 ✔ <title>:<notes 拼起来>
19
- // 不适用 ○ <title>:(不适用)<notes>
20
- // 失败 ✖ <title> <N> 条不过: 然后逐条 ` · <problem>`
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
- // 这是加法,不是新形状。
31
-
32
- // 名字与 title 一字不变 —— selfcheck 会把它与各判据模块导出的 `title` 逐个对一遍。
33
1
  export const CHECKS = {
34
- contracts: { title: '模块契约', load: () => import('./checks/contracts.mjs') },
35
- protocol: { title: '模块声明协议', load: () => import('./checks/protocol.mjs') },
36
- bundles: { title: '模块产物边界', load: () => import('./checks/bundles.mjs') },
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') },
40
- primitives: { title: 'dsh 原语只从模块表拿', load: () => import('./checks/primitives.mjs') },
41
- kit: { title: '外壳 kit 面', load: () => import('./checks/kit.mjs') },
42
- cards: { title: '卡片渲染', load: () => import('./checks/cards.mjs') },
43
- standalone: { title: '独立构建', load: () => import('./checks/standalone.mjs') },
44
- markers: { title: '公开包无内部标识', load: () => import('./checks/markers.mjs') },
45
- style: { title: '样式归属', load: () => import('./checks/style.mjs') },
46
- alias: { title: '模块不指外壳', load: () => import('./checks/alias.mjs') },
2
+ contracts: {
3
+ title: '模块契约',
4
+ summary: '单元 id、两个面、四个脚本、files、peer 下限,以及清单与代码注册对账',
5
+ layer: 'manifest',
6
+ load: () => import('./checks/contracts.mjs'),
7
+ },
8
+ protocol: {
9
+ title: '模块声明协议',
10
+ summary: 'dsh.workbench 的 apiVersion、静态半形状与未知键',
11
+ layer: 'manifest',
12
+ load: () => import('./checks/protocol.mjs'),
13
+ },
14
+ bundles: {
15
+ title: '模块产物边界',
16
+ summary: '宿主产物不含浏览器侧、浏览器产物不内联外壳 CSS 与夹具',
17
+ layer: 'build',
18
+ load: () => import('./checks/bundles.mjs'),
19
+ },
20
+ source: {
21
+ title: '模块源码边界与零注释',
22
+ summary: '零注释、包与运行时入口分开、不手写进程/锁/原子写',
23
+ layer: 'source',
24
+ load: () => import('./checks/source.mjs'),
25
+ },
26
+ arch: {
27
+ title: '模块架构(分层 / 边界 / 设计系统)',
28
+ summary: '模块自己的层序、运行时边界与设计系统配置',
29
+ layer: 'source',
30
+ load: () => import('./checks/arch.mjs'),
31
+ },
32
+ lint: {
33
+ title: '模块代码卫生(eslint)',
34
+ summary: '模块自己的 eslint.config.mjs 与规则集',
35
+ layer: 'source',
36
+ load: () => import('./checks/lint.mjs'),
37
+ },
38
+ primitives: {
39
+ title: 'dsh 原语只从模块表拿',
40
+ summary: 'dsh 包只从 kit 取,模块自己声明 external',
41
+ layer: 'build',
42
+ load: () => import('./checks/primitives.mjs'),
43
+ },
44
+ kit: {
45
+ title: '外壳 kit 面',
46
+ summary: '模块产物对得上所装外壳的 kit 面与 peer 下限',
47
+ layer: 'build',
48
+ load: () => import('./checks/kit.mjs'),
49
+ },
50
+ cards: {
51
+ title: '卡片渲染',
52
+ summary: '八态 × zh/en 渲染:不抛、不空白、不冒裸键名',
53
+ layer: 'render',
54
+ load: () => import('./checks/cards.mjs'),
55
+ },
56
+ standalone: {
57
+ title: '独立构建',
58
+ summary: '外壳源码不在场时仍能 build + typecheck',
59
+ layer: 'build',
60
+ load: () => import('./checks/standalone.mjs'),
61
+ },
62
+ markers: {
63
+ title: '公开包无内部标识',
64
+ summary: '公开包不含私有源、私网 IP、内部域名后缀',
65
+ layer: 'meta',
66
+ load: () => import('./checks/markers.mjs'),
67
+ },
68
+ style: {
69
+ title: '样式归属',
70
+ summary: '不定义 wb-* 规则;裸标签控件改走 kit;令牌名对现读的清单校验',
71
+ layer: 'source',
72
+ load: () => import('./checks/style.mjs'),
73
+ },
74
+ alias: {
75
+ title: '模块不指外壳',
76
+ summary: '模块源码里没有指向外壳 @/ 的引用',
77
+ layer: 'source',
78
+ load: () => import('./checks/alias.mjs'),
79
+ },
47
80
  };
48
81
 
49
- /** 判据只许回这三态;聚合层的打印与汇总都按它分支。 */
82
+ export function checkList() {
83
+ return Object.entries(CHECKS).map(([name, entry]) => ({
84
+ name,
85
+ title: entry.title,
86
+ summary: entry.summary,
87
+ layer: entry.layer,
88
+ }));
89
+ }
90
+
91
+ export function renderCheckTable(rows = checkList()) {
92
+ return [
93
+ '| 判据 | 层 | 摘要 |',
94
+ '|---|---|---|',
95
+ ...rows.map((row) => `| \`${row.name}\` | ${row.layer} | ${row.summary} |`),
96
+ ].join('\n');
97
+ }
98
+
99
+ function list(args) {
100
+ const rows = checkList();
101
+ if (args.includes('--json')) {
102
+ console.log(JSON.stringify(rows, null, 2));
103
+ return;
104
+ }
105
+ if (args.includes('--markdown')) {
106
+ console.log(renderCheckTable(rows));
107
+ return;
108
+ }
109
+ for (const row of rows) console.log(`${row.name}\t${row.layer}\t${row.title}\t${row.summary}`);
110
+ }
111
+
50
112
  const STATES = ['pass', 'fail', 'skip'];
51
113
 
52
- /** 一屏一条:三态各一种前缀,notes 的第一条跟在标题后面,其余各占一行。 */
53
114
  function report(title, result) {
54
115
  const notes = Array.isArray(result.notes) ? result.notes : [];
55
116
  if (result.status === 'fail') {
@@ -68,6 +129,10 @@ function report(title, result) {
68
129
  }
69
130
 
70
131
  export async function check({ only, args = [] } = {}) {
132
+ if (args.includes('--list')) {
133
+ list(args);
134
+ return;
135
+ }
71
136
  const names = only === undefined ? Object.keys(CHECKS) : [only];
72
137
  const jsonMode = args.includes('--json');
73
138
  const tally = { pass: 0, skip: 0, fail: 0 };
@@ -81,12 +146,11 @@ export async function check({ only, args = [] } = {}) {
81
146
  console.error(`不认识的判据:${name}(有:${Object.keys(CHECKS).join(' / ')})`);
82
147
  process.exit(2);
83
148
  }
84
- // 告诉这一条"这次是不是只跑它":有的判据在单跑时顺带当查询口(`--only kit` 会把
85
- // 这份外壳的 kit 成员列出来),聚合跑时那些清单属于噪音,不该倒。
149
+
86
150
  process.env.DSH_WB_ONLY = only === undefined ? '' : name;
87
151
  const module = await entry.load();
88
152
  const result = await module.run();
89
- // title 的发声处是判据模块自己;表里那份是兜底(两边必须一字不差,selfcheck 钉着)。
153
+
90
154
  const title = typeof module.title === 'string' && module.title !== '' ? module.title : entry.title;
91
155
  ran += 1;
92
156
  if (!STATES.includes(result.status)) {
@@ -108,15 +172,13 @@ export async function check({ only, args = [] } = {}) {
108
172
  process.env.DSH_WB_ONLY_ARGS = '';
109
173
  }
110
174
 
111
- // `--json` 的 stdout 只有那几份 payload:汇总行与"不适用"那几段都是人话,不进机器档。
112
175
  if (jsonMode) {
113
176
  if (tally.fail > 0) process.exit(1);
114
177
  return;
115
178
  }
116
179
 
117
180
  console.log(`${ran} 条判据:${tally.pass} 通过 · ${tally.skip} 不适用 · ${tally.fail} 不过`);
118
- // 「不适用」不是「通过」。汇总行里那两个数字长得很像,读的人(尤其 agent)很容易把
119
- // "8 通过 · 2 不适用" 念成"全过了" —— 而工坊里那两条是**结构性**跑不了,不是这次没问题。
181
+
120
182
  if (skipped.length > 0) {
121
183
  console.log(` 这 ${skipped.length} 条这次一条都没查(不是"没问题"):`);
122
184
  for (const one of skipped) console.log(` ○ ${one}`);
@@ -1,12 +1,3 @@
1
- // 模块的源码只许引**自己的** `@/modules/<unit>/*`,不许出现任何指向外壳的 `@/`。
2
- //
3
- // 为什么:模块出包后用户手上没有外壳源码,`@/app/...` 这类别名只在**本仓的构建配置**里
4
- // 解析得到 —— 装到别人那里就是构建期找不到文件(`check:standalone` 会把它变成红灯,
5
- // 这条判据则直接指到源码那一行)。
6
- //
7
- // 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
8
- // 打印与退出码归 `../check.mjs` 那一层。
9
-
10
1
  import { readdirSync, readFileSync, statSync } from 'node:fs';
11
2
  import { join, relative } from 'node:path';
12
3
  import { discoverModulePackages } from '../discover.mjs';
@@ -1,20 +1,3 @@
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
1
  import { existsSync } from 'node:fs';
19
2
  import { join } from 'node:path';
20
3
  import { builtinSourceForms, runGuard } from '@arch-guard/core';
@@ -24,7 +7,6 @@ export const title = '模块架构(分层 / 边界 / 设计系统)';
24
7
 
25
8
  const CONFIG_FILE = 'arch.config.mjs';
26
9
 
27
- // "怎么上车"那句话只写一遍:skip 的提示与 fail 的提示都要它。
28
10
  const HOW_TO_JOIN =
29
11
  `在模块包里放一份 ${CONFIG_FILE},内容一行就够:\n` +
30
12
  ` import { moduleArch } from '@dsh-workbench/module-tools/arch';\n` +
@@ -36,7 +18,6 @@ const severityIndex = (rules) => new Map(rules.map((rule) => [rule.id, rule.seve
36
18
 
37
19
  const severityOf = (finding, index) => finding.severity ?? index.get(finding.rule) ?? 'error';
38
20
 
39
- /** 一条 finding 翻成一行人话:位置 → 说明 → 规则与修法。 */
40
21
  function lineOf(name, finding) {
41
22
  const where = `${name}: ${finding.file}:${String(finding.line)}`;
42
23
  const hint = typeof finding.hint === 'string' && finding.hint !== '' ? ` —— ${finding.hint}` : '';
@@ -68,7 +49,7 @@ export async function run() {
68
49
  const problems = [];
69
50
  let files = 0;
70
51
  let warnings = 0;
71
- // 真的跑过的规则 id(与 arch-guard 报告头那句"规则 58/107"同一个口径:`stats` 是逐规则执行统计)。
52
+
72
53
  const ranRules = new Set();
73
54
 
74
55
  for (const pkg of declared) {
@@ -77,8 +58,7 @@ export async function run() {
77
58
  try {
78
59
  result = await runGuard({
79
60
  cwd: pkg.dir,
80
- // **相对**路径:arch-guard 把 configPath 拼在 cwd 后面(实测传绝对路径会拼成
81
- // `<cwd>/<绝对路径>`,再报"找不到配置文件")。
61
+
82
62
  configPath: CONFIG_FILE,
83
63
  sourceForms: builtinSourceForms,
84
64
  cache: false,
@@ -94,8 +74,6 @@ export async function run() {
94
74
  continue;
95
75
  }
96
76
 
97
- // `scopeFiles` 在 full 档是空的(实测)—— 解析过的文件数从 facts 缓存那份统计里拿
98
- // (`cache: false` 时 `misses` 就是这一轮解析了几个文件,见 arch-guard 的 RunResult 注释)。
99
77
  files += result.cache.misses;
100
78
  for (const stat of result.stats) ranRules.add(stat.rule);
101
79
  const index = severityIndex(result.config.rules);
@@ -108,7 +86,7 @@ export async function run() {
108
86
  if (problems.length > 0) {
109
87
  return {
110
88
  status: 'fail',
111
- // 提示放在 notes 里,判据层照旧只报"哪几条不过"。
89
+
112
90
  notes: [`红了怎么修:先看位置与规则号,再决定"改代码"还是"在 ${CONFIG_FILE} 里开口子"。`],
113
91
  problems,
114
92
  };
@@ -1,22 +1,3 @@
1
- // 模块包的**运行时边界**(S2 第二块:门禁跟着自述清单走,覆盖到模块包)。
2
- //
3
- // 判据与外壳 `packages/workbench/scripts/check-runtimes.mjs` 是同一条:**能力,不是目录名**。
4
- // 外壳那份走的是 vite 的 import 图;这里走**产物** —— 模块的两个入口各打成一个文件,
5
- // 越界的依赖一定会被 inline 进来,所以:
6
- //
7
- // 浏览器产物里出现 node 内置 = 宿主传输层混进来了(例如某个视图 import 了 host/opencli)
8
- // 宿主产物里出现 DOM / react = 浏览器侧混进来了
9
- //
10
- // 判据要挑**只有宿主侧才有的能力**:node 内置、`process.env`、`Buffer`。
11
- // 这类错误在源码里看目录名是看不出来的(`model/blockers.ts` 这种纯函数浏览器当然可以用),
12
- // 所以只能按能力判 —— 而产物就是能力的物证。
13
- //
14
- // 覆盖范围跟着**自述清单**走:凡 `package.json` 里声明了 `dsh.workbench` 的包都被检查,
15
- // 新加一个模块不用改这个脚本。
16
- //
17
- // 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
18
- // 打印与退出码归 `../check.mjs` 那一层。
19
-
20
1
  import { existsSync, readFileSync } from 'node:fs';
21
2
  import { join } from 'node:path';
22
3
  import { inlinedShellRules } from '../css-shell.mjs';
@@ -36,13 +17,6 @@ const HOST_FORBIDDEN = [
36
17
 
37
18
  export const title = '模块产物边界';
38
19
 
39
- /**
40
- * 模块产物里的夹具标记 —— 这几串一出现就说明"夹具又长回模块里了"。
41
- *
42
- * 为什么是"串"而不是"符号":模块产物是打包后的 JS,符号名会被内联/保留,用固定串更稳。
43
- * `-mock'` 那一条故意只匹配**带引号的模块路径尾巴**(`from './cmdb-mock'` 这类),
44
- * 避免误伤 vitest 的 `vi.mock`(那在测试里,不进产物)。
45
- */
46
20
  const FIXTURE_MARKERS = [
47
21
  'wbMock',
48
22
  'wbCrash',
@@ -88,10 +62,7 @@ export async function run() {
88
62
  );
89
63
  }
90
64
  }
91
- // 夹具只属于**外壳开发者的调试**(.scratch/dev-only-fixtures/spec.md):外壳自 0.2.12 起
92
- // 把它整棵树挪出发布构建,kit 面也不再提供那几个成员。模块带着夹具只有坏处:
93
- // 取不到旧成员会当场抛(2026-10-17 实测三个模块无条件渲染的包装组件就是这么崩的),
94
- // 而且夹具数据会跟真取数抢同一条渲染路径。这条盯**产物**:出现任何一个标记就红。
65
+
95
66
  if (fixtureMarkersIn(source).length > 0) {
96
67
  const hits = fixtureMarkersIn(source);
97
68
  problems.push(
@@ -100,10 +71,7 @@ export async function run() {
100
71
  '模块自己的 vitest 把几态各喂一遍就够。',
101
72
  );
102
73
  }
103
- // 外壳的组件样式**只有一份**(外壳注入那一次)。模块从 `@dsh-workbench/shared` 的**根入口**
104
- // 取运行期值(`h` / `Fragment` / `useState` / 纯函数…)就会把整份组件 barrel 连 CSS 一起打进
105
- // 自己的产物,注入后按顺序覆盖外壳那一份 —— 用户看到的是旧样式,而且外壳改了也不生效。
106
- // 判断看复合选择器:`._hash_row.wb-iconButton` 这种「自己的类 + 外壳的槽类」是合规的,不算。
74
+
107
75
  if (artifact.file === 'client.js') {
108
76
  const inlined = inlinedShellRules(source);
109
77
  if (inlined.length > 0) {