@dsh-workbench/module-tools 0.3.10 → 0.3.11
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/README.md +2 -2
- package/package.json +2 -2
- package/src/checks/contracts.mjs +10 -6
- package/src/checks/source.mjs +83 -1
- package/template-diy/src/messages.ts.tpl +4 -4
package/README.md
CHANGED
|
@@ -23,8 +23,8 @@ dsh-wb-module publish # 发货前的清单:门禁 + 三张表 +
|
|
|
23
23
|
|---|---|
|
|
24
24
|
| `contracts` | 模块契约:单元 id 唯一、两个面、四个脚本、`files`、peer 下限;以及**清单 ↔ 代码**(真正注册的工具)与**底稿 ↔ 清单**(`design.json` 的能力名)两处对账(见下) |
|
|
25
25
|
| `protocol` | 模块声明协议:`dsh.workbench` 的 `apiVersion` 要在、静态半形状要对、不许有协议不认识的键(`scope` 现在**就是**不认识的键之一 —— 见下) |
|
|
26
|
-
| `bundles` | 产物边界:宿主面里不许出现浏览器侧的东西;浏览器产物里不许出现**内联的外壳 CSS
|
|
27
|
-
| `source` | 源码边界:不直接引 dsh 的包、不引不是种子词的 `@deepseek-ai
|
|
26
|
+
| `bundles` | 产物边界:宿主面里不许出现浏览器侧的东西;浏览器产物里不许出现**内联的外壳 CSS**(构建会把来自 `@dsh-workbench/shared` / `core` 的 CSS 丢掉,这里是兜底:漏进来一条就说明构建那条闸被绕过了);**产物里出现夹具标记(`wbMock` / `MockStrip` / `useFixturePick` / `-mock'` / `SCENES`…)判红** —— 模块不带夹具:夹具只服务工作台开发者的调试,模块自己的 vitest 把几态各喂一遍就够 |
|
|
27
|
+
| `source` | 源码边界:不直接引 dsh 的包、不引不是种子词的 `@deepseek-ai/*`;**共享包的两个入口按运行时分开** —— 根入口(浏览器 barrel)**只许 `import type`**(取运行值:浏览器侧会打进第二份实例、宿主侧会让 `dist/host.js` 在 Node 里 import 不进来);`/host` 只许**宿主侧**取运行值(里面有 `node:crypto` / `node:fs`) |
|
|
28
28
|
| `primitives` | dsh 原语只从模块表(`kit`)拿 |
|
|
29
29
|
| `kit` | 这份外壳的 kit 成员到底有哪些(缺什么会当场点名) |
|
|
30
30
|
| `cards` | 卡片渲染:**八态 × zh/en 各渲一遍**(外壳侧输入 idle / loading / loading-刷新保留旧值(含旧时间戳 `at`)/ ready / ready-空容器 / unreadable-error / unreadable-能力闸缺插件 / unreadable-能力闸缺命令;`useApiResource` 与 `useCapabilityGate` 由判据喂,`useUnitBoard` 用模块仓里装的那份**真壳**)—— 不许抛、不许空白、文字里不许出现 `undefined` / `NaN` / `[object Object]` / `null`、不许裸键名、要套外壳的卡壳、清单 `titleKey` 在两语字典里都有落点。**不断言"画的是哪一态那一屏"**(那归模块自己的逐态测试与 `design.json` 状态矩阵);模块没装外壳时逐态那半跳过、单次全桩渲染照旧跑 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-workbench/module-tools",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"description": "模块包的构建与门禁工具:build(单文件 ESM 宿主侧 + 套 loader 壳的浏览器侧)、check(契约/产物边界/源码边界/卡片渲染)。模块包不再各带一份构建脚本。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"eslint-plugin-react": "^7.37.5",
|
|
30
30
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
31
31
|
"typescript-eslint": "^8.71.0",
|
|
32
|
-
"@dsh-workbench/shared": "0.3.
|
|
32
|
+
"@dsh-workbench/shared": "0.3.11"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"selfcheck": "node scripts/selfcheck/index.mjs"
|
package/src/checks/contracts.mjs
CHANGED
|
@@ -964,6 +964,9 @@ export async function run() {
|
|
|
964
964
|
// `{ kind: 'ask', reason, question{zh,en}, fallback{zh,en} }`(要问,且问得清)。
|
|
965
965
|
// 别的返回值(**包括 undefined**)外壳会当场 deny —— 那才是要拦的。
|
|
966
966
|
const read = verdict?.kind === 'read';
|
|
967
|
+
// deny(ADR-0032 第五态):套件说"这一通根本跑不成" —— 外壳在备料之前就拒,连问都不问。
|
|
968
|
+
// 合成调用上它合法,但和 read 一样验不到"真的调用会不会问",进 notes 让作者自己确认。
|
|
969
|
+
const deny = verdict?.kind === 'deny' && text(verdict.reason) !== '';
|
|
967
970
|
const ask =
|
|
968
971
|
verdict?.kind === 'ask' &&
|
|
969
972
|
text(verdict.reason) !== '' &&
|
|
@@ -971,19 +974,20 @@ export async function run() {
|
|
|
971
974
|
text(verdict.question?.en) !== '' &&
|
|
972
975
|
text(verdict.fallback?.zh) !== '' &&
|
|
973
976
|
text(verdict.fallback?.en) !== '';
|
|
974
|
-
if (!read && !ask) {
|
|
977
|
+
if (!read && !deny && !ask) {
|
|
975
978
|
problems.push(
|
|
976
979
|
`${name}: 工具 ${tool.name} 的清单里写着 writes:true,但 needsApproval 对合成调用 ` +
|
|
977
980
|
`${JSON.stringify(call.arguments)} 没给出合法口径 —— ` +
|
|
978
|
-
`要么 { kind: 'read' }(这件事不用问),要么 { kind: '
|
|
981
|
+
`要么 { kind: 'read' }(这件事不用问),要么 { kind: 'deny', reason }(这一通根本跑不成,` +
|
|
982
|
+
`理由原样交给 agent),要么 { kind: 'ask', reason, question: {zh,en}, ` +
|
|
979
983
|
`fallback: {zh,en} }(要问,且问得清)。别的返回值外壳会当场 deny${cannotShape}`,
|
|
980
984
|
);
|
|
981
985
|
continue;
|
|
982
986
|
}
|
|
983
|
-
// read:这次调用不用点头。**不查 approvalDetail** ——
|
|
987
|
+
// read / deny:这次调用不用点头。**不查 approvalDetail** —— 外壳在这两档都不会去问它
|
|
984
988
|
// (`approvalFor` 只在 verdict 是 ask 时才调 detailOf)。合成调用本来就不是一次真实写入,
|
|
985
989
|
// 拿它逼模块给"这次会发生什么"就是让判据替外壳问一个外壳不会问的问题。
|
|
986
|
-
if (read) {
|
|
990
|
+
if (read || deny) {
|
|
987
991
|
readOnProbe.push(tool.name);
|
|
988
992
|
continue;
|
|
989
993
|
}
|
|
@@ -1046,8 +1050,8 @@ export async function run() {
|
|
|
1046
1050
|
}
|
|
1047
1051
|
if (readOnProbe.length > 0) {
|
|
1048
1052
|
probeNotes.push(
|
|
1049
|
-
`写工具 ${readOnProbe.join(' / ')} 按合成调用回了"这次不用问"(kind: 'read')—— ` +
|
|
1050
|
-
`判据只能验到这一步:它是不是"参数里带 confirm 才真的写"那种**合法**口径,` +
|
|
1053
|
+
`写工具 ${readOnProbe.join(' / ')} 按合成调用回了"这次不用问"(kind: 'read' 或 'deny')—— ` +
|
|
1054
|
+
`判据只能验到这一步:它是不是"参数里带 confirm 才真的写" / "这一通真跑不成"那种**合法**口径,` +
|
|
1051
1055
|
`合成调用看不出来(必填项给的是占位值,不是真实写入参数)。作者自己确认一次:` +
|
|
1052
1056
|
`真的调用会不会 ask、ask 时 approvalDetail 说没说清`,
|
|
1053
1057
|
);
|
package/src/checks/source.mjs
CHANGED
|
@@ -13,6 +13,17 @@
|
|
|
13
13
|
// 装机预检与外壳都不读它。所以这里必须拿**源码**入口 —— 从产物 dist/host.js 反推不出是哪一份源码
|
|
14
14
|
// 把 dsh 的包拖进来的。
|
|
15
15
|
//
|
|
16
|
+
// 共享包(`@dsh-workbench/shared`)的**两个入口也是两个运行时**,这里守它的「运行值」边界:
|
|
17
|
+
// · 根入口 `.` 是浏览器 barrel(组件 + 它们自己的样式)。**两侧都不许从它取运行值**:
|
|
18
|
+
// 浏览器侧该走 kit(`@dsh-workbench/core/client`,单例只有那一份),宿主侧该走 `/host` ——
|
|
19
|
+
// 根 barrel 会在 `dist/host.js` 里把 dsh 的浏览器件一路拉进来,Node 里 import 不进来(实测)。
|
|
20
|
+
// · 宿主入口 `/host` 里有 node:crypto / node:fs,**只有宿主侧能引**;浏览器产物装不下。
|
|
21
|
+
// · 类型是例外:`import type` / `export type`(含逐个 `type X`)编译期擦除,两侧随便引 ——
|
|
22
|
+
// 契约类型(`Translate` / `ApiError` / `UnitClientFace` …)正是从根入口拿的。
|
|
23
|
+
//
|
|
24
|
+
// 为什么这一条住在**源码**判据里:它要知道"这个文件属于哪一侧",而侧只能从两个源码入口做可达性分析
|
|
25
|
+
// 得到(产物里看不出是哪份源码把东西拖进来的)。判据的负例在 `scripts/selfcheck/checks/negative-probe.mjs`。
|
|
26
|
+
//
|
|
16
27
|
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
17
28
|
// 打印与退出码归 `../check.mjs` 那一层。
|
|
18
29
|
|
|
@@ -25,6 +36,65 @@ import { isSeedSpecifier } from '../seed.mjs';
|
|
|
25
36
|
// 模块入口常见写法是 `await import('./views/…')`,漏了就会把可达文件判成"死代码"。
|
|
26
37
|
const IMPORT = /from\s*'([^']+)'|import\s*\(?\s*'([^']+)'/g;
|
|
27
38
|
|
|
39
|
+
// 共享包那两条规则要分清「取运行值」与「只取类型」,所以按**整条语句**匹配(`import { a }` 与
|
|
40
|
+
// `import type { A }` 的差别在语句开头与花括号里)。片段里不许出现引号或分号:否则一条 `import type`
|
|
41
|
+
// 会把后面那条运行值 import 一起吞进来,判据就漏了。
|
|
42
|
+
const SHARED_FROM =
|
|
43
|
+
/(?:^|[;}\s])(import|export)\s+(type\s+)?([^'";]{0,600}?)\s*from\s*['"](@dsh-workbench\/shared(?:\/host)?)['"]/g;
|
|
44
|
+
// 动态 `import('…')` 一定是运行值(`import type` 没有这个写法)。
|
|
45
|
+
const SHARED_DYNAMIC = /import\s*\(\s*['"](@dsh-workbench\/shared(?:\/host)?)['"]/g;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 这条 import/export 是不是**只取类型**:`import type …`、`export type …`、
|
|
49
|
+
* 以及每个具名都带 `type` 的 `import { type A, type B }`。
|
|
50
|
+
* `import X, { type A }` / `import * as ns` / `export *` 都算运行值。
|
|
51
|
+
*/
|
|
52
|
+
function typeOnlyOf(keyword, clause) {
|
|
53
|
+
if (keyword !== undefined) return true;
|
|
54
|
+
const braces = /\{([\s\S]*)\}/.exec(clause);
|
|
55
|
+
if (braces === null) return false;
|
|
56
|
+
if (clause.slice(0, braces.index).replace(/[,\s]/g, '') !== '') return false;
|
|
57
|
+
const parts = braces[1]
|
|
58
|
+
.split(',')
|
|
59
|
+
.map((one) => one.trim())
|
|
60
|
+
.filter((one) => one !== '');
|
|
61
|
+
return parts.length > 0 && parts.every((one) => one.startsWith('type '));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const SIDE_TEXT = {
|
|
65
|
+
client: '客户端侧可达',
|
|
66
|
+
host: '宿主侧可达',
|
|
67
|
+
both: '两个入口都到得了',
|
|
68
|
+
orphan: '两个入口都到不了(可能是死代码)',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** 共享包两个入口的「运行值」违规 → 一句人话(说清为什么、改哪、类型怎么引)。 */
|
|
72
|
+
function sharedProblem(name, rel, specifier, side, dynamic) {
|
|
73
|
+
const what = dynamic ? `动态 import 了 ${specifier}` : `从 ${specifier} 取了运行值`;
|
|
74
|
+
if (specifier === '@dsh-workbench/shared') {
|
|
75
|
+
const fix =
|
|
76
|
+
side === 'host'
|
|
77
|
+
? '宿主侧改从 @dsh-workbench/shared/host 引'
|
|
78
|
+
: side === 'client'
|
|
79
|
+
? '浏览器侧改从 kit(@dsh-workbench/core/client)引;kit 里没有的,先看它是不是该补进 kit'
|
|
80
|
+
: side === 'both'
|
|
81
|
+
? '这个文件两个入口都到得了:宿主要的走 /host、浏览器要的走 kit —— 两边的运行值挤在一个文件里就拆开'
|
|
82
|
+
: '这个文件两个入口都到不了(可能是死代码),证明不了它在哪一侧;挪进真正的面再引';
|
|
83
|
+
return (
|
|
84
|
+
`${name}: ${rel} 里${what} —— 根入口是浏览器 barrel` +
|
|
85
|
+
`(组件 + 它们自己的样式),两侧都不该从这里取运行值:浏览器侧会打进第二份实例、宿主侧会让 ` +
|
|
86
|
+
`dist/host.js 在 Node 里 import 不进来(实测:一路拉到 dsh 的浏览器件)。${fix}。` +
|
|
87
|
+
`只取类型就写 \`import type\`(编译期擦除、两侧都行)`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (side === 'host') return undefined;
|
|
91
|
+
return (
|
|
92
|
+
`${name}: ${rel} 里${what} —— 宿主入口里有 node:crypto / node:fs,` +
|
|
93
|
+
`只有宿主侧能引(这个文件${SIDE_TEXT[side]});浏览器产物装不下它。` +
|
|
94
|
+
`浏览器侧要的纯函数走 kit(@dsh-workbench/core/client)或自己写一个;只取类型就写 \`import type\``
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
28
98
|
function sourceFiles(dir, acc = []) {
|
|
29
99
|
for (const entry of readdirSync(dir)) {
|
|
30
100
|
const full = join(dir, entry);
|
|
@@ -189,6 +259,17 @@ export async function run() {
|
|
|
189
259
|
);
|
|
190
260
|
}
|
|
191
261
|
}
|
|
262
|
+
|
|
263
|
+
// 共享包的两个入口:根入口只许取类型、宿主入口只许宿主侧取运行值(理由见文件头)。
|
|
264
|
+
for (const match of text.matchAll(SHARED_FROM)) {
|
|
265
|
+
if (typeOnlyOf(match[2], match[3])) continue;
|
|
266
|
+
const problem = sharedProblem(name, rel, match[4], sideOf(file), false);
|
|
267
|
+
if (problem !== undefined) problems.push(problem);
|
|
268
|
+
}
|
|
269
|
+
for (const match of text.matchAll(SHARED_DYNAMIC)) {
|
|
270
|
+
const problem = sharedProblem(name, rel, match[1], sideOf(file), true);
|
|
271
|
+
if (problem !== undefined) problems.push(problem);
|
|
272
|
+
}
|
|
192
273
|
}
|
|
193
274
|
}
|
|
194
275
|
|
|
@@ -207,7 +288,8 @@ export async function run() {
|
|
|
207
288
|
notes: [
|
|
208
289
|
`${scanned} 个源文件 · 不伸进外壳 app/ · 不跨模块 · ` +
|
|
209
290
|
`平台知识要么在自己包里、要么走平台包(不许两样都有)· ` +
|
|
210
|
-
`dsh 的浏览器包只在客户端侧可用(${seedUsers}
|
|
291
|
+
`dsh 的浏览器包只在客户端侧可用(${seedUsers} 处)· ` +
|
|
292
|
+
`共享包根入口只取类型、/host 只在宿主侧取运行值`,
|
|
211
293
|
],
|
|
212
294
|
problems: [],
|
|
213
295
|
};
|
|
@@ -3,9 +3,9 @@ export const messages = {
|
|
|
3
3
|
'__UNIT__.cardTitle': '目录概览',
|
|
4
4
|
'__UNIT__.needProject': '先打开一个项目',
|
|
5
5
|
'__UNIT__.loading': '正在读目录…',
|
|
6
|
-
'__UNIT__.loadingHint': '
|
|
6
|
+
'__UNIT__.loadingHint': '正在统计文件数、目录数与最大的几个文件',
|
|
7
7
|
'__UNIT__.errorTitle': '读不到这个目录',
|
|
8
|
-
'__UNIT__.errorHint': '
|
|
8
|
+
'__UNIT__.errorHint': '先确认这个目录还在、你也能读它,然后点右上角刷新重试',
|
|
9
9
|
'__UNIT__.empty': '这个目录是空的:没有文件,也没有子目录',
|
|
10
10
|
'__UNIT__.largest': '最大的几个文件',
|
|
11
11
|
'__UNIT__.fileUnit': ' 个文件',
|
|
@@ -15,9 +15,9 @@ export const messages = {
|
|
|
15
15
|
'__UNIT__.cardTitle': 'Directory overview',
|
|
16
16
|
'__UNIT__.needProject': 'Open a project first',
|
|
17
17
|
'__UNIT__.loading': 'Reading the directory…',
|
|
18
|
-
'__UNIT__.loadingHint': '
|
|
18
|
+
'__UNIT__.loadingHint': 'Counting files, folders, and the largest files',
|
|
19
19
|
'__UNIT__.errorTitle': 'Cannot read this directory',
|
|
20
|
-
'__UNIT__.errorHint': '
|
|
20
|
+
'__UNIT__.errorHint': 'Check that the folder still exists and you can read it, then hit refresh in the header',
|
|
21
21
|
'__UNIT__.empty': 'This directory is empty: no files and no subdirectories',
|
|
22
22
|
'__UNIT__.largest': 'Largest files',
|
|
23
23
|
'__UNIT__.fileUnit': ' files',
|