@dsh-workbench/module-tools 0.3.2 → 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/README.md +134 -7
- package/bin/dsh-wb-module.mjs +53 -4
- package/package.json +11 -3
- package/src/arch.mjs +256 -0
- package/src/build.mjs +4 -1
- package/src/check-flags.mjs +22 -0
- package/src/check.mjs +23 -2
- package/src/checks/arch.mjs +127 -0
- package/src/checks/kit.mjs +68 -19
- package/src/checks/lint.mjs +351 -0
- package/src/checks/primitives.mjs +239 -5
- package/src/design.mjs +47 -5
- package/src/discover.mjs +9 -1
- package/src/init.mjs +33 -0
- package/src/lint.mjs +309 -0
- package/src/primitives-surface.mjs +120 -0
- package/src/shell-type-files.mjs +103 -0
- package/template/README.md.tpl +18 -3
- package/template/arch.config.mjs +2 -0
- package/template/eslint.config.mjs +2 -0
- package/template/package.json.tpl +4 -1
- package/template/src/client.tsx.tpl +4 -18
- package/template/src/components/HelloCard.tsx.tpl +28 -0
- package/template-diy/README.md.tpl +44 -19
- package/template-diy/arch.config.mjs +2 -0
- package/template-diy/design.json.tpl +1 -0
- package/template-diy/eslint.config.mjs +2 -0
- package/template-diy/package.json.tpl +4 -1
- package/template-diy/src/client.tsx.tpl +10 -149
- package/template-diy/src/components/OverviewCard.module.css.tpl +5 -0
- package/template-diy/src/components/OverviewCard.test.ts.tpl +118 -0
- package/template-diy/src/components/OverviewCard.tsx.tpl +92 -0
- package/template-diy/src/host.ts.tpl +1 -14
- package/template-diy/src/model/overview.test.ts.tpl +49 -0
- package/template-diy/src/model/overview.ts.tpl +61 -0
- package/template-diy/src/plugin.test.ts.tpl +4 -140
- package/template-diy/types/styles.d.ts +14 -0
package/src/design.mjs
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
// README 是给人读的落点,不是给机器判的形状。
|
|
8
8
|
//
|
|
9
9
|
// 现在的分工:
|
|
10
|
-
// · `design.json` —— 机器可读的需求底稿(三张表 + 状态矩阵 + 数据源记录 + 验收清单);
|
|
10
|
+
// · `design.json` —— 机器可读的需求底稿(三张表 + 状态矩阵 + 数据源记录 + 写通道记录 + 验收清单);
|
|
11
11
|
// · `README.md` —— 由它**生成**的那一段(夹在 `<!-- dsh-wb:design:start/end -->` 之间);
|
|
12
12
|
// · `dsh-wb-module design check` —— 把**空着的格**列出来(红),并核对 README 没落后于 design.json。
|
|
13
13
|
//
|
|
14
|
-
// 这样"空格"第一次有了形状:填空 → 再往下做;写完 →
|
|
14
|
+
// 这样"空格"第一次有了形状:填空 → 再往下做;写完 → 自己在真卡片上逐条走通,`design accept` 打勾。
|
|
15
15
|
|
|
16
16
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
17
17
|
import { join } from 'node:path';
|
|
@@ -49,6 +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', 'undo', 'failure'];
|
|
52
53
|
|
|
53
54
|
/** 一条卡背后可以有多条读(甚至跨站点):规范形状 `reads: [ … ]`,一条一行;
|
|
54
55
|
* `datasource: { … }` 是只有一条读时的写法(老模块都长这样,继续认)。 */
|
|
@@ -59,6 +60,15 @@ function readsOf(design) {
|
|
|
59
60
|
return one === undefined || one === null || typeof one !== 'object' ? [] : [one];
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/** 写侧与读侧对称:一个动作可以打几条写(先建后发布那种),规范形状 `writes: [ … ]`;
|
|
64
|
+
* `write: { … }` 是只有一条写时的写法。**没有写通道的模块不写就行** —— 这里不判"一条都没有"。 */
|
|
65
|
+
function writesOf(design) {
|
|
66
|
+
const list = design?.writes;
|
|
67
|
+
if (Array.isArray(list) && list.length > 0) return list;
|
|
68
|
+
const one = design?.write;
|
|
69
|
+
return one === undefined || one === null || typeof one !== 'object' ? [] : [one];
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
/** 「他能自己配什么」有两种,分不清就会写成一个不落盘的选择还自称配置: */
|
|
63
73
|
function persistenceLabel(value) {
|
|
64
74
|
const raw = text(value);
|
|
@@ -80,6 +90,21 @@ function datasourceRowsOf(read) {
|
|
|
80
90
|
];
|
|
81
91
|
}
|
|
82
92
|
|
|
93
|
+
function writeRowsOf(write) {
|
|
94
|
+
return [
|
|
95
|
+
'| 项 | 值(照抄,别转述) |',
|
|
96
|
+
'|---|---|',
|
|
97
|
+
`| 哪条写(命令 / URL + 方法) | ${cell(write?.command)} |`,
|
|
98
|
+
`| 参数(必填 / 取值枚举 / 上限) | ${cell(write?.params)} |`,
|
|
99
|
+
`| 凭据从哪来 | ${cell(write?.credentials)} |`,
|
|
100
|
+
`| 有没有预览档(没有就写"没有,一次就是真的") | ${cell(write?.preview)} |`,
|
|
101
|
+
`| 写完之后怎么回读确认(哪条读 / 出参哪个字段) | ${cell(write?.readback)} |`,
|
|
102
|
+
`| 配对的那个撤销 / 删除(没有就写"没有配对,一次就是真的") | ${cell(write?.undo)} |`,
|
|
103
|
+
`| 失败长什么样(含"可能已经写了") | ${cell(write?.failure)} |`,
|
|
104
|
+
`| 当初探明 / 预览用的那次调用(原文) | ${soft(write?.probeCommand)} |`,
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
|
|
83
108
|
function text(value) {
|
|
84
109
|
return typeof value === 'string' ? value.trim() : '';
|
|
85
110
|
}
|
|
@@ -121,6 +146,7 @@ function skeleton(manifest = {}) {
|
|
|
121
146
|
timeout: '',
|
|
122
147
|
probeCommand: '',
|
|
123
148
|
},
|
|
149
|
+
writes: [],
|
|
124
150
|
acceptance: [{ step: '' }],
|
|
125
151
|
};
|
|
126
152
|
}
|
|
@@ -178,6 +204,12 @@ export function emptyCells(design) {
|
|
|
178
204
|
for (const field of DATASOURCE_FIELDS) need(cells, read?.[field], `${label}.${field}`);
|
|
179
205
|
});
|
|
180
206
|
|
|
207
|
+
const writes = writesOf(design);
|
|
208
|
+
writes.forEach((write, index) => {
|
|
209
|
+
const label = writes.length === 1 ? 'write' : `writes[${index}]`;
|
|
210
|
+
for (const field of WRITE_FIELDS) need(cells, write?.[field], `${label}.${field}`);
|
|
211
|
+
});
|
|
212
|
+
|
|
181
213
|
return cells;
|
|
182
214
|
}
|
|
183
215
|
|
|
@@ -206,6 +238,7 @@ export function renderDesign(design) {
|
|
|
206
238
|
const acceptance = asRows(design?.acceptance);
|
|
207
239
|
const states = asRows(design?.states);
|
|
208
240
|
const reads = readsOf(design);
|
|
241
|
+
const writes = writesOf(design);
|
|
209
242
|
|
|
210
243
|
const stateRows = STATE_KEYS.map((key, index) => {
|
|
211
244
|
const row = states.find((one) => text(one?.key) === key) ?? {};
|
|
@@ -277,7 +310,16 @@ export function renderDesign(design) {
|
|
|
277
310
|
...(reads.length <= 1
|
|
278
311
|
? [...datasourceRowsOf(reads[0]), '']
|
|
279
312
|
: reads.flatMap((one, index) => [`#### 读 ${String(index + 1)}`, '', ...datasourceRowsOf(one), ''])),
|
|
280
|
-
|
|
313
|
+
...(writes.length === 0
|
|
314
|
+
? []
|
|
315
|
+
: [
|
|
316
|
+
'### 写通道探明记录',
|
|
317
|
+
'',
|
|
318
|
+
...(writes.length <= 1
|
|
319
|
+
? [...writeRowsOf(writes[0]), '']
|
|
320
|
+
: writes.flatMap((one, index) => [`#### 写 ${String(index + 1)}`, '', ...writeRowsOf(one), ''])),
|
|
321
|
+
]),
|
|
322
|
+
'### 验收(自己在真卡片上逐条走通后打勾:`dsh-wb-module design accept`)',
|
|
281
323
|
'',
|
|
282
324
|
...acceptanceRows,
|
|
283
325
|
'',
|
|
@@ -351,7 +393,7 @@ function checkDesign(root) {
|
|
|
351
393
|
console.error('✖ README 里那一段落后于 design.json(或者没有标记)—— 跑 dsh-wb-module design render');
|
|
352
394
|
}
|
|
353
395
|
if (cells.length === 0 && fresh) {
|
|
354
|
-
console.log(`✔ 三张表填全了,README 也是最新的${accepted === acceptance.length ? ',验收也打完了' : `;验收还差 ${acceptance.length - accepted}
|
|
396
|
+
console.log(`✔ 三张表填全了,README 也是最新的${accepted === acceptance.length ? ',验收也打完了' : `;验收还差 ${acceptance.length - accepted} 条(自己在真卡片上逐条走通后跑 design accept)`}`);
|
|
355
397
|
}
|
|
356
398
|
process.exit(cells.length === 0 && fresh ? 0 : 1);
|
|
357
399
|
}
|
|
@@ -399,7 +441,7 @@ export async function design({ root, args = [] }) {
|
|
|
399
441
|
design0.acceptedAt = new Date().toISOString();
|
|
400
442
|
writeDesign(root, design0);
|
|
401
443
|
applyToReadme(root, renderDesign(design0));
|
|
402
|
-
console.log(`✔ ${acceptance.length}
|
|
444
|
+
console.log(`✔ ${acceptance.length} 条验收全部打勾(只在真的在真卡片上逐条走通过之后再跑这条)`);
|
|
403
445
|
return;
|
|
404
446
|
}
|
|
405
447
|
|
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
|
}
|
package/src/lint.mjs
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// 模块包的**语法卫生方言**(eslint 的模块版)。
|
|
2
|
+
//
|
|
3
|
+
// 为什么要有这一份:arch-guard 的 README 把自己的边界写得很清楚 ——「本工具不是 linter:
|
|
4
|
+
// 单文件语法卫生(any、console、`!important`、颜色字面量、行数上限…)交给 eslint / oxlint /
|
|
5
|
+
// stylelint / knip」。上一期(`./arch.mjs`)把**结构**那半边管住了,语法这半边一条都没有:
|
|
6
|
+
// `as any` 把契约绕过去、`console.log` 留在产物里、渲染期写 ref、漏依赖、内联颜色 ——
|
|
7
|
+
// 这些都不改变文件的形状,S16 / S23 那种判据永远看不见。
|
|
8
|
+
//
|
|
9
|
+
// 为什么方言住 module-tools、而不是让每个模块抄一份配置:与 `moduleArch()` 逐条同一个理由 ——
|
|
10
|
+
// ① 抄一份就会各自漂成十几份"方言";② 方言应当跟**工具链**同代(工坊三个包随外壳同代自愈,
|
|
11
|
+
// 见 ADR-0056),规则变严时用户重跑一次门禁就跟着严,不用重抄。模板里那份 `eslint.config.mjs`
|
|
12
|
+
// 只留一行工厂调用,要改规则就在**这一处**改。
|
|
13
|
+
//
|
|
14
|
+
// 为什么依赖装在 module-tools 一家(eslint / @eslint/js / typescript-eslint / 两个 react 插件):
|
|
15
|
+
// 模块与工坊根的清单**一个字都不用改** —— 这个文件 import 的是 module-tools 自己的依赖,
|
|
16
|
+
// Node 从 module-tools 的位置解析;代价是工坊安装多约 17MB(第一次),换来的是用户拿到
|
|
17
|
+
// `dsh-wb-module lint [--fix]` 就能跑,不必自己装一套 eslint 再跟版本漂移打架。
|
|
18
|
+
//
|
|
19
|
+
// 为什么是这几条规则(逐条的口径都在下面各块的注释里):挑的是"语法级就能判、命中了基本
|
|
20
|
+
// 就是真错"的那批 —— 不做类型感知规则(`no-floating-promises` 那种要建 program,模块 tsconfig
|
|
21
|
+
// 形状各异、慢一个量级;外壳自己那份也把它排到 L2,见 `.scratch/eslint/spec.md`)。
|
|
22
|
+
//
|
|
23
|
+
// 指纹(**逐块比整份方言**,不只是比个名字):本文件导出的 `DIALECT_BLOCKS` 是 `moduleLint()`
|
|
24
|
+
// (不带 options)产出的**全部块**(`@eslint/js/recommended` · 三块 `typescript-eslint/*` ·
|
|
25
|
+
// `dsh-wb/module-lint` 那四块),只排除两块 options 驱动的(`/overrides`、`/root`)。
|
|
26
|
+
// 全局 ignores 那一块单列成 `DIALECT_IGNORES`,按**超集**比(模块可以多忽略,不许少忽略)。
|
|
27
|
+
// 判据(`./checks/lint.mjs`)import 模块那份配置之后**逐块对**:名字 / `files` / `rules` 有一处
|
|
28
|
+
// 不一致就红。这不是风格洁癖:没有它,模块可以把下限悄悄削掉(只留一个同名空块、筛掉基础层)
|
|
29
|
+
// 而门禁照绿 —— 2026-10-19 verifier 先用 `export default [{ name: 'dsh-wb/module-lint',
|
|
30
|
+
// rules: {} }]` 绕过去;随后又量到"只比 `dsh-wb/module-lint*` 那 4 块"还不够:**基础层**
|
|
31
|
+
//(`@eslint/js/recommended` 与三块 `typescript-eslint/*`)与**全局 ignores 块**都不在比对面里。
|
|
32
|
+
// 两者入口不同:前者**有名字**,要筛掉得显式点名(`filter((b) => b.name !== '@eslint/js/recommended')`);
|
|
33
|
+
// 后者是唯一**没有名字**的那块(全局 ignores 不能带 `name`),`filter((b) => b.name !== undefined)`
|
|
34
|
+
// 筛掉的正是它。后果一样(下限被削),所以比对面扩到整份。
|
|
35
|
+
//
|
|
36
|
+
// **残余(这类校验的天花板,文档要如实写)**:模块若自己 import 本方言、再把这几块**照抄**成
|
|
37
|
+
// 一份自己的配置(`moduleLint().map((b) => ({ ...b }))` 这类),判据分不出来 —— 它比的是形状,
|
|
38
|
+
// 不是"这段代码从哪来"。这条的目标是**抬高门槛 + 让改动必须明着写**(改方言那几块会被点名;
|
|
39
|
+
// 开口子只能走 `moduleLint({ rules, ignores })` 或自己追加一块),**不是**防住蓄意伪造。
|
|
40
|
+
|
|
41
|
+
import js from '@eslint/js';
|
|
42
|
+
import react from 'eslint-plugin-react';
|
|
43
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
|
44
|
+
import tseslint from 'typescript-eslint';
|
|
45
|
+
|
|
46
|
+
/** 方言主块的名字。判据按它认"这一块属于方言";改名字等于改协议。 */
|
|
47
|
+
export const FINGERPRINT = 'dsh-wb/module-lint';
|
|
48
|
+
|
|
49
|
+
// 只有 options 才会变的两块:它们不进"固定的方言"清单(判据不拿它们比 —— 开口子正是改它们)。
|
|
50
|
+
const OPTION_BLOCK_NAMES = new Set([`${FINGERPRINT}/overrides`, `${FINGERPRINT}/root`]);
|
|
51
|
+
|
|
52
|
+
// 全局 ignores 那一块(`{ ignores: [...] }`:没有 `name` / `files` / `rules`)。它**不按形状**比,
|
|
53
|
+
// 单列成下面的 `DIALECT_IGNORES` 按**超集**比 —— 模块多忽略几条可以(那是开口子),少一条不行。
|
|
54
|
+
// 导出它是为了让判据按**同一处**认出这一块,不在判据里另写一份"什么样的块算全局 ignores"。
|
|
55
|
+
export function isGlobalIgnoresBlock(block) {
|
|
56
|
+
return (
|
|
57
|
+
block !== null &&
|
|
58
|
+
typeof block === 'object' &&
|
|
59
|
+
Array.isArray(block.ignores) &&
|
|
60
|
+
block.files === undefined &&
|
|
61
|
+
block.rules === undefined
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 对象/数组递归重建、对象键排序:同一份形状在任何机器上 `JSON.stringify` 都是同一个串。 */
|
|
66
|
+
function sortedDeep(value) {
|
|
67
|
+
if (Array.isArray(value)) return value.map(sortedDeep);
|
|
68
|
+
if (value === null || typeof value !== 'object') return value;
|
|
69
|
+
return Object.fromEntries(
|
|
70
|
+
Object.keys(value)
|
|
71
|
+
.sort()
|
|
72
|
+
.map((key) => [key, sortedDeep(value[key])]),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 一块方言的**规范形状**(判据逐块比的就是它):名字 + `files`(没有就是 `null`)+ 键排序后的
|
|
78
|
+
* `rules`。只认这三样 —— `plugins` / `settings` / `languageOptions` 是"怎么跑起来"的细节,
|
|
79
|
+
* 掉了它们 ESLint 自己会报错,不必在这里比。
|
|
80
|
+
*/
|
|
81
|
+
export function dialectShapeOf(block) {
|
|
82
|
+
return {
|
|
83
|
+
name: block.name,
|
|
84
|
+
files: block.files ?? null,
|
|
85
|
+
rules: sortedDeep(block.rules ?? {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 产物与声明不判:`dist/**` 是 build 出来的,`client.js` 是浏览器面的产物,`*.d.ts` 是声明
|
|
90
|
+
// (`types/styles.d.ts` 那种)。
|
|
91
|
+
//
|
|
92
|
+
// `src/components/vendor/**` 是**唯一**允许 `!important` 的地方 —— 与 arch 方言的
|
|
93
|
+
// `DESIGN.vendorDir` 同一处(`./arch.mjs`):那是"覆盖第三方样式"的那一格,不是我们的代码。
|
|
94
|
+
const DEFAULT_IGNORES = [
|
|
95
|
+
'**/node_modules/**',
|
|
96
|
+
'**/dist/**',
|
|
97
|
+
'**/coverage/**',
|
|
98
|
+
'**/client.js',
|
|
99
|
+
'**/*.d.ts',
|
|
100
|
+
'**/components/vendor/**',
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
// `src/**` 里能一眼看出"手搓外观"的两串字面量。CSS 那一半归 arch-guard 的 D 族
|
|
104
|
+
// (`*.module.css` 已经管着 D09 / D12 / D17),这一半管 **TS 里的内联样式** —— 两条合起来
|
|
105
|
+
// "手搓外观"才闭合:写进 CSS 文件的和写进 style={{}} 的都跑不掉。
|
|
106
|
+
const COLOR_LITERAL = {
|
|
107
|
+
selector:
|
|
108
|
+
'Literal[value=/^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/], Literal[value=/^(?:rgba?|hsla?)\\(/]',
|
|
109
|
+
message: '颜色字面量:引用 dsh 的 --dsw-* 令牌,别写死颜色',
|
|
110
|
+
};
|
|
111
|
+
const BANG_IMPORTANT = {
|
|
112
|
+
selector: 'Literal[value=/!important/]',
|
|
113
|
+
message: '!important:只许在 src/components/vendor/** 里覆盖第三方样式',
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// `js.configs.recommended` 的 `no-undef` 在 `.mjs` / `.js` 上是开的(TS 那边 typescript-eslint
|
|
117
|
+
// 把它关了,类型才是那里的真相)。脚本与配置文件要干"算路径、起进程"的活,Node 的全局量得列出来。
|
|
118
|
+
//
|
|
119
|
+
// `URLSearchParams` 那一行是**实测**撞出来的:模板自带的 `scripts/smoke.mjs` 用它拼取数地址,
|
|
120
|
+
// 照抄仓库根那份 globals(没有它)会让模板开箱就两条 `no-undef`。
|
|
121
|
+
const NODE_GLOBALS = {
|
|
122
|
+
Buffer: 'readonly',
|
|
123
|
+
URL: 'readonly',
|
|
124
|
+
URLSearchParams: 'readonly',
|
|
125
|
+
console: 'readonly',
|
|
126
|
+
fetch: 'readonly',
|
|
127
|
+
module: 'readonly',
|
|
128
|
+
process: 'readonly',
|
|
129
|
+
setTimeout: 'readonly',
|
|
130
|
+
clearTimeout: 'readonly',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// 非源码那一组(脚本 / 配置)。为什么不判 `console`:`scripts/smoke.mjs` 里那 6 处 `console.log`
|
|
134
|
+
// 是它的**本职工作**(把一次冒烟的结果打给人看)—— 把 `no-console` 加到这一组,模板开箱就红,
|
|
135
|
+
// 而那是误报。所以 `no-console` 只住在下面那个 `src/**` 块里。
|
|
136
|
+
//
|
|
137
|
+
// 这一组是 2026-10-19 拆 selfcheck 时真被两类**静默**错误咬过才补的(标识符少写引号、模板串
|
|
138
|
+
// 写进了普通字符串),与仓库根那份同表;命中基本就是真错,不必跟噪声搏斗。
|
|
139
|
+
const SCRIPT_RULES = {
|
|
140
|
+
'no-template-curly-in-string': 'error',
|
|
141
|
+
'require-atomic-updates': ['error', { allowProperties: true }],
|
|
142
|
+
'no-promise-executor-return': 'error',
|
|
143
|
+
'no-async-promise-executor': 'error',
|
|
144
|
+
'no-throw-literal': 'error',
|
|
145
|
+
'no-useless-catch': 'error',
|
|
146
|
+
'no-constant-binary-expression': 'error',
|
|
147
|
+
'no-unmodified-loop-condition': 'error',
|
|
148
|
+
'no-self-compare': 'error',
|
|
149
|
+
'no-sequences': 'error',
|
|
150
|
+
'no-var': 'error',
|
|
151
|
+
eqeqeq: ['error', 'smart'],
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const SRC_FILES = ['src/**/*.{ts,tsx}'];
|
|
155
|
+
const TSX_FILES = ['**/*.tsx'];
|
|
156
|
+
const TS_FILES = ['**/*.{ts,tsx}'];
|
|
157
|
+
// `*.config.{js,mjs,cjs}` 把模块根那几份配置(`eslint.config.mjs` / `arch.config.mjs` /
|
|
158
|
+
// 以后可能有的 `*.config.mjs`)一起收进来;`scripts/**` 是模块自己的脚本。
|
|
159
|
+
const SCRIPT_FILES = ['scripts/**/*.{js,mjs,cjs}', '*.config.{js,mjs,cjs}'];
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 模块包的 eslint 方言。模板里那两行就是默认值:
|
|
163
|
+
*
|
|
164
|
+
* ```js
|
|
165
|
+
* import { moduleLint } from '@dsh-workbench/module-tools/lint';
|
|
166
|
+
* export default moduleLint({ rootDir: import.meta.dirname });
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* (为什么模板那份带 `rootDir`、不传行不行:见上面 `rootDir` 那一段 —— 单配置可省,多份并存必需。)
|
|
170
|
+
*
|
|
171
|
+
* 复杂模块要开口子时**从参数进**(每条口子都有名字,评审时看得见),别换掉方言:
|
|
172
|
+
*
|
|
173
|
+
* - `ignores`:追加忽略的 glob(默认那六条见 `DEFAULT_IGNORES`)
|
|
174
|
+
* - `rules`:追加 / 覆盖规则,**最后一块**,所以它对所有文件生效、且压过前面每一块
|
|
175
|
+
* - `rootDir`:这份配置所在的那个目录(模块自己那份配置里写 `import.meta.dirname`)。
|
|
176
|
+
* 为什么要有它:typescript-eslint 的 parser 在**没拿到** `tsconfigRootDir` 时会去猜 —— 猜法是看
|
|
177
|
+
* "现在正在评估哪份 `eslint.config.*`"(V8 调用栈)。一个进程里只有一份配置时它猜得对(用户在
|
|
178
|
+
* 自己模块里 `eslint .` 就是这种情形,所以**不传也能跑**);但只要同时有多份(本仓根上那份
|
|
179
|
+
* + 模块自己那份,ESLint 会按目录就近选一份),它就猜不出来,**每个文件一条解析错**
|
|
180
|
+
* (`No tsconfigRootDir was set, and multiple candidate TSConfigRootDirs are present`,
|
|
181
|
+
* 2026-10-19 整链实测)。模块这份配置声明了自己的根目录,这一条就没了。
|
|
182
|
+
*
|
|
183
|
+
* 要按目录细调(比如"只给 `src/legacy/**` 放行 `no-explicit-any`"),就在自己那份配置里
|
|
184
|
+
* 展开再追一块:
|
|
185
|
+
*
|
|
186
|
+
* ```js
|
|
187
|
+
* export default [
|
|
188
|
+
* ...moduleLint({ rules: { 'no-console': 'off' } }),
|
|
189
|
+
* { files: ['src/legacy/**'], rules: { '@typescript-eslint/no-explicit-any': 'off' } },
|
|
190
|
+
* ];
|
|
191
|
+
* ```
|
|
192
|
+
*
|
|
193
|
+
* @param {{ ignores?: string[], rules?: Record<string, unknown>, rootDir?: string }} [options]
|
|
194
|
+
* @returns {import('eslint').Linter.Config[]} 标准 flat config 数组(编辑器与 eslint 认的就是它)
|
|
195
|
+
*/
|
|
196
|
+
export function moduleLint(options = {}) {
|
|
197
|
+
const { ignores = [], rules = {}, rootDir } = options;
|
|
198
|
+
const overrideRules = Object.keys(rules).length === 0 ? [] : [{ name: 'dsh-wb/module-lint/overrides', rules }];
|
|
199
|
+
// 解析根目录那一块只在**明说** rootDir 时出现:不说就维持"typescript-eslint 自己猜"(单配置情形
|
|
200
|
+
// 猜得对,且不给用户的模块凭空塞一个他不知道的 parserOptions)。
|
|
201
|
+
const rootBlock =
|
|
202
|
+
rootDir === undefined
|
|
203
|
+
? []
|
|
204
|
+
: [
|
|
205
|
+
{
|
|
206
|
+
name: 'dsh-wb/module-lint/root',
|
|
207
|
+
languageOptions: { parserOptions: { tsconfigRootDir: rootDir } },
|
|
208
|
+
},
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
return [
|
|
212
|
+
// 第一条是**全局 ignores**(只有 `ignores` 一个键;加 `name` 会让 ESLint 不把它当全局忽略)。
|
|
213
|
+
{ ignores: [...DEFAULT_IGNORES, ...ignores] },
|
|
214
|
+
...rootBlock,
|
|
215
|
+
// 基础正确性:`no-unreachable` / `no-dupe-keys` / `no-undef`…
|
|
216
|
+
js.configs.recommended,
|
|
217
|
+
// `any` / `@ts-ignore` / 空的 interface 那一批。typescript-eslint 的 recommended 是**语法级**的
|
|
218
|
+
// (不建 program),所以它不属于"被推迟的类型感知规则"。
|
|
219
|
+
...tseslint.configs.recommended,
|
|
220
|
+
{
|
|
221
|
+
name: FINGERPRINT,
|
|
222
|
+
files: SRC_FILES,
|
|
223
|
+
rules: {
|
|
224
|
+
// `catch {}` 是本仓零注释约定下的刻意写法(注释要用 `_` 前缀表达在场),不是漏写。
|
|
225
|
+
'no-empty': ['error', { allowEmptyCatch: true }],
|
|
226
|
+
'prefer-const': 'error',
|
|
227
|
+
// 上一期的非目标之一:`as any` 把契约绕过去 —— 复杂模块最先烂在这里。
|
|
228
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
229
|
+
// 死代码。`^_` 放行:参数位保留(给契约留形状)时用下划线命名,是刻意的。
|
|
230
|
+
'@typescript-eslint/no-unused-vars': [
|
|
231
|
+
'error',
|
|
232
|
+
{ args: 'after-used', argsIgnorePattern: '^_', caughtErrors: 'none', ignoreRestSiblings: true },
|
|
233
|
+
],
|
|
234
|
+
// arch-guard 把 H01(console 残留)委派给了 eslint。放行 `warn` / `error`:真报错要能报,
|
|
235
|
+
// 调试残留(`log` / `info` / `debug`)不许留。
|
|
236
|
+
'no-console': ['error', { allow: ['warn', 'error'] }],
|
|
237
|
+
'no-debugger': 'error',
|
|
238
|
+
// 手搓外观的 TS 那一半(CSS 那一半归 arch-guard 的 D 族)。
|
|
239
|
+
'no-restricted-syntax': ['error', COLOR_LITERAL, BANG_IMPORTANT],
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'dsh-wb/module-lint/react',
|
|
244
|
+
files: TSX_FILES,
|
|
245
|
+
plugins: { react },
|
|
246
|
+
// `pragma: 'h'` 是模板的形状(`h()` 手写,不经 JSX 编译):`jsx-uses-react` / `jsx-key`
|
|
247
|
+
// 这类规则照样咬得住手写的 `h()`。版本写死而不是 `detect`:`detect` 要读文件系统、
|
|
248
|
+
// 还会打一句警告,而模块拿到的 react 是外壳经模块表给的那一份(与外壳同代)。
|
|
249
|
+
settings: { react: { version: '19.3.0', pragma: 'h', fragment: 'Fragment' } },
|
|
250
|
+
rules: {
|
|
251
|
+
'react/jsx-key': 'error',
|
|
252
|
+
'react/jsx-no-duplicate-props': 'error',
|
|
253
|
+
'react/jsx-no-target-blank': 'error',
|
|
254
|
+
'react/no-children-prop': 'error',
|
|
255
|
+
'react/no-unknown-property': 'error',
|
|
256
|
+
'react/jsx-no-comment-textnodes': 'error',
|
|
257
|
+
'react/jsx-uses-vars': 'error',
|
|
258
|
+
'react/jsx-uses-react': 'error',
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'dsh-wb/module-lint/react-hooks',
|
|
263
|
+
files: TS_FILES,
|
|
264
|
+
plugins: { 'react-hooks': reactHooks },
|
|
265
|
+
rules: {
|
|
266
|
+
// 这一组是**这一期最值钱的一条**:本仓 2026-10-16 那次真实事故(渲染期写 `useRef.current`、
|
|
267
|
+
// 漏订阅、依赖写错)就是没有 lint 造成的。recommended 全量先按 **warn** 收进来(新规则
|
|
268
|
+
// 先看一段时间,不挡门禁),四条已知会真炸的提到 **error**。
|
|
269
|
+
...Object.fromEntries(Object.keys(reactHooks.configs.recommended.rules).map((rule) => [rule, 'warn'])),
|
|
270
|
+
'react-hooks/rules-of-hooks': 'error',
|
|
271
|
+
'react-hooks/refs': 'error',
|
|
272
|
+
'react-hooks/exhaustive-deps': 'error',
|
|
273
|
+
'react-hooks/immutability': 'error',
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'dsh-wb/module-lint/scripts',
|
|
278
|
+
files: SCRIPT_FILES,
|
|
279
|
+
languageOptions: { globals: NODE_GLOBALS },
|
|
280
|
+
rules: SCRIPT_RULES,
|
|
281
|
+
},
|
|
282
|
+
...overrideRules,
|
|
283
|
+
];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* `moduleLint()`(不带 options)产出的**整份方言**的规范形状 —— 判据拿它**逐块比**。
|
|
288
|
+
*
|
|
289
|
+
* 比对面(2026-10-19 扩过一次,是个行为变更):**全部块**,只排除两块 options 驱动的
|
|
290
|
+
* (`/overrides`、`/root`);全局 ignores 那一块也不在这里(它没有 `name` / `files` / `rules`,
|
|
291
|
+
* 形状比不了),单列成下面的 `DIALECT_IGNORES` 按**超集**比。也就是说现在这些块也必须在:
|
|
292
|
+
* `@eslint/js/recommended` · `typescript-eslint/base` · `typescript-eslint/eslint-recommended` ·
|
|
293
|
+
* `typescript-eslint/recommended`,以及 `dsh-wb/module-lint` 那四块 —— 一共 8 块。
|
|
294
|
+
*
|
|
295
|
+
* 为什么算一次就够:方言只有这一份,`moduleLint()` 不带 options 时产出是确定的(同一套常量、
|
|
296
|
+
* 同一个 `tseslint.configs.recommended`),所以"该长什么样"可以在导入时钉死,判据不必自己
|
|
297
|
+
* 再拼一遍。放在文件**最尾**:`moduleLint()` 读的是上面那批模块级常量,早算会撞 TDZ。
|
|
298
|
+
*/
|
|
299
|
+
export const DIALECT_BLOCKS = moduleLint()
|
|
300
|
+
.filter((block) => !isGlobalIgnoresBlock(block))
|
|
301
|
+
.filter((block) => !(typeof block?.name === 'string' && OPTION_BLOCK_NAMES.has(block.name)))
|
|
302
|
+
.map(dialectShapeOf);
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 全局 ignores 的默认那几条("一条都不许少"的那半)。判据按**超集**比:模块可以再多忽略几条
|
|
306
|
+
* (那是 `moduleLint({ ignores })` 这个命名口子),但少一条就点名缺哪条。
|
|
307
|
+
*/
|
|
308
|
+
export const DIALECT_IGNORES = [...DEFAULT_IGNORES];
|
|
309
|
+
|
|
@@ -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
|
+
}
|