@dsh-workbench/module-tools 0.1.9 → 0.1.10

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.9",
3
+ "version": "0.1.10",
4
4
  "description": "模块包的构建与门禁工具:build(单文件 ESM 宿主侧 + 套 loader 壳的浏览器侧)、check(契约/产物边界/源码边界/卡片渲染)。模块包不再各带一份构建脚本。",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/check.mjs CHANGED
@@ -4,6 +4,7 @@ const CHECKS = {
4
4
  bundles: { title: '模块产物边界', load: () => import('./checks/bundles.mjs') },
5
5
  source: { title: '模块源码边界', load: () => import('./checks/source.mjs') },
6
6
  primitives: { title: 'dsh 原语只从模块表拿', load: () => import('./checks/primitives.mjs') },
7
+ kit: { title: '外壳 kit 面', load: () => import('./checks/kit.mjs') },
7
8
  cards: { title: '卡片渲染', load: () => import('./checks/cards.mjs') },
8
9
  standalone: { title: '独立构建', load: () => import('./checks/standalone.mjs') },
9
10
  markers: { title: '公开包无内部标识', load: () => import('./checks/markers.mjs') },
@@ -0,0 +1,183 @@
1
+ // 模块产物从外壳 kit 里取的成员,必须真的在**它承诺给用户的那份外壳**里存在。
2
+ //
3
+ // 起因(2026-10-10 现场):`module-cmdb@0.1.7` 的产物用了 0.1.8 才有的
4
+ // `kit.components.StateBlock` / `Note` / `Activity*`,而 peer 写的是 `^0.1.4`。
5
+ // 装到旧外壳上:顶层解构不抛(`undefined` 也照样解出来),卡片**首帧渲染**时才炸
6
+ // `Element type is invalid`;外壳里没有错误边界 ⇒ 整块面板被 React 卸掉。
7
+ // 维护者本机复现不了,因为本机 profile 把外壳 `link:` 到本仓(永远 HEAD)。
8
+ //
9
+ // 两条规则:
10
+ // ① **面差**:产物 bind 的 kit 成员 ⊆ 指定外壳 `client.js` 里那份 kit。
11
+ // 默认用模块包自己装的那份外壳(devDependency);`DSH_WB_CORE_CLIENT=<路径>`
12
+ // 可以指定别的一份 —— 用它验"对 peer 下限那份外壳会不会红"(发布前该跑一遍)。
13
+ // ② **下限自洽**:`peerDependencies['@dsh-workbench/core']` 的下限不许低于
14
+ // `devDependencies` 的下限。你对用户承诺的版本比你自己构建/测试用的还旧时,
15
+ // 产物的面到底够不够,没人验过 —— 这一条就是把"没人验过"变成红灯。
16
+ // 用 `DSH_WB_CORE_CLIENT` 指着下限那份外壳跑(且面差通过)= 验过了,规则②不再报。
17
+ //
18
+ // 判据跟着 `dsh.workbench` 自述清单走;没有 client.js 的包这里没事可查(由 build/契约判据管)。
19
+
20
+ import { existsSync, readFileSync } from 'node:fs';
21
+ import { createRequire } from 'node:module';
22
+ import { dirname, join } from 'node:path';
23
+ import { discoverModulePackages } from '../discover.mjs';
24
+
25
+ const SHELL = '@dsh-workbench/core';
26
+ const KIT_SPECIFIER = '@dsh-workbench/core/client';
27
+
28
+ function kitUsageOf(text) {
29
+ const bindings = new Set();
30
+ const BINDING = /(?:var|const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:__toESM\(\s*)?require\(\s*["']@dsh-workbench\/core\/client["']\s*\)/g;
31
+ for (const match of text.matchAll(BINDING)) bindings.add(match[1]);
32
+ if (bindings.size === 0) {
33
+ // 认不出绑定名时不静默放过:退化成"全文里任何 `<id>.kit.` 的 id"。
34
+ for (const match of text.matchAll(/([A-Za-z_$][\w$]*)\.kit\./g)) bindings.add(match[1]);
35
+ }
36
+
37
+ const used = new Map();
38
+ const add = (bucket, member) => {
39
+ if (bucket === undefined || member === undefined || member === '') return;
40
+ if (!used.has(bucket)) used.set(bucket, new Set());
41
+ used.get(bucket).add(member);
42
+ };
43
+ for (const id of bindings) {
44
+ const dotted = new RegExp(`\\b${id}\\.kit\\.([a-z]+)\\.([A-Za-z0-9_$]+)`, 'g');
45
+ for (const match of text.matchAll(dotted)) add(match[1], match[2]);
46
+ const destructured = new RegExp(`\\{([^{}]*)\\}\\s*=\\s*${id}\\.kit\\.([a-z]+)\\s*;`, 'g');
47
+ for (const match of text.matchAll(destructured)) {
48
+ for (const raw of match[1].split(',')) {
49
+ const name = raw.split(':')[0].trim();
50
+ if (name !== '') add(match[2], name);
51
+ }
52
+ }
53
+ }
54
+ return used;
55
+ }
56
+
57
+ function shellKitOf(clientPath) {
58
+ const stub = () => null;
59
+ const seed = new Proxy(
60
+ { default: stub },
61
+ { get: (target, key) => (key in target ? target[key] : stub) },
62
+ );
63
+ // react 得给一个**能 extends 的类**:外壳自己那段 `class CardBoundary extends Component`
64
+ // 在工厂体里就跑(错误边界只能这么写),换成箭头函数会当场 "not a constructor"。
65
+ const react = new Proxy(
66
+ { default: stub, Component: class {} },
67
+ { get: (target, key) => (key in target ? target[key] : stub) },
68
+ );
69
+ let exports;
70
+ const load = (registration) => {
71
+ exports = registration.factory((specifier) => {
72
+ if (specifier === 'react' || specifier === 'react/jsx-runtime' || specifier === 'react-dom') return react;
73
+ if (specifier.startsWith('@deepseek-ai/')) return seed;
74
+ if (specifier === SHELL) return exports;
75
+ throw new Error(`外壳产物里出现了一个不认识的 require("${specifier}")`);
76
+ });
77
+ };
78
+ const window = { __ModuleLoader__: { load } };
79
+ // eslint-disable-next-line no-new-func -- 要的就是"按浏览器加载器那样把这份产物跑一遍",静态解析会骗人
80
+ new Function('window', readFileSync(clientPath, 'utf8'))(window);
81
+ return exports?.kit ?? {};
82
+ }
83
+
84
+ function shellClientPathOf(pkgDir) {
85
+ const override = process.env.DSH_WB_CORE_CLIENT;
86
+ if (override !== undefined && override.trim() !== '') return override.trim();
87
+ try {
88
+ const require = createRequire(join(pkgDir, 'package.json'));
89
+ return join(dirname(require.resolve(`${SHELL}/package.json`)), 'client.js');
90
+ } catch {
91
+ return undefined;
92
+ }
93
+ }
94
+
95
+ function floorOf(range) {
96
+ if (typeof range !== 'string') return undefined;
97
+ const match = /(\d+)\.(\d+)\.(\d+)/.exec(range);
98
+ if (match === null) return undefined;
99
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
100
+ }
101
+
102
+ function below(a, b) {
103
+ for (let at = 0; at < 3; at += 1) {
104
+ if (a[at] !== b[at]) return a[at] < b[at];
105
+ }
106
+ return false;
107
+ }
108
+
109
+ const problems = [];
110
+ let inspected = 0;
111
+ let noArtifact = 0;
112
+ let noShell = 0;
113
+ const override = process.env.DSH_WB_CORE_CLIENT;
114
+
115
+ for (const pkg of discoverModulePackages({ root: process.cwd() })) {
116
+ const name = pkg.manifest.name;
117
+ const client = join(pkg.dir, 'client.js');
118
+ if (!existsSync(client)) {
119
+ noArtifact += 1;
120
+ continue;
121
+ }
122
+ inspected += 1;
123
+ const text = readFileSync(client, 'utf8');
124
+ const used = kitUsageOf(text);
125
+ const uses = used.size > 0 ? text.includes(KIT_SPECIFIER) : false;
126
+
127
+ if (uses) {
128
+ const shellClient = shellClientPathOf(pkg.dir);
129
+ if (shellClient === undefined || !existsSync(shellClient)) {
130
+ noShell += 1;
131
+ } else {
132
+ let kit;
133
+ try {
134
+ kit = shellKitOf(shellClient);
135
+ } catch (error) {
136
+ problems.push(
137
+ `${name}: 取不出这份外壳的 kit(${shellClient})—— ${error instanceof Error ? error.message : String(error)}。` +
138
+ `执行产物是这份判据唯一可信的取数方式;看不懂就别放着不管`,
139
+ );
140
+ kit = undefined;
141
+ }
142
+ if (kit !== undefined) {
143
+ const missing = [];
144
+ for (const [bucket, members] of used) {
145
+ for (const member of [...members].sort()) {
146
+ if (kit[bucket]?.[member] === undefined) missing.push(`kit.${bucket}.${member}`);
147
+ }
148
+ }
149
+ if (missing.length > 0) {
150
+ problems.push(
151
+ `${name}: 产物用了这份外壳(${shellClient})里没有的 ${missing.join('、')} —— ` +
152
+ `顶层解构不会抛(拿到 undefined),卡片首帧渲染才炸 "Element type is invalid",` +
153
+ `而外壳没有错误边界 ⇒ 整块面板会跟着没。要么去掉/换掉这些成员,要么把 peer 下限提到真正需要的那版`,
154
+ );
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ const peerFloor = floorOf(pkg.manifest.peerDependencies?.[SHELL]);
161
+ const devFloor = floorOf(pkg.manifest.devDependencies?.[SHELL]);
162
+ if (peerFloor !== undefined && devFloor !== undefined && below(peerFloor, devFloor) && override === undefined) {
163
+ problems.push(
164
+ `${name}: peer 下限 ${pkg.manifest.peerDependencies[SHELL]} 比 devDependency ${pkg.manifest.devDependencies[SHELL]} 还旧 —— ` +
165
+ `你对用户承诺的那版外壳,产物从没对着验过。把 peer 下限提到与 devDependency 同档;` +
166
+ `确实要支持更旧的外壳,就用 DSH_WB_CORE_CLIENT=<那版外壳的 client.js> 跑一遍 check --only kit 证明面够用`,
167
+ );
168
+ }
169
+ }
170
+
171
+ if (problems.length > 0) {
172
+ console.error(`✖ 外壳 kit 面 ${problems.length} 条不过:`);
173
+ for (const problem of problems) console.error(` · ${problem}`);
174
+ process.exit(1);
175
+ }
176
+
177
+ const notes = [
178
+ `${inspected} 个模块的产物对过外壳 kit 面`,
179
+ override === undefined ? '(按各自装的那份外壳)' : `(DSH_WB_CORE_CLIENT=${override})`,
180
+ ];
181
+ if (noArtifact > 0) notes.push(`${noArtifact} 个还没建产物(跳过)`);
182
+ if (noShell > 0) notes.push(`${noShell} 个没装外壳(跳过面差)`);
183
+ console.log(`✔ 外壳 kit 面通过:${notes.join(' · ')}`);