@dsh-workbench/module-tools 0.3.3 → 0.3.4
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/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/design.mjs +2 -1
- package/src/discover.mjs +9 -1
- package/src/init.mjs +33 -0
- package/src/primitives-surface.mjs +120 -0
- package/src/shell-type-files.mjs +103 -0
- package/template/package.json.tpl +2 -1
- package/template-diy/package.json.tpl +2 -1
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.4",
|
|
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.4"
|
|
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
|
}
|
package/src/design.mjs
CHANGED
|
@@ -49,7 +49,7 @@ const ROWS = {
|
|
|
49
49
|
acceptance: { fields: ['step'] },
|
|
50
50
|
};
|
|
51
51
|
const DATASOURCE_FIELDS = ['read', 'params', 'credentials', 'notLoggedIn', 'timeout'];
|
|
52
|
-
const WRITE_FIELDS = ['command', 'params', 'credentials', 'preview', 'readback', 'failure'];
|
|
52
|
+
const WRITE_FIELDS = ['command', 'params', 'credentials', 'preview', 'readback', 'undo', 'failure'];
|
|
53
53
|
|
|
54
54
|
/** 一条卡背后可以有多条读(甚至跨站点):规范形状 `reads: [ … ]`,一条一行;
|
|
55
55
|
* `datasource: { … }` 是只有一条读时的写法(老模块都长这样,继续认)。 */
|
|
@@ -99,6 +99,7 @@ function writeRowsOf(write) {
|
|
|
99
99
|
`| 凭据从哪来 | ${cell(write?.credentials)} |`,
|
|
100
100
|
`| 有没有预览档(没有就写"没有,一次就是真的") | ${cell(write?.preview)} |`,
|
|
101
101
|
`| 写完之后怎么回读确认(哪条读 / 出参哪个字段) | ${cell(write?.readback)} |`,
|
|
102
|
+
`| 配对的那个撤销 / 删除(没有就写"没有配对,一次就是真的") | ${cell(write?.undo)} |`,
|
|
102
103
|
`| 失败长什么样(含"可能已经写了") | ${cell(write?.failure)} |`,
|
|
103
104
|
`| 当初探明 / 预览用的那次调用(原文) | ${soft(write?.probeCommand)} |`,
|
|
104
105
|
];
|
package/src/discover.mjs
CHANGED
|
@@ -29,7 +29,15 @@ export function discoverPackages({ root = process.cwd() } = {}) {
|
|
|
29
29
|
const packagesDir = join(workspace, 'packages');
|
|
30
30
|
if (existsSync(packagesDir)) {
|
|
31
31
|
for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
|
|
32
|
-
|
|
32
|
+
// 点开头的目录一律不认:那是**自检的临时探针**(mkdtemp 造、跑完删;名字前那一点就是为了
|
|
33
|
+
// 不跟真包混)。不排除它们有个很坑的后果 —— 别的链/别的进程正在写一个探针时,这条链会把
|
|
34
|
+
// 它当成真模块包算进契约 / 卡片 / kit 那几条,于是红得跟你改的东西毫无关系:
|
|
35
|
+
// 2026-10-19 实测:一条链留下的 `.writes-*` 让另一条链的 lint 阶段
|
|
36
|
+
// ERR_MODULE_NOT_FOUND(探针里的 eslint.config.mjs 解析不到 module-tools);
|
|
37
|
+
// 另一条链正在写的 `.init-*` 让四条判据集体红("它们会被当成真的模块包参与后面的每一条判据")。
|
|
38
|
+
// 残留探针本身仍由自检那条「探针清理干净」点名叫出来 —— 这里只是**不让它参与判据**。
|
|
39
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
40
|
+
dirs.push(join(packagesDir, entry.name));
|
|
33
41
|
}
|
|
34
42
|
}
|
|
35
43
|
dirs.push(root, workspace);
|
package/src/init.mjs
CHANGED
|
@@ -16,6 +16,34 @@ const UNIT_ID = /^[a-z][a-z0-9-]*$/;
|
|
|
16
16
|
// 为什么两个 token 共用一个值:`check:kit` 的口径是 peer 下限不许低于 devDependency 下限。
|
|
17
17
|
const FALLBACK_RANGE = '^0.1.0';
|
|
18
18
|
|
|
19
|
+
// 模板默认带的那份 dsh 原语(`devDependencies` 精确版 + `dsh.client.external` 一行)。
|
|
20
|
+
//
|
|
21
|
+
// 版本取自**装机外壳 manifest 的 `devDependencies`** —— 那是外壳编译时对账过的那版
|
|
22
|
+
// (`packages/workbench/package.json` 的 `@deepseek-ai/dsh-client-ui-primitives`),
|
|
23
|
+
// 不是外壳自己的版本号,两者不是一个号(外壳 0.3.x / 原语 0.2.1-rc.x)。
|
|
24
|
+
// 为什么用精确版:类型面要对齐对账过的那一版;`^` 会把"没验过的下一版"放进来。
|
|
25
|
+
// 读不到时兜底 + 说一句人话(与 FALLBACK_RANGE 同一处理):静默留空会让生成的
|
|
26
|
+
// package.json 声明一个谁也不认的版本。见 `.scratch/dsh-primitives-face/spec.md` §3.1。
|
|
27
|
+
const FALLBACK_DSH_UI = '0.2.1-rc.2';
|
|
28
|
+
const DSH_UI = '@deepseek-ai/dsh-client-ui-primitives';
|
|
29
|
+
|
|
30
|
+
const dshUiVersion = (root) => {
|
|
31
|
+
try {
|
|
32
|
+
const pinned = JSON.parse(
|
|
33
|
+
readFileSync(join(root, 'node_modules/@dsh-workbench/core/package.json'), 'utf8'),
|
|
34
|
+
).devDependencies?.[DSH_UI];
|
|
35
|
+
if (typeof pinned === 'string' && pinned !== '') {
|
|
36
|
+
return { version: pinned, source: '外壳 manifest 里钉的(对账过的那版)' };
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// 读不到就走下面的兜底
|
|
40
|
+
}
|
|
41
|
+
console.error(
|
|
42
|
+
`读不到外壳 manifest 里钉的 ${DSH_UI}:模板保持 ${FALLBACK_DSH_UI}(与外壳对账过的那版);在装好依赖的工程里 init 就能盖上当下那版`,
|
|
43
|
+
);
|
|
44
|
+
return { version: FALLBACK_DSH_UI, source: '兜底常量(没读到外壳 manifest)' };
|
|
45
|
+
};
|
|
46
|
+
|
|
19
47
|
const versionIn = (file) => {
|
|
20
48
|
try {
|
|
21
49
|
const version = JSON.parse(readFileSync(file, 'utf8')).version;
|
|
@@ -59,11 +87,13 @@ export async function init({ root, args = [] }) {
|
|
|
59
87
|
console.error(`${target} 已经存在且不是空目录,不覆盖`);
|
|
60
88
|
process.exit(2);
|
|
61
89
|
}
|
|
90
|
+
const dshUi = dshUiVersion(root);
|
|
62
91
|
const tokens = {
|
|
63
92
|
__UNIT__: unit,
|
|
64
93
|
__NAME__: `@dsh-workbench/module-${unit}`,
|
|
65
94
|
__DESC__: description,
|
|
66
95
|
__SHELL_RANGE__: shellRange(root),
|
|
96
|
+
__DSH_UI_VERSION__: dshUi.version,
|
|
67
97
|
};
|
|
68
98
|
// 按**已知 token 名**逐个盖章,而不是按 `__[A-Z]+__` 的形状:名字里带下划线的 token
|
|
69
99
|
// (`__SHELL_RANGE__`)也要认;同时 DIY 模板 README 里真写着的 `workbench___UNIT___rebuild`
|
|
@@ -98,4 +128,7 @@ export async function init({ root, args = [] }) {
|
|
|
98
128
|
const tables = existsSync(join(target, DESIGN_FILE)) ? `${DESIGN_FILE} 里那份三张表` : '还没有三张表(dsh-wb-module design init 生成)';
|
|
99
129
|
console.log(`${target} 建好了:${unit} 模块骨架(模板 ${which};build / check / design / selfcheck 已接工具)`);
|
|
100
130
|
console.log(` 需求底稿在 ${tables};空格用 dsh-wb-module design check 查,README 由它生成`);
|
|
131
|
+
console.log(` 模板默认带了 ${DSH_UI}@${dshUi.version}(${dshUi.source})—— 这是 dsh 原语的类型面 + external 声明两处;`);
|
|
132
|
+
console.log(' 装完先在这个 workspace 里跑一次 pnpm install,pnpm typecheck 才认得到它(骨架的判据不依赖它)。');
|
|
133
|
+
console.log(' 确实用不到就连 devDependencies 与 dsh.client.external 两处一起删,判据不拦。');
|
|
101
134
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// dsh 原语本包的**可见面**:从装机那份类型面(`lib/types/**/*.d.ts`)取出导出名,按名字形状分桶。
|
|
2
|
+
//
|
|
3
|
+
// 为什么读类型面而不是源码:这个包发布出去的 `files` 只有
|
|
4
|
+
// `lib/index.js` / `lib/**/*.css` / `lib/types/**/*.d.ts` —— **源码树不随包发布**
|
|
5
|
+
// (`package.json` 的 `exports['./src/*']` 在装机包里是死条目),类型面是唯一机器可读的清单。
|
|
6
|
+
//
|
|
7
|
+
// 为什么不上 TypeScript API:module-tools 自己没有 `typescript` 依赖(只有 `typescript-eslint`
|
|
8
|
+
// 的 peer),而这份 barrel 是 tsc 生成的、形状稳定:`export { A, B } from './X.tsx'` +
|
|
9
|
+
// `export type { … }`(滤掉)+ 少量 `export declare const X` + 一条 `export *`(要跟着走)。
|
|
10
|
+
// 代价是"上游换了 emit 形状没人知道"—— 由 selfcheck 那条"清单非空 / 含 Button / `Icon*Regular` ≥ 90"
|
|
11
|
+
// 兜住(`.scratch/dsh-primitives-face/spec.md` §3.4 / §8.1)。
|
|
12
|
+
//
|
|
13
|
+
// 分桶**只按名字形状**,不判用途:`Icon*` → 图标;`^use[A-Z]` → hooks;首字母大写 → 组件;
|
|
14
|
+
// 其余(常量、纯函数)→ 工具。"这件是不是该用、该怎么用"是判断,留给 agent / 人(TOOLING.md 那条口径)。
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import { dirname, join } from 'node:path';
|
|
19
|
+
|
|
20
|
+
export const PRIMITIVES_SPECIFIER = '@deepseek-ai/dsh-client-ui-primitives';
|
|
21
|
+
|
|
22
|
+
/** 从 `from`(一个包的目录)解析这个原语包;解析不到返回 undefined。 */
|
|
23
|
+
export function installedPrimitives(from) {
|
|
24
|
+
try {
|
|
25
|
+
const require = createRequire(join(from, 'package.json'));
|
|
26
|
+
const manifest = require.resolve(`${PRIMITIVES_SPECIFIER}/package.json`);
|
|
27
|
+
const parsed = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
28
|
+
const version = typeof parsed.version === 'string' ? parsed.version : undefined;
|
|
29
|
+
// 打印/落点用它**从 node_modules 看**的那条路径(pnpm 把真身放在 `.pnpm/` 哈希目录里,
|
|
30
|
+
// `require.resolve` 给的是 realpath)—— agent 照着能直接打开,也短得多。
|
|
31
|
+
const viaNodeModules = join(from, 'node_modules', ...PRIMITIVES_SPECIFIER.split('/'));
|
|
32
|
+
const dir = existsSync(join(viaNodeModules, 'package.json')) ? viaNodeModules : dirname(manifest);
|
|
33
|
+
return { manifest, dir, version };
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 外壳 manifest 里钉的那份原语版本(`devDependencies` 那一格)—— 模板盖章与漂移 note 都用它。 */
|
|
40
|
+
export function pinnedPrimitivesVersion(manifestPath) {
|
|
41
|
+
try {
|
|
42
|
+
const pinned = JSON.parse(readFileSync(manifestPath, 'utf8')).devDependencies?.[PRIMITIVES_SPECIFIER];
|
|
43
|
+
return typeof pinned === 'string' && pinned !== '' ? pinned : undefined;
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `export … from '…'` 一条条看:`export type` 滤掉、`export *` 跟着走、其余按名字拆。 */
|
|
50
|
+
const STATEMENT = /export\s+(type\s+)?(\*|\{[^}]*\})\s*from\s*'([^']+)'/g;
|
|
51
|
+
const DECLARED = /^export declare (?:const|function|class)\s+([A-Za-z_$][\w$]*)/gm;
|
|
52
|
+
const MAX_STAR_DEPTH = 3;
|
|
53
|
+
|
|
54
|
+
/** 说明符落到同目录的 `.d.ts`:`./icons/index.tsx` → `<types>/icons/index.d.ts`。 */
|
|
55
|
+
function dtsTargetOf(fromFile, specifier) {
|
|
56
|
+
const base = join(dirname(fromFile), specifier);
|
|
57
|
+
const stripped = base.replace(/\.(tsx?|jsx?|mjs|cjs)$/, '');
|
|
58
|
+
const candidates = [`${stripped}.d.ts`, join(stripped, 'index.d.ts'), `${base}.d.ts`];
|
|
59
|
+
return candidates.find((one) => existsSync(one));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function namesOf(statement) {
|
|
63
|
+
return statement
|
|
64
|
+
.replace(/^\{/, '')
|
|
65
|
+
.replace(/\}$/, '')
|
|
66
|
+
.split(',')
|
|
67
|
+
.map((raw) => raw.trim().split(/\s+as\s+/).pop().trim())
|
|
68
|
+
.filter((name) => /^[A-Za-z_$][\w$]*$/.test(name));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function collect(file, origin, acc, depth) {
|
|
72
|
+
let text;
|
|
73
|
+
try {
|
|
74
|
+
text = readFileSync(file, 'utf8');
|
|
75
|
+
} catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
for (const match of text.matchAll(DECLARED)) acc.set(match[1], { origin, file });
|
|
79
|
+
for (const match of text.matchAll(STATEMENT)) {
|
|
80
|
+
if (match[1] !== undefined) continue;
|
|
81
|
+
if (match[2] === '*') {
|
|
82
|
+
if (depth >= MAX_STAR_DEPTH) continue;
|
|
83
|
+
const target = dtsTargetOf(file, match[3]);
|
|
84
|
+
if (target !== undefined) collect(target, origin, acc, depth + 1);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// 名字**声明**在哪份 `.d.ts`:`export { Button } from './Button.tsx'` 的答案就是
|
|
88
|
+
// `lib/types/Button.d.ts` —— 这条信息就是"agent 想用哪个直接去看"的落点,别丢。
|
|
89
|
+
const declaredIn = dtsTargetOf(file, match[3]) ?? file;
|
|
90
|
+
for (const name of namesOf(match[2])) acc.set(name, { origin, file: declaredIn });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 装机那份类型面的导出,按名字形状分成四桶。
|
|
96
|
+
* `typesDir` = 原语包的 `lib/types`;读不到入口时抛(调用方给人话,不给空表)。
|
|
97
|
+
* `files` = 名字 → 声明它的那份 `.d.ts` 的绝对路径(188 枚图标同指 `icons/index.d.ts`)。
|
|
98
|
+
*/
|
|
99
|
+
export function surfaceOf(typesDir) {
|
|
100
|
+
const entry = join(typesDir, 'index.d.ts');
|
|
101
|
+
if (!existsSync(entry)) throw new Error(`类型面不在:${entry}`);
|
|
102
|
+
const found = new Map();
|
|
103
|
+
collect(entry, 'root', found, 0);
|
|
104
|
+
const iconsEntry = join(typesDir, 'icons', 'index.d.ts');
|
|
105
|
+
if (existsSync(iconsEntry)) collect(iconsEntry, 'icons', found, 0);
|
|
106
|
+
|
|
107
|
+
const buckets = { components: [], icons: [], hooks: [], lib: [] };
|
|
108
|
+
const files = {};
|
|
109
|
+
for (const [name, { origin, file }] of found) {
|
|
110
|
+
files[name] = file;
|
|
111
|
+
if (origin === 'icons' && !name.startsWith('Icon')) buckets.lib.push(name);
|
|
112
|
+
else if (name.startsWith('Icon')) buckets.icons.push(name);
|
|
113
|
+
else if (/^use[A-Z]/.test(name)) buckets.hooks.push(name);
|
|
114
|
+
else if (/^[A-Z][A-Z0-9_]*$/.test(name)) buckets.lib.push(name);
|
|
115
|
+
else if (/^[A-Z]/.test(name)) buckets.components.push(name);
|
|
116
|
+
else buckets.lib.push(name);
|
|
117
|
+
}
|
|
118
|
+
for (const key of Object.keys(buckets)) buckets[key].sort();
|
|
119
|
+
return { ...buckets, total: found.size, files };
|
|
120
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// kit 成员 → 类型文件:外壳那两个包的类型树(`@dsh-workbench/shared` 的 `dist` 与外壳自己的 `dist/types`)。
|
|
2
|
+
//
|
|
3
|
+
// 为什么要扫:kit 的成员是**运行期**从外壳产物 `client.js` 里跑出来的(见 `checks/kit.mjs`),
|
|
4
|
+
// 产物里没有"这个成员来自哪个模块";而类型树里每个成员的声明都躺在某份 `.d.ts` 里 ——
|
|
5
|
+
// 扫一遍就能把"名字"落到"文件",于是 agent「想问哪个直接去看那份文件」有确切路径。
|
|
6
|
+
//
|
|
7
|
+
// 为什么是名字扫描而不是真正的类型解析:这些 `.d.ts` 是 tsc 生成的、声明形状规整
|
|
8
|
+
// (`export declare const X` / `export interface X` / `export { X } from '…'`),而 kit 的成员
|
|
9
|
+
// 本来就是运行期对象上的键名。扫描不引 TypeScript 依赖、贵在稳;代价是"同名声明出现在多处"时
|
|
10
|
+
// 要挑一个(挑法见 `preferred`),以及极少数成员指到的是 barrel 而不是最终实现文件。
|
|
11
|
+
// 实测本机 190 个 kit 成员 190 个都落得到文件(见 `.scratch/dsh-primitives-face/verify.md` §2)。
|
|
12
|
+
|
|
13
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
const PATTERNS = [
|
|
18
|
+
/^export declare (?:const|function|class) (\w+)/gm,
|
|
19
|
+
/^export (?:interface|type) (\w+)/gm,
|
|
20
|
+
/^export \{([^}]*)\}/gm,
|
|
21
|
+
/^declare (?:const|function|class) (\w+)/gm,
|
|
22
|
+
/^(?:interface|type) (\w+)/gm,
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function walk(dir, acc = []) {
|
|
26
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
27
|
+
const full = join(dir, entry.name);
|
|
28
|
+
if (entry.isDirectory()) walk(full, acc);
|
|
29
|
+
else if (full.endsWith('.d.ts')) acc.push(full);
|
|
30
|
+
}
|
|
31
|
+
return acc;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function namesIn(clause) {
|
|
35
|
+
if (!clause.includes(',') && !clause.includes(' ')) return [clause];
|
|
36
|
+
return clause
|
|
37
|
+
.split(',')
|
|
38
|
+
.map((raw) => raw.trim().split(/\s+as\s+/).pop().trim())
|
|
39
|
+
.filter((name) => /^[A-Za-z_$][\w$]*$/.test(name));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 外壳那两个包的类型树(`shared` 没装就只给外壳那棵)。 */
|
|
43
|
+
export function shellTypeRoots(shellClientPath) {
|
|
44
|
+
const shellRoot = dirname(shellClientPath);
|
|
45
|
+
const roots = {};
|
|
46
|
+
const shellTypes = join(shellRoot, 'dist', 'types');
|
|
47
|
+
if (existsSync(shellTypes)) roots.shell = shellTypes;
|
|
48
|
+
// 打印用"从 node_modules 看"的那条路径(pnpm 的真身在 `.pnpm/` 哈希目录里);读文件两条都行。
|
|
49
|
+
const viaNodeModules = join(shellRoot, 'node_modules', '@dsh-workbench', 'shared', 'dist');
|
|
50
|
+
try {
|
|
51
|
+
// 解析 "." 那个入口(= `dist/index.js`)再取 dirname —— shared 的 `exports` 里**没有**
|
|
52
|
+
// `./package.json` 这一条,直接 resolve 它的 package.json 会 ERR_PACKAGE_PATH_NOT_EXPORTED。
|
|
53
|
+
const sharedEntry = createRequire(join(shellRoot, 'package.json')).resolve('@dsh-workbench/shared');
|
|
54
|
+
const sharedTypes = dirname(sharedEntry);
|
|
55
|
+
if (existsSync(sharedTypes)) roots.shared = existsSync(viaNodeModules) ? viaNodeModules : sharedTypes;
|
|
56
|
+
} catch {
|
|
57
|
+
// 没装 shared:只有外壳自己那棵,仍然能落一部分成员
|
|
58
|
+
}
|
|
59
|
+
return { shellRoot, roots };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 同名多处时挑哪个:组件 > hooks > 库 > 其他,同档挑路径短的。 */
|
|
63
|
+
function preferred(candidates) {
|
|
64
|
+
const rank = (file) =>
|
|
65
|
+
file.includes('/components/') ? 0 : file.includes('/hooks/') ? 1 : file.includes('/lib/') ? 2 : 3;
|
|
66
|
+
return [...candidates].sort((a, b) => rank(a.file) - rank(b.file) || a.file.length - b.file.length)[0];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 把 kit 的成员名落到类型文件。
|
|
71
|
+
* 返回 `{ roots, files: Map<名字, 绝对路径>, missing: 名字[] }` —— `roots` 是两棵类型树的绝对根。
|
|
72
|
+
*/
|
|
73
|
+
export function memberFilesOf(shellClientPath, names) {
|
|
74
|
+
const { roots } = shellTypeRoots(shellClientPath);
|
|
75
|
+
const index = new Map();
|
|
76
|
+
for (const root of Object.values(roots)) {
|
|
77
|
+
for (const file of walk(root)) {
|
|
78
|
+
let text;
|
|
79
|
+
try {
|
|
80
|
+
text = readFileSync(file, 'utf8');
|
|
81
|
+
} catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
for (const pattern of PATTERNS) {
|
|
85
|
+
for (const match of text.matchAll(pattern)) {
|
|
86
|
+
for (const name of namesIn(match[1])) {
|
|
87
|
+
if (!index.has(name)) index.set(name, []);
|
|
88
|
+
const found = index.get(name);
|
|
89
|
+
if (!found.some((one) => one.file === file)) found.push({ file });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const files = new Map();
|
|
96
|
+
const missing = [];
|
|
97
|
+
for (const name of names) {
|
|
98
|
+
const candidates = index.get(name);
|
|
99
|
+
if (candidates === undefined || candidates.length === 0) missing.push(name);
|
|
100
|
+
else files.set(name, preferred(candidates).file);
|
|
101
|
+
}
|
|
102
|
+
return { roots, files, missing };
|
|
103
|
+
}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"client": {
|
|
24
24
|
"platform": "web",
|
|
25
25
|
"inject": ["@dsh-workbench/core"],
|
|
26
|
-
"external": ["@dsh-workbench/core/client"]
|
|
26
|
+
"external": ["@dsh-workbench/core/client", "@deepseek-ai/dsh-client-ui-primitives"]
|
|
27
27
|
},
|
|
28
28
|
"workbench": {
|
|
29
29
|
"apiVersion": 1,
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"peerDependencies": { "@dsh-workbench/core": "__SHELL_RANGE__", "react": "*" },
|
|
38
38
|
"peerDependenciesMeta": { "react": { "optional": true } },
|
|
39
39
|
"devDependencies": {
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-primitives": "__DSH_UI_VERSION__",
|
|
40
41
|
"@dsh-workbench/core": "__SHELL_RANGE__",
|
|
41
42
|
"@dsh-workbench/module-tools": "__SHELL_RANGE__",
|
|
42
43
|
"@types/node": "^24.19.0",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"client": {
|
|
25
25
|
"platform": "web",
|
|
26
26
|
"inject": ["@dsh-workbench/core"],
|
|
27
|
-
"external": ["@dsh-workbench/core/client"]
|
|
27
|
+
"external": ["@dsh-workbench/core/client", "@deepseek-ai/dsh-client-ui-primitives"]
|
|
28
28
|
},
|
|
29
29
|
"workbench": {
|
|
30
30
|
"apiVersion": 1,
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"peerDependencies": { "@dsh-workbench/core": "__SHELL_RANGE__", "react": "*" },
|
|
39
39
|
"peerDependenciesMeta": { "react": { "optional": true } },
|
|
40
40
|
"devDependencies": {
|
|
41
|
+
"@deepseek-ai/dsh-client-ui-primitives": "__DSH_UI_VERSION__",
|
|
41
42
|
"@dsh-workbench/core": "__SHELL_RANGE__",
|
|
42
43
|
"@dsh-workbench/module-tools": "__SHELL_RANGE__",
|
|
43
44
|
"@types/node": "^24.19.0",
|