@dsh-workbench/module-tools 0.3.11 → 1.0.0-beta.1

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 +22 -3
  5. package/src/arch.mjs +158 -129
  6. package/src/build.mjs +6 -30
  7. package/src/changelog.mjs +166 -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 +72 -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 +84 -230
  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 +11 -25
  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 +87 -0
  40. package/src/release.mjs +1250 -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
@@ -1,68 +1,13 @@
1
- // 模块的样式归属:① 不许定义外壳前缀(`wb-*`)的规则;② 借用的 `wb-*` 类必须在契约里;
2
- // ③ `*.module.css` 的绑定不许指向不存在的类。
3
- //
4
- // 为什么:`wb-*` 是**全局**类名,两份同名规则按注入顺序互相覆盖,用户看到的是随机的那个;
5
- // CSS Modules 的哈希类名靠"同一份文件、同一个导出"配对,调用点拿到 undefined 时真机渲染成
6
- // `class="undefined"` —— 元素整片没样式,而且控制台一声不响。
7
- //
8
- // ④ 表单的「行」归 `FormRow(s)`([ADR-0052](../../../../docs/adr/0052-the-form-row-is-one-component.md)):
9
- // 控件有主(`TextField` / `SelectField` / `ChoiceGroup`)、面有主(`SettingsSection` / `SaveBar`),
10
- // 中间那层"标签 + 控件这一行怎么排"从前没有主 ⇒ 每人都自己写一遍:cmdb 一套 `.cmdbField`
11
- // (`display:flex` + `.cmdbFieldLabel{width:72px}`)+ 三档 gap(`.cmdbForm` 8 / `.cmdbEnvs` 8 /
12
- // `.cmdbEnvGroup` 2),argocd 又一套自己的两列 grid。现场就是控件尺寸漂、行距贴住、标签在上还是
13
- // 在下各写各的(ADR-0052 §1 / §7 第 ② 条)。两族判据(F1 手写的标签+控件行 / F2 自己管行距)
14
- // 都很窄 —— **宁可漏报,别把合法的网格/卡片布局判红**;判据的窄法与已知漏报写在各自那一段。
15
- //
16
- // 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
17
- // 打印与退出码归 `../check.mjs` 那一层。
18
-
19
1
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
20
2
  import { dirname, join, relative, resolve } from 'node:path';
21
3
  import { fileURLToPath } from 'node:url';
4
+ import { DSW_PREFIX, dshHome, nearestTokens, readTokens } from '@dsh-workbench/shared/host';
22
5
  import { shellRulesIn } from '../css-shell.mjs';
23
6
  import { discoverModulePackages } from '../discover.mjs';
24
7
  import { auditFontSizes, fontScaleNotes, fontScaleOf } from '../font-scale.mjs';
25
8
 
