@dsh-workbench/module-tools 0.1.7 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/module-tools",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "模块包的构建与门禁工具:build(单文件 ESM 宿主侧 + 套 loader 壳的浏览器侧)、check(契约/产物边界/源码边界/卡片渲染)。模块包不再各带一份构建脚本。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -64,7 +64,22 @@ function fakeKit() {
64
64
  const callable = () => anyStub('result');
65
65
  return {
66
66
  hooks: new Proxy({}, { get: () => callable }),
67
- components: new Proxy({}, { get: () => (props) => ({ type: 'stub', props: props ?? {}, children: [] }) }),
67
+ // 组件替身**带上自己的名字**(`stub:Card`):卡片壳现在是 `kit.components.Card` 这个组件,
68
+ // 产物里不会再出现 `wb-card` 这个类名 —— 判据要能认出"用了外壳给的卡壳"。
69
+ // children 要**往下传**:吞掉的话,卡片体内用 kit 组件写的内容就永远渲染不到(节点数会假少)。
70
+ components: new Proxy(
71
+ {},
72
+ {
73
+ get: (_target, key) => (props) => {
74
+ const kids = props?.children;
75
+ return {
76
+ type: `stub:${String(key)}`,
77
+ props: props ?? {},
78
+ children: kids === undefined ? [] : Array.isArray(kids) ? kids : [kids],
79
+ };
80
+ },
81
+ },
82
+ ),
68
83
  api: new Proxy({}, { get: () => callable }),
69
84
  lib: anyStub('lib'),
70
85
  };
@@ -114,6 +129,8 @@ function loadFace(clientFile) {
114
129
 
115
130
  // 递归**调用**函数组件 —— 不调用的话,树里只有最外层那个 `type`,
116
131
  // 卡片体内的代码一行都没跑(第一次写漏了这步,4 张卡片都只数出 3 个节点)。
132
+ //
133
+ // 顺手把**用到的类名**收集起来:卡片得自己套外壳的卡片契约类(见文件尾那条判据)。
117
134
  function render(node, depth, budget) {
118
135
  if (node === null || node === undefined || typeof node === 'boolean') return 0;
119
136
  if (typeof node === 'string' || typeof node === 'number') return 1;
@@ -127,6 +144,14 @@ function render(node, depth, budget) {
127
144
  budget.left -= 1;
128
145
  budget.maxDepth = Math.max(budget.maxDepth, depth);
129
146
 
147
+ const classes = node.props?.className;
148
+ if (typeof classes === 'string') {
149
+ for (const one of classes.split(/\s+/)) if (one !== '') budget.classNames.add(one);
150
+ }
151
+ if (typeof node.type === 'string' && node.type.startsWith('stub:')) {
152
+ budget.stubs.add(node.type.slice('stub:'.length));
153
+ }
154
+
130
155
  const children = [...(node.children ?? [])];
131
156
  if (typeof node.type === 'function') {
132
157
  if (depth > 40) throw new Error('组件递归超过 40 层(自引用?)');
@@ -217,7 +242,7 @@ for (const pkg of modulePackages()) {
217
242
  continue;
218
243
  }
219
244
 
220
- const budget = { left: 20000, maxDepth: 0 };
245
+ const budget = { left: 20000, maxDepth: 0, classNames: new Set(), stubs: new Set() };
221
246
  let count;
222
247
  try {
223
248
  count = render(tree, 0, budget);
@@ -230,6 +255,20 @@ for (const pkg of modulePackages()) {
230
255
  problems.push(`${name}: 卡片只画出一个节点 —— 卡片体没跑(或什么都没画)`);
231
256
  continue;
232
257
  }
258
+ // 卡片得**套上外壳给的卡壳**:要么用 `kit.components.Card` 这个组件(推荐、也是 hello 现在的做法),
259
+ // 要么自己写契约类 `wb-card`(`board.css` 里那条,兄弟模块现在还是这么写的)。
260
+ //
261
+ // 用户实测(2026-10-09):"我们的 hello module 怎么不是卡片" —— 面板里那块内容既没有底色也没有
262
+ // 描边,看着就是标题加一段话。原因是卡片函数没套壳:当时用的是 `wb-view`(那是面板列布局),
263
+ // 更早那份装进 profile 的产物还自己搓过 `wb-hello*`。两种都查得出来:组件走 stub 名,类名走 classNames。
264
+ const shelled = budget.classNames.has('wb-card') || budget.stubs.has('Card');
265
+ if (!shelled) {
266
+ problems.push(
267
+ `${name}: 卡片没套外壳的卡壳 —— 用 kit.components.Card(推荐),或自己写契约类 wb-card + ` +
268
+ `wb-cardBody;裸内容在面板里看起来就"不是一张卡片"`,
269
+ );
270
+ continue;
271
+ }
233
272
  rows.push(`${name}: 空数据下画得出 ${count} 个节点(最深 ${budget.maxDepth} 层)`);
234
273
  }
235
274
 
@@ -6,9 +6,20 @@ import { discoverModulePackages } from '../discover.mjs';
6
6
  const SHELL_SELECTOR = /(^|[\s,>+~])\.wb-[a-z0-9-]+/;
7
7
  const RULE = /\.wb-[a-z0-9-]+[^{}]*\{/;
8
8
  const CLASS_TOKEN = /(^|[^-\w])\.?(wb-[a-zA-Z0-9-]+)/g;
9
- const CONTRACT = fileURLToPath(
10
- new URL('../../../workbench/src/shared/styles/board.css', import.meta.url),
11
- );
9
+ // 契约 = 外壳**所有**声明了 `wb-*` 的样式表,不只是那份全局 `board.css`:
10
+ // ADR-0029 要求组件的样式跟组件走,所以 `wb-card` / `wb-rows` / `wb-note` 这些已经搬进
11
+ // `packages/shared/src/components/*.css`(谁渲染谁拥有)。契约跟着搬,模块借用它们才不误报。
12
+ const CONTRACT_FILES = [
13
+ fileURLToPath(new URL('../../../workbench/src/shared/styles/board.css', import.meta.url)),
14
+ ...(() => {
15
+ const dir = fileURLToPath(new URL('../../../shared/src/components', import.meta.url));
16
+ return existsSync(dir)
17
+ ? readdirSync(dir)
18
+ .filter((name) => name.endsWith('.css'))
19
+ .map((name) => join(dir, name))
20
+ : [];
21
+ })(),
22
+ ];
12
23
 
13
24
  function walk(dir, acc = []) {
14
25
  for (const entry of readdirSync(dir)) {
@@ -24,9 +35,11 @@ function isTestSource(file) {
24
35
  return /(^|[\\/])__tests__[\\/]/.test(file) || /\.test\.[cm]?tsx?$/.test(file);
25
36
  }
26
37
 
27
- const contract = existsSync(CONTRACT)
38
+ const contract = CONTRACT_FILES.some((file) => existsSync(file))
28
39
  ? new Set(
29
- Array.from(readFileSync(CONTRACT, 'utf8').matchAll(CLASS_TOKEN), (match) => match[2]),
40
+ CONTRACT_FILES.filter((file) => existsSync(file)).flatMap((file) =>
41
+ Array.from(readFileSync(file, 'utf8').matchAll(CLASS_TOKEN), (match) => match[2]),
42
+ ),
30
43
  )
31
44
  : null;
32
45
 
@@ -124,7 +137,7 @@ if (borrowed.length > 0) {
124
137
  const contractNote =
125
138
  contract === null
126
139
  ? '契约文件不在(这次只查了"模块不许定义 wb-*")'
127
- : `借用的类都在 board.css 契约里`;
140
+ : `借用的类都在契约里(board.css + 外壳组件自己的样式表)`;
128
141
  console.log(
129
142
  `✔ 样式归属清楚:${modules.length} 个模块都没有定义外壳前缀(wb-*)的 CSS 规则;${contractNote}`,
130
143
  );