@dsh-workbench/module-tools 0.3.3 → 0.3.5
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 +1 -1
- package/bin/dsh-wb-module.mjs +3 -0
- package/package.json +2 -2
- package/src/arch.mjs +25 -3
- package/src/check-flags.mjs +22 -0
- package/src/check.mjs +21 -2
- package/src/checks/kit.mjs +68 -19
- package/src/checks/primitives.mjs +239 -5
- package/src/checks/style.mjs +231 -13
- package/src/declared-tools.mjs +1 -1
- package/src/design.mjs +2 -1
- package/src/discover.mjs +9 -1
- package/src/init.mjs +33 -0
- package/src/lint.mjs +1 -1
- package/src/primitives-surface.mjs +120 -0
- package/src/shell-type-files.mjs +119 -0
- package/template/package.json.tpl +2 -1
- package/template-diy/package.json.tpl +2 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ dsh-wb-module publish # 发货前的清单:门禁 + 三张表 +
|
|
|
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` 状态矩阵);模块没装外壳时逐态那半跳过、单次全桩渲染照旧跑 |
|
|
31
31
|
| `standalone` | 独立构建:产物不依赖工坊里跑 |
|
|
32
32
|
| `markers` | 公开包无内部标识(内网域名、企业内部名) |
|
|
33
|
-
| `style` | 样式归属:模块不许定义外壳类(`wb-*`)的规则(判据看**复合选择器**,`:global(.wb-x)` 也算,只有同一个复合选择器里带模块自己的类才放行);**裸 `<a>` 与裸交互控件(`<button>` / `<input>` / `<select>`)都判红** —— 前者用 kit 的 `Link
|
|
33
|
+
| `style` | 样式归属:模块不许定义外壳类(`wb-*`)的规则(判据看**复合选择器**,`:global(.wb-x)` 也算,只有同一个复合选择器里带模块自己的类才放行);**裸 `<a>` 与裸交互控件(`<button>` / `<input>` / `<select>`)都判红** —— 前者用 kit 的 `Link`(**只判「正文里的文字链」那一种**;三种 `Link` 表达不了的形态不判、逐文件收在 `ANCHOR_EXEMPT` 里:**图标链**(`<a className="wb-iconButton">` 包一枚图标 —— `Link` 会多画一颗箭头)· **按钮皮的链**(`.anchorButton` 那类"看起来像按钮、实际是链接",ADR-0054 §11 判过:必须是链接的动作留真 `<a>`,不许塞成 `window.open` 按钮)· **整行链**(`.issueLink` 那类 `flex: 1` / `color: inherit` 的行的布局槽)。`<a` **写在行尾**(属性在下一行)也算 —— 这条是**全文**匹配、不逐行,别改回去),后者用 `Button` / `TextField` / `SelectField` / `ChoiceGroup`;只有 `<textarea>` 与 `input[type=hidden\|file]` 例外(两处都没有那种形态);**手写的「标签 + 控件」行(标签元素紧邻 `SelectField` / `TextField` / `ChoiceGroup` / `Input`)与模块自己给 `field`/`form` 容器设的行距(`gap` / 上下 `margin`)也判红** —— 这一层归 kit 的 `FormRow(s)`(面 → 行 → 控件);现存违规收进 `FORM_ROW_EXEMPT`(棘轮:迁一处删一行,且表里已经不再命中的行会当场报红);**手写的操作区(借了 `wb-formActions` 槽类手摆按钮)也判红** —— 操作区归 kit 的 `FormActions`(顺序 / 一个区一颗 `primary` / `danger` 的位置 / 间距 / 窄容器换行都由它定,调用点只给数据与语义),现存违规收进 `FORM_ACTIONS_EXEMPT`(同一套棘轮:迁一处删一行,表里不再命中的行当场报红);**手写的整张表单也判红** —— 模块源码(`.tsx` / `.ts`,测试文件除外)里的**裸 `<form>` 元素**与**手写行的槽类 `wb-formRow` / `wb-formRows` / `wb-field`** 都算,整张表单的面(行的排布 / 校验的时机 / 错误摆哪 / 提交怎么被挡住 / 第一处错误怎么找 / 操作区怎么排)归 kit 的 `Form`(`fields` 进、其余全归它,调用点只给数据与语义),现存违规收进 `FORM_EXEMPT`(同一套棘轮:迁一处删一行,表里不再命中的行当场报红);那一族判据**故意窄**(`wb-field` 后面跟着 `Label`/`Hint` 这些**兄弟**类的不算,测试文件也算在射程外),模块自己用 div 拼一张既没有 `<form>`、也没借槽类的表单**不判** —— 判宽了就是"用户每次改界面都先撞门禁",已知漏报见 `style.mjs` 里那一段注释 |
|
|
34
34
|
| `alias` | 模块不指外壳:源码里没有指向外壳 `@/` 的引用 |
|
|
35
35
|
| `arch` | 模块自己的**分层形状**(下面单开一节):层序表外的目录 / 反向依赖 / 跨运行时 / 巨文件 / 模型层没测试 / 样式里手搓数值与 `!important`。**只跑带 `arch.config.mjs` 的模块** —— 没有这份文件就是 `○ 不适用`,不是红 |
|
|
36
36
|
| `lint` | 模块自己的**语法卫生**(下面单开一节):`as any` / 残留 `console` / `debugger` / 内联颜色字面量 / 字符串里的 `!important` / react-hooks。**只跑带 `eslint.config.mjs` 的模块** —— 没有那份文件就是 `○ 不适用`,不是红;规则住在 module-tools 的方言里、随工具链同代 |
|
package/bin/dsh-wb-module.mjs
CHANGED
|
@@ -55,6 +55,9 @@ const commands = { build, check, init, design, publish, sync, lint };
|
|
|
55
55
|
if (!command || !commands[command]) {
|
|
56
56
|
console.error(`用法:dsh-wb-module <build|check|init|design|publish|sync|lint> [--cwd <目录>]
|
|
57
57
|
dsh-wb-module check [--only ${Object.keys(CHECKS).join('|')}]
|
|
58
|
+
dsh-wb-module check --only kit # 单跑当查询口:这份外壳的 kit 里有什么(--file <名字> 给类型文件绝对路径)
|
|
59
|
+
dsh-wb-module check --only primitives [--all|--files|--json]
|
|
60
|
+
# 单跑当查询口:dsh 原语里有什么 + --file <名字> 给类型文件绝对路径
|
|
58
61
|
dsh-wb-module init <unit> [--dir <目录>] [--template minimal|diy] [--description <文字>]
|
|
59
62
|
dsh-wb-module design <init|check|render|accept> # 三张表:填、查空格、生成 README、自验打勾
|
|
60
63
|
dsh-wb-module sync # 按清单生成 src/declared-tools.ts(工具名只有清单一处写者)
|
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.5",
|
|
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.5"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"selfcheck": "node scripts/selfcheck/index.mjs"
|
package/src/arch.mjs
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
styles,
|
|
50
50
|
} from '@arch-guard/core/presets';
|
|
51
51
|
import { DECLARED_TOOLS_FILE } from './declared-tools.mjs';
|
|
52
|
+
import { SEED_SPECIFIERS } from './seed.mjs';
|
|
52
53
|
|
|
53
54
|
// 两个面**就是** `dsh.workbenchFaces` 指的那两个文件(协议要求,见 ADR-0038)。改这两个名字
|
|
54
55
|
// 等于改声明,判据(contracts / source)先红 —— 所以这里是常量,不是选项。
|
|
@@ -119,9 +120,28 @@ const STYLE_EXCEPTION = {
|
|
|
119
120
|
'dsh 改名会由外壳升级暴露,而不是由模块本地的表兜住。',
|
|
120
121
|
};
|
|
121
122
|
|
|
122
|
-
// 模块包**运行时**允许的依赖:react(外壳经模块表提供)+
|
|
123
|
+
// 模块包**运行时**允许的依赖:react(外壳经模块表提供)+ 外壳自己那两个包 + **平台种子词那一整族**。
|
|
124
|
+
//
|
|
125
|
+
// 种子词为什么要一起放进来(2026-10-19 补,用户口径「arch 没加依赖」):那些包是 dsh 的模块表
|
|
126
|
+
// 天生就提供的(`seed.mjs` 是唯一真相),模块引它们走 `dsh.client.external` 是**被允许的路**
|
|
127
|
+
// (ADR-0028 §5 / ADR-0063)。但作者把它们写进 `dependencies` 时,P01 会报「未登记的运行时依赖」,
|
|
128
|
+
// 而 arch-guard 给的那句修法是「加进 `deps({ allow })`」—— 对平台件这是**误导**:它逼每个模块
|
|
129
|
+
// 为一条本来就合法的事开口子。所以这里按 `seed.mjs` 推导(dsh 加词,这张表自己跟着长),
|
|
130
|
+
// 表外的包(真正的第三方 SDK)照旧要明着登记。
|
|
131
|
+
//
|
|
132
|
+
// 推荐写法仍是 **devDependency(类型)+ `dsh.client.external`**:放进 `dependencies` 不会报错,
|
|
133
|
+
// 但 pnpm 会给模块装一份**运行时用不到的**副本(产物里是 `require`,实例由外壳给)。
|
|
134
|
+
// 那是"浪费"不是"错误",所以不由 arch 判;由 `check:source` / `check:primitives` 守真正的错误
|
|
135
|
+
// (宿主侧碰它、漏声明 external、产物被内联)。
|
|
123
136
|
// `@dsh-workbench/module-tools` 是构建/门禁工具,是 devDependency,不进这张表。
|
|
124
|
-
const
|
|
137
|
+
const SEED_PACKAGES = [
|
|
138
|
+
...new Set(
|
|
139
|
+
SEED_SPECIFIERS.map((name) => (name.startsWith('@') ? name.split('/').slice(0, 2).join('/') : name.split('/')[0])),
|
|
140
|
+
),
|
|
141
|
+
];
|
|
142
|
+
const ALLOWED_DEPS = [
|
|
143
|
+
...new Set([...SEED_PACKAGES, '@dsh-workbench/core', '@dsh-workbench/shared']),
|
|
144
|
+
];
|
|
125
145
|
|
|
126
146
|
// 我们**不拥有**主题:颜色 / 圆角 / 字号 / 时长一律引用 dsh 的 `--dsw-*` 令牌,
|
|
127
147
|
// 所以这里没有 tokenDir / palette(声明一个自己的令牌表就是编造一处不存在的真相,
|
|
@@ -150,7 +170,9 @@ const DESIGN = {
|
|
|
150
170
|
* 复杂模块要开口子时**从参数进**(每条口子都有名字,评审时看得见),别改判据:
|
|
151
171
|
*
|
|
152
172
|
* - `fileLines` / `viewLines` / `functionLines`:放宽体积阈值(默认 200 / 200 / 80)
|
|
153
|
-
* - `deps`:追加允许的运行时依赖(比如宿主侧接了某个 SDK
|
|
173
|
+
* - `deps`:追加允许的运行时依赖(比如宿主侧接了某个 SDK)。dsh 的**平台种子词**
|
|
174
|
+
* (`seed.mjs` 那一族:`react` / `zod` / `@deepseek-ai/dsh-client-*` …)与 `@dsh-workbench/*`
|
|
175
|
+
* 已经按种子表推导进表里,不用为它们开口子
|
|
154
176
|
* - `roles`:追加角色(自定义目录 / 文件;`{ id, pattern, layer }`)。
|
|
155
177
|
* 内置已有三条**文件级**角色:`src/messages.ts`、`src/declared-tools.ts`,以及 `__tests__/`
|
|
156
178
|
* 目录下的文件(`src/` 内任意一层)—— 后两条一条是 `sync` 生成的、一条是方言声明的测试家,
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
export function flagsOf() {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(process.env.DSH_WB_ONLY_ARGS ?? '[]');
|
|
10
|
+
return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
11
|
+
} catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** `--file <名字>` 那种带值的开关取它的值(没给值返回 undefined)。 */
|
|
17
|
+
export function flagValue(flags, flag) {
|
|
18
|
+
const at = flags.indexOf(flag);
|
|
19
|
+
if (at === -1) return undefined;
|
|
20
|
+
const value = flags[at + 1];
|
|
21
|
+
return value === undefined || value.startsWith('--') ? undefined : value;
|
|
22
|
+
}
|
package/src/check.mjs
CHANGED
|
@@ -19,6 +19,15 @@
|
|
|
19
19
|
// 不适用 ○ <title>:(不适用)<notes>
|
|
20
20
|
// 失败 ✖ <title> <N> 条不过: 然后逐条 ` · <problem>`
|
|
21
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
|
+
// 这是加法,不是新形状。
|
|
22
31
|
|
|
23
32
|
// 名字与 title 一字不变 —— selfcheck 会把它与各判据模块导出的 `title` 逐个对一遍。
|
|
24
33
|
export const CHECKS = {
|
|
@@ -58,11 +67,13 @@ function report(title, result) {
|
|
|
58
67
|
for (const note of notes.slice(1)) console.log(` ${note}`);
|
|
59
68
|
}
|
|
60
69
|
|
|
61
|
-
export async function check({ only } = {}) {
|
|
70
|
+
export async function check({ only, args = [] } = {}) {
|
|
62
71
|
const names = only === undefined ? Object.keys(CHECKS) : [only];
|
|
72
|
+
const jsonMode = args.includes('--json');
|
|
63
73
|
const tally = { pass: 0, skip: 0, fail: 0 };
|
|
64
74
|
const skipped = [];
|
|
65
75
|
let ran = 0;
|
|
76
|
+
process.env.DSH_WB_ONLY_ARGS = JSON.stringify(args);
|
|
66
77
|
try {
|
|
67
78
|
for (const name of names) {
|
|
68
79
|
const entry = CHECKS[name];
|
|
@@ -89,10 +100,18 @@ export async function check({ only } = {}) {
|
|
|
89
100
|
}
|
|
90
101
|
tally[result.status] += 1;
|
|
91
102
|
if (result.status === 'skip') skipped.push(`${title}(${result.notes[0] ?? ''})`);
|
|
92
|
-
|
|
103
|
+
if (jsonMode && result.payload !== undefined) console.log(JSON.stringify(result.payload, null, 2));
|
|
104
|
+
else report(title, result);
|
|
93
105
|
}
|
|
94
106
|
} finally {
|
|
95
107
|
process.env.DSH_WB_ONLY = '';
|
|
108
|
+
process.env.DSH_WB_ONLY_ARGS = '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// `--json` 的 stdout 只有那几份 payload:汇总行与"不适用"那几段都是人话,不进机器档。
|
|
112
|
+
if (jsonMode) {
|
|
113
|
+
if (tally.fail > 0) process.exit(1);
|
|
114
|
+
return;
|
|
96
115
|
}
|
|
97
116
|
|
|
98
117
|
console.log(`${ran} 条判据:${tally.pass} 通过 · ${tally.skip} 不适用 · ${tally.fail} 不过`);
|
package/src/checks/kit.mjs
CHANGED
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
|
|
23
23
|
import { existsSync, readFileSync } from 'node:fs';
|
|
24
24
|
import { join } from 'node:path';
|
|
25
|
+
import { flagValue, flagsOf } from '../check-flags.mjs';
|
|
25
26
|
import { discoverModulePackages } from '../discover.mjs';
|
|
27
|
+
import { memberFilesOf } from '../shell-type-files.mjs';
|
|
26
28
|
import { shellClientPathOf, shellKitOf } from './shell-artifact.mjs';
|
|
27
29
|
|
|
28
30
|
const SHELL = '@dsh-workbench/core';
|
|
@@ -82,14 +84,19 @@ function below(a, b) {
|
|
|
82
84
|
// 只在单跑时打:外壳把整份 `@dsh-workbench/shared` barrel 摊进了每个桶,所以这份名单
|
|
83
85
|
// 有一百多个名字 —— 聚合跑时它只是噪音,查询时才值钱。
|
|
84
86
|
//
|
|
85
|
-
//
|
|
87
|
+
// 名字还要能落到**文件**("想问哪个直接去看"):默认打两棵类型树的绝对根 + 取法,
|
|
88
|
+
// `--files` 打逐条 `名字 → 绝对路径`(190 条 / ~9k 字符,会被 tool-result-pruner 裁中间,
|
|
89
|
+
// 所以它得配 `| grep <名字>` 用,默认档不内联)。`--json` 给机器档。
|
|
90
|
+
//
|
|
91
|
+
// 返回的是 `{ notes, payload }`(notes 每条一行,交给聚合层加缩进;payload 归 `--json`)——
|
|
92
|
+
// 打印归 check.mjs。
|
|
86
93
|
function faceNotes(asked, shells) {
|
|
87
94
|
const faceShell = shells[shells.length - 1] ?? shellClientPathOf(process.cwd());
|
|
88
95
|
if (!asked) {
|
|
89
|
-
return ['(想知道这份外壳的 kit 里有什么:dsh-wb-module check --only kit)'];
|
|
96
|
+
return { notes: ['(想知道这份外壳的 kit 里有什么:dsh-wb-module check --only kit)'] };
|
|
90
97
|
}
|
|
91
98
|
if (faceShell === undefined || !existsSync(faceShell)) {
|
|
92
|
-
return ['(没找到外壳的 client.js,列不出 kit 面:先确认 node_modules 里装着 @dsh-workbench/core)'];
|
|
99
|
+
return { notes: ['(没找到外壳的 client.js,列不出 kit 面:先确认 node_modules 里装着 @dsh-workbench/core)'] };
|
|
93
100
|
}
|
|
94
101
|
try {
|
|
95
102
|
const kit = shellKitOf(faceShell);
|
|
@@ -97,23 +104,59 @@ function faceNotes(asked, shells) {
|
|
|
97
104
|
.filter((bucket) => typeof kit[bucket] === 'object' && kit[bucket] !== null)
|
|
98
105
|
.sort();
|
|
99
106
|
if (buckets.length === 0) {
|
|
100
|
-
return [`(${faceShell} 里没取出 kit:外壳版本可能太旧)`];
|
|
107
|
+
return { notes: [`(${faceShell} 里没取出 kit:外壳版本可能太旧)`] };
|
|
101
108
|
}
|
|
102
109
|
const every = new Map();
|
|
103
110
|
for (const bucket of buckets) {
|
|
104
111
|
for (const name of Object.keys(kit[bucket])) every.set(name, (every.get(name) ?? 0) + 1);
|
|
105
112
|
}
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
const flags = flagsOf();
|
|
114
|
+
const one = flagValue(flags, '--file');
|
|
115
|
+
const { roots, files, missing } = memberFilesOf(faceShell, [...every.keys()]);
|
|
116
|
+
const rootLine =
|
|
117
|
+
Object.entries(roots).length === 0
|
|
118
|
+
? ' 类型面:读不到(外壳包里没有 dist/types,@dsh-workbench/shared 也没装)'
|
|
119
|
+
: ` 类型面(绝对路径):${Object.entries(roots).map(([pkg, dir]) => `${pkg} → ${dir}`).join(' | ')}` +
|
|
120
|
+
` (某个名字的文件:--file <名字>,或 --files | grep <名字>)`;
|
|
121
|
+
|
|
122
|
+
const notes = [`这份外壳(${faceShell})的 kit 面 —— 只能用这些,别自己造:`, rootLine];
|
|
123
|
+
if (one !== undefined) {
|
|
124
|
+
const file = files.get(one);
|
|
125
|
+
notes.push(
|
|
126
|
+
file === undefined
|
|
127
|
+
? ` --file ${one}:不在这份清单里(用 --files | grep ${one} 找找近似的名字)`
|
|
128
|
+
: ` --file ${one}:${file} (打开看它的 JSDoc 与 prop:sed -n '1,60p' 那一份)`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (flags.includes('--files')) {
|
|
132
|
+
notes.push(
|
|
133
|
+
` 名字 → 类型文件(绝对路径,全表 ${every.size} 条、会被 tool-result-pruner 裁中间:用 --files | grep <名字> 取一件)`,
|
|
134
|
+
);
|
|
135
|
+
for (const name of [...every.keys()].sort()) {
|
|
136
|
+
notes.push(` ${name} → ${files.get(name) ?? '(没扫到,见上面那两棵根)'}`);
|
|
137
|
+
}
|
|
138
|
+
if (missing.length > 0) notes.push(` ${missing.length} 个没扫到:${missing.join('、')}`);
|
|
139
|
+
} else if (one === undefined) {
|
|
140
|
+
for (const bucket of buckets) {
|
|
141
|
+
const names = Object.keys(kit[bucket]);
|
|
142
|
+
const only = names.filter((name) => every.get(name) === 1).sort();
|
|
143
|
+
notes.push(` ${bucket}(${names.length}):${only.length === 0 ? '(全部来自下面的共用面)' : only.join(', ')}`);
|
|
144
|
+
}
|
|
145
|
+
const shared = [...every.entries()].filter(([, count]) => count > 1).map(([name]) => name).sort();
|
|
146
|
+
notes.push(` 共用面(${shared.length}):每个桶里都有 —— ${shared.join(', ')}`);
|
|
111
147
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
148
|
+
return {
|
|
149
|
+
notes,
|
|
150
|
+
payload: {
|
|
151
|
+
shell: faceShell,
|
|
152
|
+
roots,
|
|
153
|
+
buckets: Object.fromEntries(buckets.map((bucket) => [bucket, Object.keys(kit[bucket])])),
|
|
154
|
+
files: Object.fromEntries([...every.keys()].sort().map((name) => [name, files.get(name) ?? null])),
|
|
155
|
+
missing,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
115
158
|
} catch (error) {
|
|
116
|
-
return [`(取不出这份外壳的 kit 面:${error instanceof Error ? error.message : String(error)})`];
|
|
159
|
+
return { notes: [`(取不出这份外壳的 kit 面:${error instanceof Error ? error.message : String(error)})`] };
|
|
117
160
|
}
|
|
118
161
|
}
|
|
119
162
|
|
|
@@ -183,19 +226,25 @@ export async function run() {
|
|
|
183
226
|
}
|
|
184
227
|
}
|
|
185
228
|
|
|
186
|
-
if (problems.length > 0) {
|
|
187
|
-
return { status: 'fail', notes: [], problems };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
229
|
const asked = process.env.DSH_WB_ONLY === 'kit';
|
|
191
230
|
const face = faceNotes(asked, shells);
|
|
231
|
+
const payload = face.payload === undefined ? {} : { payload: face.payload };
|
|
232
|
+
|
|
233
|
+
if (problems.length > 0) {
|
|
234
|
+
// 清单是**事实**、红是**判据**:判据红不该把清单一起扣下。`--json` 的消费者(工坊那把
|
|
235
|
+
// `workbench_diy_components`)要靠这份机器档回答"这份外壳有哪些件",而判据红经常是**别的
|
|
236
|
+
// 模块**的事 —— 2026-10-20 真机:三个模块用了 MockStrip / useFixturePick,工具整条查询直接
|
|
237
|
+
// 报错、agent 什么也没拿到。退出码照旧 1(红没被藏起来),problems 也一起进机器档。
|
|
238
|
+
const extra = face.payload === undefined ? {} : { payload: { ...face.payload, problems } };
|
|
239
|
+
return { status: 'fail', notes: [], problems, ...extra };
|
|
240
|
+
}
|
|
192
241
|
|
|
193
242
|
if (inspected === 0) {
|
|
194
243
|
const why =
|
|
195
244
|
modulePackages.length === 0
|
|
196
245
|
? '没有模块包(没有声明 dsh.workbench 的包)—— 没有产物可对'
|
|
197
246
|
: `${noArtifact} 个还没建产物(跳过)`;
|
|
198
|
-
return { status: 'skip', notes: [why, ...face], problems: [] };
|
|
247
|
+
return { status: 'skip', notes: [why, ...face.notes], problems: [], ...payload };
|
|
199
248
|
}
|
|
200
249
|
|
|
201
250
|
const notes = [
|
|
@@ -204,5 +253,5 @@ export async function run() {
|
|
|
204
253
|
];
|
|
205
254
|
if (noArtifact > 0) notes.push(`${noArtifact} 个还没建产物(跳过)`);
|
|
206
255
|
if (noShell > 0) notes.push(`${noShell} 个没装外壳(跳过面差)`);
|
|
207
|
-
return { status: 'pass', notes: [...notes, ...face], problems: [] };
|
|
256
|
+
return { status: 'pass', notes: [...notes, ...face.notes], problems: [], ...payload };
|
|
208
257
|
}
|
|
@@ -12,16 +12,46 @@
|
|
|
12
12
|
//
|
|
13
13
|
// 判据跟着 `dsh.workbench` 自述清单走:没用 dsh 原语的模块直接跳过(0 命中 = 通过)。
|
|
14
14
|
//
|
|
15
|
+
// ── 查询口:`check --only primitives` 单跑时顺带当"dsh 原语里有什么"的取数处 ──────────────
|
|
16
|
+
//
|
|
17
|
+
// 与 `--only kit` 同一个惯例(`kit.mjs` 的 faceNotes):单跑时把**能用的东西**列出来,
|
|
18
|
+
// 聚合跑时只留一句提示。这份清单的真相只有一处 —— 装机包里那份类型面
|
|
19
|
+
// (`lib/types/**/*.d.ts`;源码树不随包发布),详见 `../primitives-surface.mjs` 与
|
|
20
|
+
// `.scratch/dsh-primitives-face/spec.md`。
|
|
21
|
+
//
|
|
22
|
+
// 为什么放在这条判据里:它本来就要按模块逐个解析种子词的声明与产物,而"有哪些名字可用"正是
|
|
23
|
+
// 造模块的人(尤其 DIY 工坊里那位)要的那件事;单开一个顶层命令要多一处分发、一处用法文案、
|
|
24
|
+
// 一处文档,而这里的机制(`DSH_WB_ONLY`)已经在了。
|
|
25
|
+
//
|
|
15
26
|
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
16
|
-
// 打印与退出码归 `../check.mjs`
|
|
27
|
+
// 打印与退出码归 `../check.mjs` 那一层。`--json` 时聚合层打 `payload`(见 `check.mjs` 头注释)。
|
|
17
28
|
|
|
18
|
-
import { existsSync,
|
|
29
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
30
|
+
import { createRequire } from 'node:module';
|
|
19
31
|
import { join, relative } from 'node:path';
|
|
32
|
+
import { flagValue, flagsOf } from '../check-flags.mjs';
|
|
20
33
|
import { discoverModulePackages } from '../discover.mjs';
|
|
34
|
+
import {
|
|
35
|
+
PRIMITIVES_SPECIFIER,
|
|
36
|
+
installedPrimitives,
|
|
37
|
+
pinnedPrimitivesVersion,
|
|
38
|
+
surfaceOf,
|
|
39
|
+
} from '../primitives-surface.mjs';
|
|
21
40
|
import { BROWSER_SEED_SPECIFIERS, SEED_SPECIFIERS } from '../seed.mjs';
|
|
41
|
+
import { shellClientPathOf, shellKitOf } from './shell-artifact.mjs';
|
|
22
42
|
|
|
23
43
|
const IMPORT = /from\s*'([^']+)'|import\s*\(?\s*'([^']+)'/g;
|
|
24
44
|
const SEED = new Set(SEED_SPECIFIERS);
|
|
45
|
+
const SHELL = '@dsh-workbench/core';
|
|
46
|
+
|
|
47
|
+
/** 摘要档每个桶最多列几个名字;`--all` 去掉截断。 */
|
|
48
|
+
const SUMMARY_PER_BUCKET = 12;
|
|
49
|
+
const BUCKETS = [
|
|
50
|
+
['components', 'components'],
|
|
51
|
+
['icons', 'icons'],
|
|
52
|
+
['hooks', 'hooks'],
|
|
53
|
+
['lib', 'lib'],
|
|
54
|
+
];
|
|
25
55
|
|
|
26
56
|
/** 短名写法(`ui-primitives`)也算声明过 —— 老包里有这种写法,不该因为写法短被判红。 */
|
|
27
57
|
function declares(declared, specifier) {
|
|
@@ -46,8 +76,203 @@ function sourceFiles(dir, acc = []) {
|
|
|
46
76
|
|
|
47
77
|
export const title = 'dsh 原语只从模块表拿';
|
|
48
78
|
|
|
79
|
+
/** 从 `from` 找到一份能解析到的原语包:先看模块包自己装的,再看当前 cwd。 */
|
|
80
|
+
function primitivesFrom(modulePackages) {
|
|
81
|
+
for (const pkg of modulePackages) {
|
|
82
|
+
const found = installedPrimitives(pkg.dir);
|
|
83
|
+
if (found !== undefined) return { ...found, from: pkg.dir };
|
|
84
|
+
}
|
|
85
|
+
const here = installedPrimitives(process.cwd());
|
|
86
|
+
return here === undefined ? undefined : { ...here, from: process.cwd() };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 外壳 manifest 的路径(那份 `devDependencies` 里钉着原语版本)。 */
|
|
90
|
+
function shellManifestOf(from) {
|
|
91
|
+
try {
|
|
92
|
+
return createRequire(join(from, 'package.json')).resolve(`${SHELL}/package.json`);
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 外壳 kit 的成员表:`名字 → 桶`(取不到就空表 —— 求交只是锦上添花,不该把查询口弄崩)。 */
|
|
99
|
+
function kitMembersOf(from) {
|
|
100
|
+
const client = shellClientPathOf(from);
|
|
101
|
+
if (client === undefined || !existsSync(client)) return new Map();
|
|
102
|
+
try {
|
|
103
|
+
const kit = shellKitOf(client);
|
|
104
|
+
const members = new Map();
|
|
105
|
+
for (const [bucket, value] of Object.entries(kit)) {
|
|
106
|
+
if (typeof value !== 'object' || value === null) continue;
|
|
107
|
+
for (const name of Object.keys(value)) if (!members.has(name)) members.set(name, bucket);
|
|
108
|
+
}
|
|
109
|
+
return members;
|
|
110
|
+
} catch {
|
|
111
|
+
return new Map();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function truncated(names, all) {
|
|
116
|
+
if (all || names.length <= SUMMARY_PER_BUCKET) return names.join(', ');
|
|
117
|
+
return `${names.slice(0, SUMMARY_PER_BUCKET).join(', ')} …(还有 ${names.length - SUMMARY_PER_BUCKET} 个,加 --all 看全表)`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 单跑这一条时的清单。返回 `{ notes, payload }`:
|
|
122
|
+
* notes 是人话(聚合层逐行打),payload 是 `--json` 的机器档。
|
|
123
|
+
*/
|
|
124
|
+
function faceOf(modulePackages) {
|
|
125
|
+
const flags = flagsOf();
|
|
126
|
+
const all = flags.includes('--all');
|
|
127
|
+
const from = primitivesFrom(modulePackages) ?? { from: process.cwd() };
|
|
128
|
+
const shellManifest = shellManifestOf(from.from);
|
|
129
|
+
const pinned = shellManifest === undefined ? undefined : pinnedPrimitivesVersion(shellManifest);
|
|
130
|
+
const shellVersion = shellManifest === undefined ? undefined : versionOf(shellManifest);
|
|
131
|
+
|
|
132
|
+
const declaredLines = [];
|
|
133
|
+
const modules = [];
|
|
134
|
+
for (const pkg of modulePackages) {
|
|
135
|
+
const declared = pkg.manifest.devDependencies?.[PRIMITIVES_SPECIFIER];
|
|
136
|
+
modules.push({ name: pkg.manifest.name, declared: declared ?? null });
|
|
137
|
+
if (declared !== undefined) declaredLines.push(`${pkg.manifest.name} → ${declared}`);
|
|
138
|
+
}
|
|
139
|
+
const drift =
|
|
140
|
+
pinned === undefined ? [] : modules.filter((one) => one.declared !== null && one.declared !== pinned);
|
|
141
|
+
const moduleLine =
|
|
142
|
+
declaredLines.length > 0
|
|
143
|
+
? `模块声明:${declaredLines.join(' | ')}` +
|
|
144
|
+
(drift.length > 0
|
|
145
|
+
? ` ⚠️ 与外壳钉的 ${pinned} 不一致(只提醒,不判红;对齐它的写法见下面那段)`
|
|
146
|
+
: '')
|
|
147
|
+
: '模块声明:(还没有模块声明它 —— 模板生成的骨架默认带这一行)';
|
|
148
|
+
|
|
149
|
+
const installed = installedPrimitives(from.from);
|
|
150
|
+
if (installed === undefined) {
|
|
151
|
+
const want = pinned ?? '<外壳钉的那版>';
|
|
152
|
+
const notes = [
|
|
153
|
+
`这份工程里没装 ${PRIMITIVES_SPECIFIER} —— 列不出清单。`,
|
|
154
|
+
` 它在 @deepseek-ai/dsh-client-ui-primitives 那个包的 lib/types/**/*.d.ts 里(源码不随包发布)。`,
|
|
155
|
+
` 先加一行再 pnpm install:`,
|
|
156
|
+
` "devDependencies": { "${PRIMITIVES_SPECIFIER}": "${want}" }`,
|
|
157
|
+
` 两个模板默认带这一行;加完在 dsh.client.external 里也声明 "${PRIMITIVES_SPECIFIER}"(两处都要)。`,
|
|
158
|
+
];
|
|
159
|
+
if (pinned !== undefined) notes.push(` 外壳(${SHELL}${shellVersion === undefined ? '' : `@${shellVersion}`})钉的是 ${pinned} —— 对齐它。`);
|
|
160
|
+
return { notes, payload: { installed: null, pinned: pinned ?? null, modules, snippet: devDependencySnippet(want) } };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let surface;
|
|
164
|
+
try {
|
|
165
|
+
surface = surfaceOf(join(installed.dir, 'lib', 'types'));
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return {
|
|
168
|
+
notes: [
|
|
169
|
+
`装了 ${PRIMITIVES_SPECIFIER}@${installed.version},但读不出类型面:${error instanceof Error ? error.message : String(error)}`,
|
|
170
|
+
` 上游可能换了 emit 形状 —— 先打开 ${join(installed.dir, 'lib', 'types', 'index.d.ts')} 看一眼,再改 ../primitives-surface.mjs`,
|
|
171
|
+
],
|
|
172
|
+
payload: { installed: { version: installed.version, dir: installed.dir }, pinned: pinned ?? null, modules, error: String(error) },
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const kit = from.from === undefined ? new Map() : kitMembersOf(from.from);
|
|
177
|
+
const covered = [];
|
|
178
|
+
const only = { components: [], icons: [], hooks: [], lib: [] };
|
|
179
|
+
for (const [bucket] of BUCKETS) {
|
|
180
|
+
for (const name of surface[bucket]) {
|
|
181
|
+
if (kit.has(name)) covered.push({ name, bucket: kit.get(name) });
|
|
182
|
+
else only[bucket].push(name);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
covered.sort((a, b) => a.name.localeCompare(b.name));
|
|
186
|
+
|
|
187
|
+
const notes = ['这份装机包的 dsh 原语面(kit 里没有的才用它):'];
|
|
188
|
+
notes.push(` 装机:${installed.version}`);
|
|
189
|
+
notes.push(` 类型面(绝对路径):${join(installed.dir, 'lib', 'types')} (某个名字的文件:--files | grep <名字>)`);
|
|
190
|
+
notes.push(
|
|
191
|
+
pinned === undefined
|
|
192
|
+
? ` 外壳钉的:读不到(${shellManifest ?? '找不到 @dsh-workbench/core 的 manifest'})`
|
|
193
|
+
: ` 外壳钉的:${pinned}${shellVersion === undefined ? '' : `(${SHELL}@${shellVersion} 的 devDependencies)`}${pinned === installed.version ? '' : ' ⚠️ 与装机那份不一致'}`,
|
|
194
|
+
);
|
|
195
|
+
notes.push(` ${moduleLine}`);
|
|
196
|
+
const one = flagValue(flags, '--file');
|
|
197
|
+
if (one !== undefined) {
|
|
198
|
+
const file = surface.files[one];
|
|
199
|
+
notes.push(
|
|
200
|
+
file === undefined
|
|
201
|
+
? ` --file ${one}:不在这份清单里(用 --files | grep ${one} 找找近似的名字)`
|
|
202
|
+
: ` --file ${one}:${file} (打开看它的 JSDoc 与 prop:sed -n '1,60p' 那一份)`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (flags.includes('--files')) {
|
|
206
|
+
notes.push(
|
|
207
|
+
` 名字 → 类型文件(绝对路径,全表约 ${Object.keys(surface.files).length} 条、会被 tool-result-pruner 裁中间:用 --files | grep <名字> 取一件)`,
|
|
208
|
+
);
|
|
209
|
+
const iconsFile = surface.files[surface.icons[0]];
|
|
210
|
+
for (const [bucket] of BUCKETS) {
|
|
211
|
+
const names = only[bucket];
|
|
212
|
+
if (names.length === 0) continue;
|
|
213
|
+
if (bucket === 'icons') {
|
|
214
|
+
notes.push(` ${bucket}(${names.length}):全部在 ${iconsFile ?? '?'}`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
for (const name of names) notes.push(` ${name} → ${surface.files[name] ?? '?'}`);
|
|
218
|
+
}
|
|
219
|
+
} else if (one === undefined) {
|
|
220
|
+
notes.push(
|
|
221
|
+
covered.length === 0
|
|
222
|
+
? ' kit 里已有(优先用 kit):(这份外壳的 kit 里没有同名的,下面这些都得引原语)'
|
|
223
|
+
: ` kit 里已有(优先用 kit,别重复引原语):${truncated(covered.map((one) => `${one.name}(${one.bucket})`), all)}`,
|
|
224
|
+
);
|
|
225
|
+
notes.push(` 只有原语有(用之前先在 dsh.client.external 里声明 "${PRIMITIVES_SPECIFIER}"):`);
|
|
226
|
+
for (const [bucket] of BUCKETS) {
|
|
227
|
+
const names = only[bucket];
|
|
228
|
+
if (names.length === 0) continue;
|
|
229
|
+
notes.push(` ${bucket}(${names.length}):${truncated(names, all)}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (one === undefined) {
|
|
233
|
+
notes.push(' package.json 两处这样写:');
|
|
234
|
+
notes.push(` "dsh": { "client": { "external": [ …, "${PRIMITIVES_SPECIFIER}" ] } }`);
|
|
235
|
+
notes.push(` "devDependencies": { "${PRIMITIVES_SPECIFIER}": "${pinned ?? installed.version}" }${pinned !== undefined && pinned !== installed.version ? ' ← 与外壳对齐的那版' : ''}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
notes,
|
|
240
|
+
payload: {
|
|
241
|
+
installed: { version: installed.version, dir: installed.dir },
|
|
242
|
+
roots: { primitives: join(installed.dir, 'lib', 'types') },
|
|
243
|
+
files: surface.files,
|
|
244
|
+
pinned: pinned ?? null,
|
|
245
|
+
shellVersion: shellVersion ?? null,
|
|
246
|
+
modules,
|
|
247
|
+
kitCovered: covered,
|
|
248
|
+
onlyPrimitives: only,
|
|
249
|
+
counts: { total: surface.total, ...Object.fromEntries(BUCKETS.map(([bucket]) => [bucket, only[bucket].length])) },
|
|
250
|
+
snippet: devDependencySnippet(pinned ?? installed.version),
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function devDependencySnippet(version) {
|
|
256
|
+
return {
|
|
257
|
+
external: PRIMITIVES_SPECIFIER,
|
|
258
|
+
devDependency: { name: PRIMITIVES_SPECIFIER, version },
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function versionOf(manifestPath) {
|
|
263
|
+
try {
|
|
264
|
+
const version = JSON.parse(readFileSync(manifestPath, 'utf8')).version;
|
|
265
|
+
return typeof version === 'string' ? version : undefined;
|
|
266
|
+
} catch {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
49
271
|
export async function run() {
|
|
50
272
|
const modulePackages = discoverModulePackages({ root: process.cwd() });
|
|
273
|
+
const face = process.env.DSH_WB_ONLY === 'primitives'
|
|
274
|
+
? faceOf(modulePackages)
|
|
275
|
+
: { notes: ['(想知道 dsh 原语里有什么:dsh-wb-module check --only primitives)'], payload: undefined };
|
|
51
276
|
const problems = [];
|
|
52
277
|
let modulesUsingPrimitives = 0;
|
|
53
278
|
let modulesUsingReactOnly = 0;
|
|
@@ -113,21 +338,28 @@ export async function run() {
|
|
|
113
338
|
}
|
|
114
339
|
}
|
|
115
340
|
|
|
341
|
+
const payload = face.payload === undefined ? {} : { payload: face.payload };
|
|
342
|
+
|
|
116
343
|
if (problems.length > 0) {
|
|
117
|
-
return { status: 'fail', notes:
|
|
344
|
+
return { status: 'fail', notes: face.notes, problems, ...payload };
|
|
118
345
|
}
|
|
119
346
|
if (modulePackages.length === 0) {
|
|
120
347
|
return {
|
|
121
348
|
status: 'skip',
|
|
122
|
-
notes: ['没有模块包(没有声明 dsh.workbench 的包)—— 没有 dsh 原语的声明与产物可对'],
|
|
349
|
+
notes: ['没有模块包(没有声明 dsh.workbench 的包)—— 没有 dsh 原语的声明与产物可对', ...face.notes],
|
|
123
350
|
problems: [],
|
|
351
|
+
...payload,
|
|
124
352
|
};
|
|
125
353
|
}
|
|
126
354
|
if (modulesTouchingSeeds === 0) {
|
|
127
355
|
return {
|
|
128
356
|
status: 'skip',
|
|
129
|
-
notes: [
|
|
357
|
+
notes: [
|
|
358
|
+
'没有模块用到平台种子词(react / react-dom / dsh 的浏览器包)—— 没有可对照的声明与产物',
|
|
359
|
+
...face.notes,
|
|
360
|
+
],
|
|
130
361
|
problems: [],
|
|
362
|
+
...payload,
|
|
131
363
|
};
|
|
132
364
|
}
|
|
133
365
|
return {
|
|
@@ -135,7 +367,9 @@ export async function run() {
|
|
|
135
367
|
notes: [
|
|
136
368
|
`${modulesUsingPrimitives} 个模块在用 dsh 的包(都声明了 external、产物里都是 ` +
|
|
137
369
|
`require、宿主产物里没有它们)· 另有 ${modulesUsingReactOnly} 个模块只用到 react/react-dom`,
|
|
370
|
+
...face.notes,
|
|
138
371
|
],
|
|
139
372
|
problems: [],
|
|
373
|
+
...payload,
|
|
140
374
|
};
|
|
141
375
|
}
|