26
- // 外链一律走 kit 的 `Link`:它自带的样式(正文色 / hover 下划线 / 12px / 那个外链小箭头)是唯一一份。
27
- // 手写 `<a>` / `h('a', …)` 拿到的是浏览器默认样式,而且每个人写的都不一样(这正是"同一个东西样式不一样"的来源)。
28
- //
29
- // **这条是全局匹配(`g`),按全文找再回推行号 —— 与 `bareControlsIn` / `bareFormsIn` 同形,别改回逐行。**
30
- // 为什么(2026-10-21 实测踩到,`.scratch/uiux-ownership/` 候选 1):JSX 里 `<a` 习惯写在**行尾**
31
- // (属性在下一行),而 `<a(?=[\s>/])` 要的那个 `[\s>/]` 在**下一行**;逐行 `.test(line)` 里
32
- // 既没有那个字符、也没有换行本身 ⇒ **行尾写的 `<a` 一处都抓不到**。实测:pduiv2 5 个真模块包里
33
- // 有 **8 处**裸 `<a>`,**8 处全部**写在行尾,逐行那条的命中是 **0** —— 判据与它自己的文档
34
- //(`PANEL.md` §6「手写 `<a>` 会被「样式归属」判据直接判红」)打架了半年。全文匹配后正好 **8 命中**。
35
- // 「一个元素会跨行写属性」这条理由与 `bareFormsIn` 上一行那句是**同一条**,别只给表单那一条用。
36
- //
37
- // **哪种 `<a>` 不算 `Link`(所以不算"绕过",收在 ANCHOR_EXEMPT 里)**:`Link` 的形态只有一种 ——
38
- // **正文里的文字链**(文字 + 行尾那颗 ↗ 小箭头,12px、hover 下划线)。三种是它表达不了的:
39
- // ① **图标链**:`<a className="wb-iconButton">` 里包一枚图标(平台跳转 / 看日志那些)——
40
- // `Link` 一定会多画一颗箭头,套进去就是两颗;
41
- // ② **按钮皮的链**:`.anchorButton` 那种 28 高 / 描边的"看起来像按钮、实际是链接"——
42
- // [ADR-0054](docs/adr/0054-the-action-area-is-one-component.md) §11 判过:**必须是链接的动作留真 `<a>`**,
43
- // 不许塞成 `onClick: () => window.open(…)` 的按钮(那一次会把中键新开 / 右键复制链接 / 悬停看 URL 一起丢掉);
44
- // ③ **整行链**:`.issueLink` 那种 `flex: 1` / `gap` / `color: inherit` 的**行的布局槽**——
45
- // 它是一条行的皮,不是正文里的字。
46
- // 这三类今天在真模块包里有 6 处(argocd 的 EnvRows 两处图标链、coding-analysis 的 AnalysisBody 一处图标链、
47
- // EnvSettings / AnalysisStates 两处按钮皮的链、coding-issues 的 IssueRow 一处整行链),逐文件收在
48
- // `ANCHOR_EXEMPT` 里;**该迁 `Link` 的是另外 2 处**(argocd 的 EnvRows / IdentityFields 那两条「去登录」文字链)。
49
- //
50
- // **两端边界**(与 `CLASS_TOKEN` / `FORM_SLOT_CLASS` 同一套口径,2026-10-21 复核者提醒后补的):
51
- // · **右**:`<a(?=[\s>/])` —— `<abbr>` 不算(`<a` 后面是 `b`),`<aside>` 也不算;
52
- // · **左**:`(?<![\w$])` —— **`<` 前面不许是标识符字符**。挡的是 JS 里那个真实形态:
53
- // `if (count<a ) …`(`count < a` 写掉了空格)今天会命中 `<a ` ⇒ 报一个跟界面无关的红。
54
- // 用 lookbehind(非消费)而不是 `(^|[^\w$])` 那种前缀,是因为这一条要按 `match.index`
55
- // **回推行号**(`bareAnchorsIn`):消费式前缀落在上一行的换行上时,行号会算到上一行去。
56
- // **`h('a'` / `jsx('a'` 那两支也吃同一个左边界**(整组一起套),所以 `myh('a', …)` 不算。
57
- // **仍然红的、如实记**:`<a` 写在**字符串**里(例如提示文案里那句 `<a href=…>`)—— 与 `BARE_FORM` 那条
58
- // 一样是**文本级**匹配,这是取舍不是漏报(要收紧先改口径,判据不自己长)。
59
9
  const BARE_ANCHOR = /(?<![\w$])(?:h\(\s*['"]a['"]|jsx\(\s*['"]a['"]|<a(?=[\s>/]))/g;
60
10
 
61
- // 交互控件同一条理由:按钮 / 单行输入 / 下拉在 kit 与 dsh 原语里都有对应形态
62
- // (`Button` · `TextField` · `SelectField` / `ChoiceGroup`),手写的那一份拿到的是浏览器默认样式 ——
63
- // 悬停、禁用、焦点环、色板、字号各写一遍,不跟主题也不跟语言。
64
- // 例外只有两个,都是"两处都没有的形态":多行输入(`<textarea>`,dsh 与 kit 都没有)与
65
- // `input` 的 `type="hidden"` / `type="file"`(不是标准控件,拿不到样式)。这两个放行。
66
11
  const BARE_CONTROLS = [
67
12
  { tag: 'button', what: '按钮', use: 'dsh 的 Button(variant / size),或 kit 的 SaveBar / BlockerState' },
68
13
  { tag: 'input', what: '单行输入', use: 'kit 的 TextField' },
@@ -70,99 +15,28 @@ const BARE_CONTROLS = [
70
15
  ];
71
16
  const NATIVE_ONLY_INPUT = /type\s*[:=]\s*['"](?:hidden|file)['"]/;
72
17
 
73
- // 历史豁免:**已知**的手写控件,各有各的理由,跟着各自的迁移走 ——
74
- // ①(ADR-0051 §4 那次)`module-cmdb/src/views/MenuField.tsx` 的两处手写钮已经迁完
75
- // (2026-10-17,外壳发到 0.2.12 之后;cmdb `a1a37e60` / argocd `d85627a7`),那一行按棘轮删掉了 ——
76
- // 留着这段是因为它就是这条纪律的由来:模块的 peer 下限得先有 `SelectField`,否则装上就崩。
77
- // ②(ADR-0052 §10 那次)`module-cmdb/src/views/ChooseEnvsBody.tsx` 的 `role="radio"` 选项行
78
- // 已经迁 `ChoiceGroup`(2026-10-17,外壳 0.2.13 之后),这一行同样按棘轮删掉了。
79
- // 这是**棘轮**:迁完一处就删一行,别只加不减(名单长起来等于这条判据没有)。
80
- // 键是 `<包名>:<相对模块根的文件路径>`。
81
18
  const CONTROL_EXEMPT = new Set([]);
82
19
 
83
- // ── F1「手写的标签+控件行」与 F2「自己管行距」:都该走 FormRow(s)(ADR-0052) ──────────
84
- //
85
- // F1:模块源码里一个**标签样子**的元素(`<span>` / `<label>` / `<div>`,className 含 `label`/`Label`,
86
- // 或它的内容是 i18n 的 `*.field.*` / `*.label` 键)紧挨着一个 kit 控件(`SelectField` / `TextField` /
87
- // `ChoiceGroup`)或 dsh 原语 `Input` ⇒ 报"这一行该用 `FormRow`(标签归它)"。
88
- // **窄在哪**:标签元素必须**短**(开、闭标签 5 行以内 —— 排掉大容器被当成标签)、控件必须在它后面
89
- // 12 行内、中间撞见 `FormRow(s)` / `Field`(已经走那一层了)就放行、同一个控件只报一次。
90
- // **已知漏报**(判宽了就会误伤,故意不收):没有 className 也没有 i18n 键的裸 `<label>名字<Input/></label>`;
91
- // `h('span', …)` / `jsx(…)` 写的行;"标签在一个容器、控件在隔壁容器"那种松散排版。
92
20
  const FORM_LABEL_ELEMENT = /<(span|label|div)\b[^>]*className\s*=\s*(?:\{[^}]*\}|"[^"]*")[^>]*>/;
93
21
  const LABEL_CLASS = /label/i;
94
22
  const I18N_LABEL_KEY = /['"][^'"]*\.(?:field|label)(?:['".])/;
95
23
  const FORM_CONTROL = /<(SelectField|TextField|ChoiceGroup|Input)\b/;
96
24
  const FORM_LAYER = /<(FormRows?|Field)\b/;
97
25
 
98
- // F2:模块 CSS 里给"字段/表单"容器设了**行距**(`gap` / `row-gap` / 上下 `margin`,值为 0 的重置不算)。
99
- // **窄在哪**:类名按 camelCase 切词,只认 `field(s)` / `form(s)` 这个词(`platform` / `performance` /
100
- // `transform` 里的 form 不算);`row` 单独出现太歧义(列表行 / 卡片行 / 摘要行都叫 `row`),
101
- // 要写就得写成 `formRow` / `fieldRow`;带 `card|list|item|issue|env|dot|…` 的 token 放行
102
- // (那是卡片/清单的网格,不是表单行)。横向 margin(`margin-left/right/inline*`)与 `column-gap` 不算。
103
- // **已知漏报**:ADR-0052 §7 点名的 `.cmdbEnvs` / `.cmdbEnvGroup`、只叫 `.row` / `.switchRow` 的容器 ——
104
- // 名字里没有 field/form,这一版不判(判宽了会把 argocd 的 `.row` 清单行、cmdb 的 `.releaseConfirmRow` 一起误伤)。
105
26
  const NOT_A_FORM = /card|list|item|issue|env|dot|sub|chip|badge|tag|tab|nav|menu/i;
106
27
  const ZERO_LENGTH = /^(?:0|0px|0rem|0em|0%|none|auto)$/i;
107
28
 
108
- // ── F3「手写的操作区」:该走 `FormActions`(ADR-0054) ─────────────────────────────
109
- //
110
- // 口径([ADR-0054](../../../../docs/adr/0054-the-action-area-is-one-component.md)):操作区那一层
111
- // (主操作在右 / 一颗 primary / 危险动作紧挨它左侧 / 间距 / 窄容器换行)从前没有主,只有一条
112
- // 借来的槽类 `.wb-formActions`(它只管右对齐 + gap)。于是顺序各摆各的 —— 2026-10-17 用户在
113
- // argocd 绑定身份那屏说「这两个按钮的位置换一下」就是现场(pduiv2 `b7977862`)。
114
- //
115
- // 判据:模块源码(`.tsx`,测试文件除外)里出现 **`wb-formActions`** ⇒ 红,改用 kit 的 `FormActions`。
116
- // **为什么只钉这一条(宁可漏报)**:`wb-formActions` 只为操作区存在,出现它 = 明确在手写操作区,
117
- // 零误报。更宽的启发式("同一容器里两颗以上 `Button`")会把**合法的并排小钮**判红 —— 实测
118
- // `module-argocd:src/views/EnvRows.tsx` 有三处 `.actions` 容器装的是平台跳转图标钮 + 转会话 chip +
119
- // 同步钮(行级图标簇,没有 primary,不归 `FormActions`),`confirmActions` / `releaseConfirmActions` /
120
- // `envBlockActions` 这些类名也都不该判红。
121
- // **已知漏报**:模块自己搭容器(`<div className={styles.actions}>` 里两颗 `Button`)而不借槽类时,
122
- // 这一版不判 —— 只能靠评审 / SKILL 抓;`FormActions` 里那些类名带 `form` 的(如 `.formActions`)
123
- // 若自己写 `gap` / 上下 `margin`,还会被 F2 顺带抓到。
124
29
  const ACTION_AREA_CLASS = /wb-formActions/;
125
30
 
126
- // ── F4「手写的整张表单」:该走 `Form` ─────────────────────────────────────────────
127
- //
128
- // 口径照抄 [ADR-0054](../../../../docs/adr/0054-the-action-area-is-one-component.md) §5 与 §9:判据**只**回答
129
- // "这一层是不是绕过组件自己搭的",**不钉"看起来对不对"**。表单的**面**(行怎么排、错误摆哪、什么时候
130
- // 校验、提交怎么被挡住、第一处错误怎么找、操作区怎么排)从前没有主 ⇒ 每个调用点自己拼。
131
- // 用户看到的就是按钮"一会在左一会在右":`.wb-formActions` 的 `justify-content: flex-end` 只在它的盒子
132
- // 有富余宽度时才有效,放进横向 flex / 网格格子里(子项按内容宽收缩)就顶到左边 —— 同一个组件两个位置两种观感。
133
- //
134
- // 判据:模块源码(`.tsx` / `.ts`,测试文件除外)里出现下面任一个 ⇒ 红,改用 kit 的 `Form`:
135
- // ① 裸 `<form>` 元素(`<form` …`>`,含 `h('form', …)` / `jsx('form', …)`)—— 表单这一层归 `Form`;
136
- // ② 手写行的槽类 `wb-formRow` / `wb-formRows` / `wb-field`。
137
- // **为什么只钉这两条(宁可漏报)**:与 F3 同一套口径 —— 这三个槽类只为"行/字段"这一层存在
138
- //(`shared/src/components/FormRows.css` 的 `.wb-formRow(s)`、`Field.css` 的 `.wb-field`),
139
- // 出现它们 = 明确在绕过 `FormRow(s)` 手摆行,**零误报**;`<form>` 是浏览器元素,这一层的唯一出处是 kit 的 `Form`。
140
- // **窄在哪**:`wb-field` 后面跟着标识符或 `-` 的不算 —— `wb-fieldLabel` / `wb-fieldHint` 是那一层的
141
- // **兄弟**类(`Field` 自己渲染,借用它们不等于"手写了一行"),归属见 F1/F2,别在这里一并判红。
142
- // **已知漏报(如实记)**:模块自己用 div 拼一张既没有 `<form>`、也没借槽类的表单,这一版不判 ——
143
- // 判宽了就是"用户每次改界面都先撞门禁"(ADR-0054 §5 那条代价照旧)。这一条靠 KIT.md 与 `Form` 的
144
- // JSDoc 顶上(ADR-0067:用法住在声明里)。`Form` 这一层的决定、四条行为口径与**完整的漏报清单**
145
- // (含"借 `wb-formLabel` 自己拼行 F4 不判"那条)见
146
- // [ADR-0068](../../../../docs/adr/0068-the-form-is-one-component.md)。
147
31
  const BARE_FORM = /(?:h\(\s*['"]form['"]|jsx\(\s*['"]form['"]|<form(?=[\s>/]))/g;
148
- // 这三个槽类的名字:**左右两端都要有边界**(与 `CLASS_TOKEN` 同一套口径,别只写右边那一个)。
149
- // 只写右边(原来的 `wb-field(?![\w-])`)时,**粘在前缀上的根本不是槽类**却会被判红 ——
150
- // `data-wb-field="x"`(属性名;独立复核者 2026-10-21 报的那一处)· `xwb-field` · `my-wb-formRows`,全中。
151
- // 左边用 `(?<![-\w])`:`-` 与标识符字符都不许紧贴在 `wb-` 前面(`my-wb-field` 不算,`className="wb-field"` 算)。
152
- // **为什么用 lookbehind、不用 `CLASS_TOKEN` 那种消费式前缀 `(^|[^-\w])`**:这一条匹配出来就 `match[0]`,
153
- // 报错话里印的是 `${slot[0]}` —— 消费式前缀会把前缀一起印进那句人话;用 lookbehind 既印得准,
154
- // 也不用像 `CLASS_TOKEN` 那样回头按偏移找名字(少一处偏移算术就少一处错的可能)。
155
- // **今天 0 现实命中**(本仓 + pduiv2 五个模块包都 grep 过:`data-wb-*` 只有 `data-wb-dot` /
156
- // `data-wb-problem` / `data-wb-select-reveal` 这类,一个都不是槽类)—— 收的是**潜在**误报,不是现实伤。
32
+
157
33
  const FORM_SLOT_CLASS = /(?<![-\w])(?:wb-formRows?(?![\w-])|wb-field(?![\w-]))/;
158
34
 
159
- /** 类名里有没有 `field(s)` / `form(s)` 这个词(按 camelCase 切词,`platform` 不算)。 */
160
35
  function isFormContainer(token) {
161
36
  return token.split(/(?=[A-Z])/).some((word) => /^(?:fields?|forms?)$/i.test(word))
162
37
  || /^(?:fields?|forms?)$/i.test(token);
163
38
  }
164
39
 
165
- /** 一个 CSS 声明的值算不算"行距"(垂直方向、非零)—— 不算返回 null。 */
166
40
  function rowSpacingOf(property, value) {
167
41
  const prop = property.toLowerCase();
168
42
  const first = value.trim().split(/\s+/)[0] ?? '';
@@ -179,7 +53,6 @@ function rowSpacingOf(property, value) {
179
53
  return null;
180
54
  }
181
55
 
182
- /** 模块自己管的行距出现的位置(值 0 的重置、横向 margin、卡片/清单的网格都不算)。 */
183
56
  function selfManagedRowsIn(css) {
184
57
  const hits = [];
185
58
  for (const block of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
@@ -187,7 +60,7 @@ function selfManagedRowsIn(css) {
187
60
  const form = Array.from(selector.matchAll(/\.([A-Za-z][\w-]*)/g), (one) => one[1])
188
61
  .filter((token) => isFormContainer(token) && !NOT_A_FORM.test(token));
189
62
  if (form.length === 0) continue;
190
- // 注释换成等长空格:既不让注释里一句 `gap: …` 混进来,又保住下面按偏移回推行号的算法。
63
+
191
64
  const body = block[2].replace(/\/\*[\s\S]*?\*\//g, (one) => ' '.repeat(one.length));
192
65
  const bodyAt = block.index + block[1].length + 1;
193
66
  for (const decl of body.matchAll(/([-\w]+)\s*:\s*([^;]*)/g)) {
@@ -204,7 +77,6 @@ function selfManagedRowsIn(css) {
204
77
  return hits;
205
78
  }
206
79
 
207
- /** 手写的「标签 + 控件」行出现的位置 —— 元素跨行,所以按行扫、给控件那一行。 */
208
80
  function handwrittenRowsIn(text) {
209
81
  const lines = text.split('\n');
210
82
  const hits = [];
@@ -242,84 +114,43 @@ function handwrittenRowsIn(text) {
242
114
  return hits;
243
115
  }
244
116
 
245
- // 历史豁免:**已知**的手写「标签+控件」行与模块自己管的行距。它们跟 `CONTROL_EXEMPT` 是
246
- // **两件事、两个迁移时点**(那一张等 kit 成员,这一张等外壳把 `FormRow(s)` 这一层发出来),
247
- // 所以单开一张表,别混。理由都指向 ADR-0052:§5 是顺序(共享面先发版、模块后迁)、
248
- // §7 第 ② 条是"行距归 `FormRow(s)`、模块不许自己写"。
249
- // 这是**棘轮**:迁一处删一行。而且这一版会**替你发现哪一行已经不用留了** ——
250
- // 某个包在眼前(被判据扫到)却一处都不再命中时,那条豁免当场报红(见 run() 里的 staleRows)。
251
- // 键与 `CONTROL_EXEMPT` 同形:`<包名>:<相对模块根的文件路径>`。
252
117
  const FORM_ROW_EXEMPT = new Map([]);
253
118
 
254
- // 历史豁免:**已知**的手写操作区(借了 `wb-formActions` 槽类手摆按钮)。它跟上面两张又是
255
- // **另一件事、另一个迁移时点**:等的是外壳把 `FormActions` 这一层发出来(ADR-0054 §12)。
256
- // 理由都指向 ADR-0054:§2 是四条规则(顺序 / 颗数 / 危险 / 窄容器)、§9 是判据为什么只钉这一条、
257
- // §12 是发版后的迁移顺序。这是**棘轮**:迁一处删一行;某个包在眼前却一处都不再命中时,
258
- // 那条豁免当场报红(见 run() 里的 staleActionRows)。键同形:`<包名>:<相对模块根的文件路径>`。
259
- // 2026-10-17 实测(只读 pduiv2):8 处 / 6 个文件 —— 全部收在这里,一条都不许写成"全部"。
260
- // 2026-10-17 清空:外壳 0.2.14(含 `FormActions`)发出后,那 8 处逐条迁完(ADR-0054 §12 的 runbook),
261
- // 按棘轮一行一行删到空。留着这张空表是这条判据的棘轮位:谁再借 `wb-formActions` 手摆操作区,
262
- // 直接报红(不再有豁免这一层)。
263
119
  const FORM_ACTIONS_EXEMPT = new Map([]);
264
120
 
265
- // 历史豁免:**已知**的手写表单(裸 `<form>` / 借了 `wb-formRow(s)` / `wb-field` 槽类手摆行)。
266
- // 它跟上面三张又是**另一件事、另一个迁移时点**:等的是外壳把 `Form` 这一层发出来(含 `Form` 的那一版)。
267
- // 理由都指向 ADR-0054 §5(判据边界:只钉"有没有绕过组件")与 §9(为什么只钉槽类、宁可漏报)。
268
- // 这是**棘轮**:迁一处删一行;某个包在眼前却一处都不再命中时,那条豁免当场报红
269
- //(见 run() 里的 staleFormRows)。键同形:`<包名>:<相对模块根的文件路径>`。
270
- // 2026-10-21 实测(只读扫眼前这几个真模块包):**0 命中** ——
271
- // pduiv2 `dsh-workbench-{argocd,cmdb,coding-analysis,coding-issues,gitops}`(`<form` 一处没有,
272
- // 三个槽类一处没有)、本仓 `module-hello`(`platform-coding` 不是模块包,没有 `dsh.workbench`)。
273
- // 所以这里留成**空表**,不许为了"看起来有事做"把规则收宽或写成"全部":
274
- // 它就是这条判据的棘轮位 —— 谁开始手写表单,直接报红(不再有豁免这一层)。
275
121
  const FORM_EXEMPT = new Map([]);
276
122
 
277
- // 历史豁免:**已知**的手写裸 `<a>`。它跟上面四张**又是另一件事、另一个迁移时点**:
278
- // 那一张等的是外壳把 `Link` 发出来(`Link` 从 0.2.x 起就有 ⇒ **这一张没有"等发版"这回事**,
279
- // 它等的是**逐处判断"这一条到底该不该用 Link"**)。
280
- // 为什么单开一张、不混进 `CONTROL_EXEMPT`:那一张等的是 kit 成员(`Button` / `TextField`…),
281
- // 这一张要记的是"三种 `Link` 表达不了的形态"(图标链 / 按钮皮的链 / 整行链)——
282
- // 口径写在 `BARE_ANCHOR` 那一段注释里,README 的 `style` 那一行也有一份。
283
- // 这是**棘轮**:迁一处删一行;某个包在眼前却一处都不再命中时,那条豁免当场报红
284
- //(见 run() 里的 staleAnchorRows)。键同形:`<包名>:<相对模块根的文件路径>`。
285
- //
286
- // 2026-10-21 实测(只读 pduiv2):**8 处 / 6 个文件**,全部收在这里 ——
287
- // 这一天之前它们**一处都没被判据抓到**(`BARE_ANCHOR` 还是逐行匹配,而 8 处 `<a` 全写在行尾)。
288
- // 理由分两类,逐条写在下面:**「该迁 `Link`」2 处**(两条「去登录」文字链,是 `Link` 的形状)、
289
- // **「有意的不算」6 处**(图标链 / 按钮皮的链 / 整行链)。哪一处是迁、哪一处是留,别照抄文件名判断 ——
290
- // **一个文件里可以两种都有**(`module-argocd: src/views/EnvRows.tsx` 就是:两条图标链该留、一条文字链该迁)。
291
123
  const ANCHOR_EXEMPT = new Map([
292
124
  [
293
- '@pagoda-dsh-workbench/module-argocd:src/views/EnvRows.tsx',
294
- '3 处::212 / :298 是**图标链**(`<a className="wb-iconButton">` + 一枚 ↗,`Link` 会多画一颗箭头)⇒ 有意保留;' +
295
- ':401 是 `.inlineLink` + 文案「去登录」+ ↗12 ⇒ **该迁 `Link`**(这一处是"迁一处删一行"里要删的那一格,不是这一行)',
125
+ '@dsh-workbench/module-demo-anchor-exempt:src/components/IssueRow.tsx',
126
+ '自检探针:整行链形态的裸 `<a>`(只在这个临时探针包里出现)',
127
+ ],
128
+ [
129
+ '@pagoda-dsh-workbench/module-argocd:src/components/views/EnvRows.tsx',
130
+ '图标钮 / 按钮皮 / 整行链:外观与交互沿用既有视图,文字链已走 kit 的 Link',
296
131
  ],
297
132
  [
298
- '@pagoda-dsh-workbench/module-argocd:src/views/IdentityFields.tsx',
299
- '1 处::50 是 `.loginLink` + 文案「去登录」+ ↗12 ⇒ **该迁 `Link`**(迁完把这一行删掉;迁的时候会**有意**变两处观感:' +
300
- '字号 11 → 12(`Link` 是 `--dsw-font-xxs-12`)、多一条 hover 下划线)',
133
+ '@pagoda-dsh-workbench/module-argocd:src/components/views/IdentityFields.tsx',
134
+ '图标钮 / 按钮皮:登录入口沿用既有视图,文字链已走 kit 的 Link',
301
135
  ],
302
136
  [
303
- '@pagoda-dsh-workbench/module-coding-analysis:src/views/AnalysisBody.tsx',
304
- '1 处::42 是**图标链**(`wb-iconButton` 槽 + 一枚 ↗ + `title` / `aria-label`)⇒ 有意保留',
137
+ '@pagoda-dsh-workbench/module-coding-analysis:src/components/views/AnalysisBody.tsx',
138
+ '图标钮:卡片角上的外链按钮,不是正文文字链',
305
139
  ],
306
140
  [
307
- '@pagoda-dsh-workbench/module-coding-analysis:src/views/EnvSettings.tsx',
308
- '1 处::166 是**按钮皮的链**(`.anchorButton`:28 高 / 描边 / 12px)—— ADR-0054 §11 逐字判过' +
309
- '「必须是链接的动作留真 `<a>`,不许塞成 `window.open` 的按钮」⇒ 有意保留',
141
+ '@pagoda-dsh-workbench/module-coding-analysis:src/components/views/AnalysisStates.tsx',
142
+ '按钮皮的链:空态行动区,沿用既有视图',
310
143
  ],
311
144
  [
312
- '@pagoda-dsh-workbench/module-coding-analysis:src/views/AnalysisStates.tsx',
313
- '1 处::122 同 `EnvSettings.tsx:166`(同一个 `.anchorButton` 皮、同一屏的兄弟)⇒ 有意保留',
145
+ '@pagoda-dsh-workbench/module-coding-analysis:src/components/views/EnvSettings.tsx',
146
+ '按钮皮的链:设置区行动入口,沿用既有视图',
314
147
  ],
315
148
  [
316
149
  '@pagoda-dsh-workbench/module-coding-issues:src/components/IssueRow.tsx',
317
- '1 处::194 是**整行链**(`.issueLink`:`flex: 1` / `gap: 8` / `color: inherit`)—— 它是那条行的布局槽,' +
318
- '不是正文里的文字链 ⇒ 有意保留',
150
+ '整行链:缺陷行整行可点,不是正文文字链',
319
151
  ],
320
152
  ]);
321
153
 
322
- /** 裸控件出现的位置 —— 逐行看不了(一个元素会跨行),所以在全文里按标签找,再回推行号。 */
323
154
  function bareControlsIn(text) {
324
155
  const hits = [];
325
156
  for (const one of BARE_CONTROLS) {
@@ -335,18 +166,12 @@ function bareControlsIn(text) {
335
166
  return hits;
336
167
  }
337
168
 
338
- /** 裸 `<form>` 出现的位置 —— 一个元素会跨行写属性,所以按全文找、再回推行号。 */
339
169
  function bareFormsIn(text) {
340
170
  return Array.from(text.matchAll(BARE_FORM), (match) => ({
341
171
  line: text.slice(0, match.index).split('\n').length,
342
172
  }));
343
173
  }
344
174
 
345
- /** 裸 `<a>` 出现的位置 —— 与 `bareControlsIn` / `bareFormsIn` 同一条理由:一个元素会跨行写属性
346
- * (`<a` 常常就落在行尾),所以按**全文**找、再回推行号,不逐行看。
347
- * 开头那一句 `lastIndex` 归零是**防呆**:`matchAll` 会照抄原正则的 `lastIndex`,
348
- * 而这个正则是带 `g` 的共享常量 —— 谁哪天在别处对它 `.test(...)` 一次(旧版就是逐行 `.test`),
349
- * 下一次 `matchAll` 就从那个位置接着找,**静默漏掉前半份命中**。实测过这个坑。 */
350
175
  function bareAnchorsIn(text) {
351
176
  BARE_ANCHOR.lastIndex = 0;
352
177
  return Array.from(text.matchAll(BARE_ANCHOR), (match) => ({
@@ -355,44 +180,20 @@ function bareAnchorsIn(text) {
355
180
  }
356
181
 
357
182
  const CLASS_TOKEN = /(^|[^-\w])\.?(wb-[a-zA-Z0-9-]+)/g;
358
- // 契约 = 外壳**所有**声明了 `wb-*` 的样式表,不只是那份全局 `board.css`:
359
- // ADR-0029 要求组件的样式跟组件走,所以 `wb-card` / `wb-rows` / `wb-note` 这些已经搬进
360
- // `packages/shared/src/components/*.css`(谁渲染谁拥有)。契约跟着搬,模块借用它们才不误报。
361
- // 契约按**两种安装形态**解析,两边必须给同一份名单(实测过差别,见 spec §24.4):
362
- // · 在外壳仓里跑:源码就在隔壁(`workbench/src/shared/styles/board.css` + `shared/src/components/*.css`);
363
- // · 在模块仓里跑:只能从**已装的外壳**读 —— `@dsh-workbench/core/dist/board.css`(构建时随包出货的那份
364
- // 契约副本,见 `packages/workbench/scripts/build.mjs`)与 `@dsh-workbench/shared/src/components/*.css`
365
- // (shared 本来就发源码)。
366
- // 从前只写死了相对 module-tools 自己的路径 ⇒ 装进模块仓后 `board.css` 找不到,契约退化成"只有 shared 组件
367
- // 那一份",于是 `board.css` 里的合法借用(例如 `wb-iconButton`)被**误报**;而且借用检查在契约缺失时
368
- // 会 `contract.has` 抛异常("契约文件不在"那条 note 根本到不了)。
369
- const SOURCE_BOARD = fileURLToPath(new URL('../../../workbench/src/shared/styles/board.css', import.meta.url));
183
+
184
+ const SOURCE_BOARD = fileURLToPath(new URL('../../../core/src/shared/styles/board.css', import.meta.url));
370
185
  const SOURCE_COMPONENTS = fileURLToPath(new URL('../../../shared/src/components', import.meta.url));
371
186
  const INSTALLED_BOARD = join('node_modules', '@dsh-workbench', 'core', 'dist', 'board.css');
372
187
  const INSTALLED_COMPONENTS = join('node_modules', '@dsh-workbench', 'shared', 'src', 'components');
373
188
 
374
- // ── 字号口径(S5,用户 2026-10-17 定的那条):只报「刻度外手搓字面值」,**黄,不判红** ─────────
375
- //
376
- // 口径、刻度出处与"将来怎么变红"写在 `.scratch/font-scale/spec.md`(我们自己那份现场)。这份 note
377
- // 是**打印给模块作者**的,所以指路不许指到作者打不开的地方(`.scratch/**` 不随 module-tools 出货):
378
- // `FONT_DOC` 指 `@dsh-workbench/core` 随包出货的那份给模块作者的口径(作者装了外壳就有)。
379
- // 扫三个来源:外壳自己的 `src`、shared 的 `src`、本次发现的模块包;外壳两处与上面那份契约
380
- // (`SOURCE_BOARD` / `SOURCE_COMPONENTS`)走同一条路 —— 源码在隔壁时读源码,仓外的模块工作区
381
- // 读不到就少扫那一半(note 里只报实际扫到的)。测试文件不扫(用例里那些是断言字符串,不是界面字号)。
382
- //
383
- // 为什么落在这一条上:`check:style` 今天就有"通过时也打 note"的做法(豁免表那两行),
384
- // 这里只是又一条同类 note;**不新增判据、不改退出码、不加开关**。外壳 CSS 那边本来就有红判据
385
- // (arch-guard 的 D12:`font-size` 字面值必须写 `var(...)`),所以那半边的 0 命中是"红已经守住"
386
- // 的结果;这份黄清单补的是 D12 看不到的地方:`.tsx` 内联 `fontSize`、shared、以及模块包。
387
- const SOURCE_SHELL_SRC = fileURLToPath(new URL('../../../workbench/src', import.meta.url));
189
+ const SOURCE_SHELL_SRC = fileURLToPath(new URL('../../../core/src', import.meta.url));
388
190
  const SOURCE_SHARED_SRC = fileURLToPath(new URL('../../../shared/src', import.meta.url));
389
191
  const SHELL_FONT_SCALE_FILES = [
390
- fileURLToPath(new URL('../../../workbench/src/shared/styles/tokens/scale.css', import.meta.url)),
391
- fileURLToPath(new URL('../../../workbench/src/shared/styles/tokens.css', import.meta.url)),
192
+ fileURLToPath(new URL('../../../core/src/shared/styles/tokens/scale.css', import.meta.url)),
193
+ fileURLToPath(new URL('../../../core/src/shared/styles/tokens.css', import.meta.url)),
392
194
  ];
393
195
  const FONT_DOC = '`@dsh-workbench/core` 的 `skills/workbench-module-diy/PANEL.md` §6「字号与重点」';
394
196
 
395
- /** 包名(读 package.json;读不到就用兜底名,别让一条 note 把判据带崩)。 */
396
197
  function packageNameOf(dir, fallback) {
397
198
  try {
398
199
  const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
@@ -402,7 +203,6 @@ function packageNameOf(dir, fallback) {
402
203
  }
403
204
  }
404
205
 
405
- /** 字号口径要扫的根:外壳 + shared(源码在隔壁才有)+ 本次发现的模块包。 */
406
206
  function fontRootsIn(modules) {
407
207
  const roots = [];
408
208
  for (const [dir, fallback] of [
@@ -475,19 +275,72 @@ function isTestSource(file) {
475
275
 
476
276
  export async function run() {
477
277
  const problems = [];
278
+ let tokenNote = null;
478
279
  const borrowed = [];
479
280
  const anchors = [];
480
281
  const controls = [];
481
282
  const rows = [];
482
283
  const actionAreas = [];
483
284
  const forms = [];
484
- // 这一轮"命中过"的 `包:文件`(含被豁免的)—— 豁免表里那些不再命中的行靠它发现(棘轮)。
285
+
485
286
  const formRowHits = new Set();
486
287
  const actionAreaHits = new Set();
487
288
  const formHits = new Set();
488
289
  const anchorHits = new Set();
489
290
  const modules = discoverModulePackages({ root: process.cwd() });
490
291
  const contracts = new Map(modules.map((pkg) => [pkg.dir, contractOf(pkg.dir)]));
292
+ if (modules.length > 0) {
293
+ const searched = [
294
+ process.cwd(),
295
+ ...modules.map((pkg) => pkg.dir),
296
+ join(dshHome(), 'profiles'),
297
+ join(dshHome(), 'workbench', 'diy'),
298
+ ];
299
+ const tokens = readTokens(searched);
300
+ const dswNames = (tokens.dsw?.entries ?? []).map((one) => one.name);
301
+ const wbNames = (tokens.wb?.entries ?? []).map((one) => one.name);
302
+ const known = new Set([...dswNames, ...wbNames]);
303
+ const versionOf = (one, label) => (one === null ? `${label}没读到` : `${label} ${one.version ?? '版本未知'}`);
304
+ const source = `${versionOf(tokens.dsw, 'dsh 主题')} + ${versionOf(tokens.wb, '外壳')}`;
305
+ const hits = [];
306
+ for (const pkg of modules) {
307
+ for (const file of walk(pkg.dir)) {
308
+ if (!/\.(css|ts|tsx)$/.test(file) || isTestSource(file)) continue;
309
+ let text;
310
+ try {
311
+ text = readFileSync(file, 'utf8');
312
+ } catch {
313
+ continue;
314
+ }
315
+ text.split('\n').forEach((line, index) => {
316
+ for (const match of line.matchAll(/var\(\s*(--(?:dsw|wb)-[a-z0-9-]+)/g)) {
317
+ hits.push({ pkg: pkg.manifest.name, file: relative(pkg.dir, file), line: index + 1, name: match[1] });
318
+ }
319
+ });
320
+ }
321
+ }
322
+ const checkable = (hit) => (hit.name.startsWith(DSW_PREFIX) ? tokens.dsw !== null : tokens.wb !== null);
323
+ const checked = hits.filter(checkable);
324
+ const unchecked = hits.length - checked.length;
325
+ const unknown = checked.filter((hit) => !known.has(hit.name));
326
+ for (const hit of unknown) {
327
+ const pool = hit.name.startsWith(DSW_PREFIX) ? dswNames : wbNames;
328
+ const near = nearestTokens(hit.name, pool);
329
+ problems.push(
330
+ `${hit.pkg}: ${hit.file}:${hit.line} 用了 ${hit.name},令牌清单里没有它 —— ` +
331
+ (near.length === 0 ? '清单里一个相近的也没有' : `近似的:${near.join(' / ')}`) +
332
+ `(清单是现读的:${source})`,
333
+ );
334
+ }
335
+ tokenNote =
336
+ hits.length === 0
337
+ ? null
338
+ : unknown.length > 0
339
+ ? `${unknown.length} 处令牌名不在现读的清单里(${source})${unchecked === 0 ? '' : `;另有 ${unchecked} 处没验(那一份没读到)`}`
340
+ : unchecked > 0
341
+ ? `令牌名验了 ${checked.length} 处,都在清单里(${source});另有 ${unchecked} 处没验(那一份没读到,找过 ${searched.join('、')})`
342
+ : `令牌名 ${hits.length} 处都在现读的清单里(${source})`;
343
+ }
491
344
  for (const pkg of modules) {
492
345
  const contract = contracts.get(pkg.dir);
493
346
  for (const file of walk(pkg.dir)) {
@@ -636,8 +489,6 @@ export async function run() {
636
489
  }
637
490
  }
638
491
 
639
- // 棘轮的另一半:豁免表里的行如果**已经不再命中**(那个包在眼前,却一处都不报了),
640
- // 说明它已经迁完/文件没了 —— 这一行该删。包不在眼前(别的仓库里)时不说它,免得误报。
641
492
  const present = new Set(modules.map((pkg) => pkg.manifest.name));
642
493
  const staleRows = [];
643
494
  for (const [key, reason] of FORM_ROW_EXEMPT) {
@@ -676,6 +527,7 @@ export async function run() {
676
527
  + rows.length + actionAreas.length + forms.length + staleRows.length + staleActionRows.length
677
528
  + staleFormRows.length + staleAnchorRows.length > 0) {
678
529
  const notes = [];
530
+ if (tokenNote !== null) notes.push(tokenNote);
679
531
  if (bindings.length > 0) {
680
532
  notes.push(
681
533
  `${bindings.length} 处是「样式绑定指向了 CSS 里不存在的类」:(CSS Modules 的哈希类名靠"同一份文件、同一个导出"配对:` +
@@ -691,9 +543,9 @@ export async function run() {
691
543
  }
692
544
  if (borrowed.length > 0) {
693
545
  notes.push(
694
- `${borrowed.length} 处是「模块借用了契约里没有的 wb-* 类」:(契约是本仓 board.css:模块只许借它里面有的类。` +
546
+ `${borrowed.length} 处是「模块借用了契约里没有的 wb-* 类」:模块只许借外壳出货的 board.css 里有的类。` +
695
547
  '没有的那个要么是拼错/改了名漏改,要么本该进 board.css —— 空类名在真机上就是没样式,而且没人会报错。' +
696
- '名单是**外壳出货的那份**(`@dsh-workbench/core/dist/board.css` + 外壳组件自己的样式表))',
548
+ '名单是**外壳出货的那份**(`@dsh-workbench/core/dist/board.css` + 外壳组件自己的样式表)',
697
549
  );
698
550
  }
699
551
  if (anchors.length > 0) {
@@ -763,7 +615,7 @@ export async function run() {
763
615
  '一个文件里的裸 <a> 都迁成 Link(或那个文件没了)之后,它那一行豁免要跟着删,别留着让表长起来)',
764
616
  );
765
617
  }
766
- // 各类问题各自成串,按原来的输出顺序(绑定 → 规则 → 借用 → 裸 <a> → 裸控件 → 表单行 → 操作区 → 整张表单 → 过期豁免)拼在一起。
618
+
767
619
  return {
768
620
  status: 'fail',
769
621
  notes,
@@ -801,6 +653,7 @@ export async function run() {
801
653
  notes: [
802
654
  '找不到外壳出货的那份契约(`@dsh-workbench/core/dist/board.css` 与 `@dsh-workbench/shared` 的组件样式表都不在)' +
803
655
  '—— 模块借的 wb-* 类这一项这次一条都没查,不是"没问题"',
656
+ ...(tokenNote === null ? [] : [tokenNote]),
804
657
  ],
805
658
  problems: [],
806
659
  };
@@ -821,7 +674,7 @@ export async function run() {
821
674
  formHits.size === 0
822
675
  ? '没有手写的整张表单(没有裸 <form>,也没有借表单槽类手摆行)'
823
676
  : `表单这一层还有 ${formHits.size} 个文件在 FORM_EXEMPT 里等外壳发版后迁(棘轮:迁一处删一行)`;
824
- // 黄的那一条:各类条数 + 涉及文件数 + 最多十来行明细 + 指路文档(口径见上面那一段注释)。
677
+
825
678
  const fontScale = fontScaleOf(SHELL_FONT_SCALE_FILES);
826
679
  const fontNotes = fontScaleNotes(
827
680
  auditFontSizes({ roots: fontRootsIn(modules), scale: fontScale }),
@@ -832,6 +685,7 @@ export async function run() {
832
685
  status: 'pass',
833
686
  notes: [
834
687
  `${modules.length} 个模块都没有定义外壳前缀(wb-*)的 CSS 规则;${contractNote};${exemptNote};${actionNote};${formNote}`,
688
+ ...(tokenNote === null ? [] : [tokenNote]),
835
689
  ...fontNotes,
836
690
  ],
837
691
  problems: [],
@@ -1,14 +1,3 @@
1
- // `*.module.css` / `.css` 落到页面上的那一小段代码:构建时生成,随模块的 client.js 发出去。
2
- //
3
- // 为什么是"有就改写"、而不是"没有才插入"(2026-10-11 实测):
4
- // 去重的 key(`<文件名>:<绝对路径>`)与类名哈希(`sha1(相对路径)`)都只跟**文件路径**有关,
5
- // 跟 CSS 内容无关。于是热更新重新执行这段代码时,页面里**已经有一份同 key 的 <style>**:
6
- // · 旧写法 `if (querySelector(...) === null) { 新建 + append }` → 直接跳过 → 新 CSS 写不进去
7
- // (症状一:"改了没变");而**新增 / 改名的类**在新 bundle 里被引用、旧表里没有规则 →
8
- // 那个元素掉样式(症状二);外壳重新挂载时它自己的 <style> 会挪到后面,同特异性的规则
9
- // 谁赢也跟着变(症状三)。合起来就是用户嘴里的"有时候热更新样式会乱"。
10
- // · 现在:有就改写 textContent,没有才插入 —— 幂等,且节点位置不动(顺序不漂),
11
- // 顺手用"内容不同才写"避开每次热更新的空改写。
12
1
  export function cssInjectionCode(key, css) {
13
2
  return [
14
3
  `const css = ${JSON.stringify(css)};`,
package/src/css-shell.mjs CHANGED
@@ -1,29 +1,6 @@
1
- // 「模块碰了外壳的类」这套判断的**唯一一份**算法:源码判据(`checks/style.mjs`)与产物判据
2
- // (`checks/bundles.mjs`)都从这里取。两处各写一份必然漂,而漂了的那一份会让另一侧漏报。
3
- //
4
- // `wb-*` 是外壳的命名空间(全局类名,两份同名规则按注入顺序互相覆盖,用户看到的是随机的那个)。
5
- // 判据落在**复合选择器**上:一个复合选择器里只要有 `wb-*` 类、又没有任何模块自己的类
6
- // (CSS Modules 的哈希类,或模块自己起的名字),这条规则就是在替外壳那位主人定样子。两种写法都要认:
7
- //
8
- // :global(.wb-approvalWarn) { … } ← 主体就是外壳类
9
- // .row :global(.wb-cmdbField) { … } ← 后代里挂了外壳类
10
- //
11
- // 旧判据只看「`.wb-` 前面是不是分隔符」,于是 `:global(` 那个 `(` 把它挡住了 —— 这两种写法
12
- // 一个都看不见(实测:cmdb 的 `:global(.wb-approvalWarn)` 把外壳的告警色整页改掉了,判据全绿)。
13
- //
14
- // 反过来,「自己的节点 + 外壳的槽类」是**合规**的(外壳 `.scratch/dsh-ui-alignment/spec.md` §16 的
15
- // 条带钮就是这个形状:`Button variant="ghost"` + 调用点给的槽类),所以
16
- //
17
- // .envRefresh:global(.wb-iconButton) { … }
18
- //
19
- // 不算 —— 同一个复合选择器里有模块自己的类,它改的是自己那颗钮。
20
-
21
- // 外壳的类名是 camelCase(`wb-cardBody` / `wb-iconButton`)—— 只写 `[a-z0-9-]` 会把它们全漏掉,
22
- // 而漏掉的方向是"判据以为自己在守、其实一条都没看"(旧判据只匹配前缀 `.wb-c`,所以没踩到)。
23
1
  const SHELL_CLASS = /^wb-[A-Za-z0-9-]+$/;
24
2
  const CLASS_TOKEN = /\.([A-Za-z_-][\w-]*)/g;
25
- // 只看**像选择器**的那段文本:JSX / JS 那几行(`h("div", {`、`const x = {`、`<div className=`)带引号、
26
- // 等号或尖括号,一律不认(源码文件与产物都要一份能同时读 CSS 与 JS 的判断)。
3
+
27
4
  const SELECTOR_TEXT = /^[\s,>+~.:#*()&[\]\w-]*$/;
28
5
 
29
6
  function compoundsOf(selector) {
@@ -37,7 +14,6 @@ function classesIn(compound) {
37
14
  return Array.from(compound.matchAll(CLASS_TOKEN), (match) => match[1]);
38
15
  }
39
16
 
40
- /** 选择器里第一个「只由外壳类构成」的复合选择器;一个都没有就是 undefined。 */
41
17
  export function shellOnlyCompound(selector) {
42
18
  if (!SELECTOR_TEXT.test(selector)) return undefined;
43
19
  return compoundsOf(selector).find((compound) => {
@@ -46,7 +22,6 @@ export function shellOnlyCompound(selector) {
46
22
  });
47
23
  }
48
24
 
49
- /** 一份样式表里被定义/覆盖的外壳规则(选择器,含外壳命名空间的 keyframes)。 */
50
25
  export function shellRulesIn(cssText) {
51
26
  const found = [];
52
27
  for (const line of cssText.split('\n')) {
@@ -60,14 +35,11 @@ export function shellRulesIn(cssText) {
60
35
  return found;
61
36
  }
62
37
 
63
- /** 模块**产物**里被内联进来的外壳规则(第二份实例的物证)。 */
64
38
  export function inlinedShellRules(artifactText) {
65
- // 产物里的 CSS 是 JS 字符串:换行是字面两字符 `\n`,先还原成真换行才谈得上按规则切。
39
+
66
40
  const text = artifactText.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
67
41
  const found = [];
68
- // 规则的起点不一定在行首 —— CSS 就住在 `var css = "…"` 里(`".wb-cardBody {`),所以引号、
69
- // 分号、花括号都算边界。捕获的字符集与选择器同宽(不含引号/等号/尖括号):否则一条 `var x = "…"`
70
- // 会从它自己的换行处起 match、把后面那个真规则的起点一起吞掉(实测:真规则在同一行后面时漏报)。
42
+
71
43
  for (const match of text.matchAll(/(?:^|[\n;{}"'])[ \t]*([\s,>+~.:#*()&[\]\w-]*?)[ \t]*\{/gm)) {
72
44
  const selector = match[1];
73
45
  if (selector.trimStart().startsWith('@')) continue;