@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
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// 模块包的**语法卫生判据**(`check` 的第十三条):把 eslint 跑在**模块自己的方言**上。
|
|
2
|
+
//
|
|
3
|
+
// 为什么模块包需要这一条:另外十二条判据里,结构那一半归 arch-guard(`./arch.mjs`),
|
|
4
|
+
// 而"单文件语法卫生"那一半一条都没有 —— `as any` / `console` 残留 / `debugger` / 内联颜色 /
|
|
5
|
+
// 手写 `!important` / 渲染期写 ref / 漏依赖,这些都不改变文件形状,结构判据永远看不见。
|
|
6
|
+
// arch-guard 自己的 README 把这一半明确委派给了 eslint。
|
|
7
|
+
//
|
|
8
|
+
// 判据的口径(三条,与第十二条 `arch` 逐条对齐 —— 同一套承诺):
|
|
9
|
+
// ① **只跑带 `eslint.config.mjs` 的模块**:没有那份文件就 `skip`(不适用),不是红。
|
|
10
|
+
// 工坊里已经造好的模块(以及别的仓里已经发出去的模块)不会被这次升级炸掉 —— 补两行就上车。
|
|
11
|
+
// ② **判据不自己编规则**:它 import 模块那份配置,**逐块核对整份方言**(`../lint.mjs` 的
|
|
12
|
+
// `DIALECT_BLOCKS` 八块:名字 / `files` / `rules` 一处不一致就红;全局 ignores 按 `DIALECT_IGNORES`
|
|
13
|
+
// 的**超集**比)再跑;规则与阈值都在 `../lint.mjs` 那一处。
|
|
14
|
+
// **残余**:模块自己 import 方言再照抄整份,判据分不出来 —— 这条的目标是"抬高门槛 +
|
|
15
|
+
// 让改动必须明着写",不是防住蓄意伪造(详见 `../lint.mjs` 文件头那段)。
|
|
16
|
+
// ③ **只把 `error` 算红**:`warn` 照外壳口径计数、不挡门禁(`eslint .` 也只对 error 退 1);
|
|
17
|
+
// 单跑(`--only lint`)时把 warn 逐条列出来(与 `--only kit` 列 kit 成员同一个约定)。
|
|
18
|
+
//
|
|
19
|
+
// ESLint 的**唯一构造处**也在这里(`lintPackage`):判据与 `dsh-wb-module lint [--fix]` 共用它,
|
|
20
|
+
// 于是"门禁跑的那份"与"命令跑的那份"在形状上不可能漂。**判据永远不动手改文件**:只有
|
|
21
|
+
// `fix: true` 才会写回,而 `fix` 只从命令那条路进来(自检有负例钉着这一点)。
|
|
22
|
+
//
|
|
23
|
+
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
24
|
+
// 打印与退出码归 `../check.mjs` 那一层。
|
|
25
|
+
|
|
26
|
+
import { existsSync } from 'node:fs';
|
|
27
|
+
import { join, relative } from 'node:path';
|
|
28
|
+
import { pathToFileURL } from 'node:url';
|
|
29
|
+
import { ESLint } from 'eslint';
|
|
30
|
+
import { discoverModulePackages } from '../discover.mjs';
|
|
31
|
+
import { DIALECT_BLOCKS, DIALECT_IGNORES, FINGERPRINT, dialectShapeOf, isGlobalIgnoresBlock } from '../lint.mjs';
|
|
32
|
+
|
|
33
|
+
export const title = '模块代码卫生(eslint)';
|
|
34
|
+
|
|
35
|
+
/** 上车标记:模块根有这份文件才被这条判据管(opt-in,与第十二条同一形状)。 */
|
|
36
|
+
export const CONFIG_FILE = 'eslint.config.mjs';
|
|
37
|
+
|
|
38
|
+
// 聚合跑时逐条列出的上限:一条模块可能带几百条 error,门禁那一屏不该被它刷掉、也不该只剩
|
|
39
|
+
// 最后几条 —— 截断 + 一句"还有 N 条"(单跑 `--only lint` 时不截断:那时你就是来看全的)。
|
|
40
|
+
const MAX_PROBLEMS = 40;
|
|
41
|
+
|
|
42
|
+
// "怎么上车"那句话只写一遍:skip 的提示与方言对不上的人都该看见它。
|
|
43
|
+
// 不提本仓内部路径(`src/lint.mjs` 那句):这份提示会随 skill 文档发给用户,用户手上只有
|
|
44
|
+
// 装出来的那个包。方言与阈值随 `@dsh-workbench/module-tools` 同代。
|
|
45
|
+
const HOW_TO_JOIN =
|
|
46
|
+
`在模块包里放一份 ${CONFIG_FILE},内容两行就够(与模板里那份逐字一致):\n` +
|
|
47
|
+
` import { moduleLint } from '@dsh-workbench/module-tools/lint';\n` +
|
|
48
|
+
' export default moduleLint({ rootDir: import.meta.dirname });\n' +
|
|
49
|
+
' `rootDir` 是给 typescript-eslint 钉 tsconfigRootDir 的(一个进程里同时有多份 eslint 配置时必需;\n' +
|
|
50
|
+
' 模块自己单独跑一份配置时可以省)。规则与阈值只有这一份(随 @dsh-workbench/module-tools 同代);\n' +
|
|
51
|
+
' 要开口子就从 moduleLint({ ignores, rules, rootDir }) 的参数进。';
|
|
52
|
+
|
|
53
|
+
const messageOf = (error) => (error instanceof Error ? error.message : String(error));
|
|
54
|
+
|
|
55
|
+
// 一行一条:把消息压成单行再截断。react-hooks 那几条的 message 里带**整段代码框**(多行),
|
|
56
|
+
// 原样倒出来会把"逐条"变成几十行、`·` 前缀也只挂在第一行;前面那句结论("Cannot call impure
|
|
57
|
+
// function during render")才是要看的东西,所以压平 + 截断。
|
|
58
|
+
const TEXT_LIMIT = 200;
|
|
59
|
+
function oneLine(text) {
|
|
60
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
61
|
+
return flat.length > TEXT_LIMIT ? `${flat.slice(0, TEXT_LIMIT)}…` : flat;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 一行人话:`名字: 文件:行:列 规则 —— 一句话`(规则缺了就是解析错误 —— 那也是要立刻看见的)。 */
|
|
65
|
+
function lineOf(name, dir, result, message) {
|
|
66
|
+
const line = message.line ?? 1;
|
|
67
|
+
const column = message.column ?? 1;
|
|
68
|
+
const where = `${name}: ${relative(dir, result.filePath)}:${String(line)}:${String(column)}`;
|
|
69
|
+
const rule = message.ruleId ?? '解析错误';
|
|
70
|
+
return `${where} ${rule} —— ${oneLine(message.message)}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 方言那几块的名字(基础块 + 四块 `dsh-wb/module-lint*`)+ 全局 ignores 那一块 —— 认"有没有方言痕迹"。 */
|
|
74
|
+
const DIALECT_NAMES = new Set(DIALECT_BLOCKS.map((block) => block.name));
|
|
75
|
+
|
|
76
|
+
function isDialectNamed(block) {
|
|
77
|
+
return (
|
|
78
|
+
block !== null &&
|
|
79
|
+
typeof block === 'object' &&
|
|
80
|
+
typeof block.name === 'string' &&
|
|
81
|
+
(DIALECT_NAMES.has(block.name) || block.name.startsWith(`${FINGERPRINT}/`))
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 配置里有没有一点方言的痕迹(全局 ignores 那一块 / 方言那几块)—— 用来区分"没上车"与"改坏了"。 */
|
|
86
|
+
function hasDialectTrace(config) {
|
|
87
|
+
return config.some((block) => isGlobalIgnoresBlock(block) || isDialectNamed(block));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 配置里所有全局 ignores 块的 glob 并集(方言那一块 + 模块自己追加的,可能有不止一块)。 */
|
|
91
|
+
function globalIgnoresOf(config) {
|
|
92
|
+
const globs = new Set();
|
|
93
|
+
for (const block of config) {
|
|
94
|
+
if (block !== null && typeof block === 'object' && isGlobalIgnoresBlock(block)) {
|
|
95
|
+
for (const glob of block.ignores) globs.add(glob);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return globs;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 两份 rules 差在哪:少了哪些 / 多了哪些 / 哪些值被改了(形状已键排序,逐键比即可)。 */
|
|
102
|
+
function rulesDivergenceOf(expected, actual) {
|
|
103
|
+
const gone = Object.keys(expected).filter((rule) => !(rule in actual));
|
|
104
|
+
const extra = Object.keys(actual).filter((rule) => !(rule in expected));
|
|
105
|
+
const changed = Object.keys(expected).filter(
|
|
106
|
+
(rule) => rule in actual && JSON.stringify(expected[rule]) !== JSON.stringify(actual[rule]),
|
|
107
|
+
);
|
|
108
|
+
const parts = [];
|
|
109
|
+
if (gone.length > 0) parts.push(`少了 ${gone.join('、')}`);
|
|
110
|
+
if (extra.length > 0) parts.push(`多了 ${extra.join('、')}`);
|
|
111
|
+
if (changed.length > 0) parts.push(`改了 ${changed.join('、')}`);
|
|
112
|
+
return parts.join(';');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* **逐块比**方言(这一期 hardening 的核心):`moduleLint()` 产出的**整份方言**,模块那份配置里
|
|
117
|
+
* 必须一块不少、且名字 / `files` / `rules` 逐项一致;全局 ignores 那一块按**超集**比(多忽略
|
|
118
|
+
* 可以、少忽略点名)。
|
|
119
|
+
*
|
|
120
|
+
* 为什么不能只比"有没有那个名字":2026-10-19 verifier 造出
|
|
121
|
+
* `export default [{ name: 'dsh-wb/module-lint', rules: {} }]` —— 只比名字时它给 ✔ 0 error,
|
|
122
|
+
* 而"方言只有一份、下限不能被悄悄删"正是这条判据的立论。同名多块也判红:扁平配置里**只有
|
|
123
|
+
* 最后一块算数**,"留一块好的、再追加一块掏空的"同样是把下限删掉。
|
|
124
|
+
*
|
|
125
|
+
* 为什么后来把基础块也收进来:只比 `dsh-wb/module-lint*` 那 4 块时,verifier 量到的残余有**两种
|
|
126
|
+
* 筛法**:`moduleLint().filter((b) => b.name !== undefined)` 筛掉的是**唯一没有名字**的那块(全局
|
|
127
|
+
* ignores 不能带 `name`);基础块(`@eslint/js/recommended` 与三块 `typescript-eslint/*`,**都带
|
|
128
|
+
* 名字**)则要**显式点名**(`(b) => b.name !== '@eslint/js/recommended'` 这种)才筛得掉 ——
|
|
129
|
+
* 两种都是"下限被悄悄删"。
|
|
130
|
+
*/
|
|
131
|
+
function dialectDivergences(config) {
|
|
132
|
+
const blocks = config.filter((block) => block !== null && typeof block === 'object');
|
|
133
|
+
const problems = [];
|
|
134
|
+
|
|
135
|
+
const missingGlobs = DIALECT_IGNORES.filter((glob) => !globalIgnoresOf(config).has(glob));
|
|
136
|
+
if (missingGlobs.length > 0) problems.push(`少了全局 ignores(缺 ${missingGlobs.join('、')})`);
|
|
137
|
+
|
|
138
|
+
for (const expected of DIALECT_BLOCKS) {
|
|
139
|
+
const same = blocks.filter((block) => block.name === expected.name);
|
|
140
|
+
if (same.length === 0) {
|
|
141
|
+
problems.push(`少了「${expected.name}」那一块`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (same.length > 1) {
|
|
145
|
+
problems.push(`「${expected.name}」出现了 ${String(same.length)} 次(同名块只有最后一块算数)`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const actual = dialectShapeOf(same[0]);
|
|
149
|
+
if (JSON.stringify(actual.files) !== JSON.stringify(expected.files)) {
|
|
150
|
+
problems.push(`改了「${expected.name}」那一块的 files`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const rules = rulesDivergenceOf(expected.rules, actual.rules);
|
|
154
|
+
if (rules !== '') problems.push(`改了「${expected.name}」那一块的 rules(${rules})`);
|
|
155
|
+
}
|
|
156
|
+
return problems;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function loadConfig(pkg) {
|
|
160
|
+
const file = join(pkg.dir, CONFIG_FILE);
|
|
161
|
+
let module_;
|
|
162
|
+
try {
|
|
163
|
+
module_ = await import(pathToFileURL(file).href);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return {
|
|
166
|
+
error:
|
|
167
|
+
`${pkg.manifest.name}: ${CONFIG_FILE} 跑不起来 —— ${messageOf(error)}` +
|
|
168
|
+
'(先确认这份配置能 import、没有语法错,再确认它 import 的东西在工坊里解析得到)',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const config = module_.default;
|
|
173
|
+
if (!Array.isArray(config)) {
|
|
174
|
+
return {
|
|
175
|
+
error:
|
|
176
|
+
`${pkg.manifest.name}: ${CONFIG_FILE} 的默认导出不是 flat config 数组 —— ` +
|
|
177
|
+
`这份配置不是 moduleLint() 来的。\n ${HOW_TO_JOIN}`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (!hasDialectTrace(config)) {
|
|
181
|
+
return {
|
|
182
|
+
error:
|
|
183
|
+
`${pkg.manifest.name}: ${CONFIG_FILE} 里没有 ${FINGERPRINT} 那几块 —— ` +
|
|
184
|
+
`这份配置不是 moduleLint() 来的(要加规则请展开 moduleLint() 再追加一块,别手写一份自己的方言)。` +
|
|
185
|
+
`\n ${HOW_TO_JOIN}`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const diverged = dialectDivergences(config);
|
|
189
|
+
if (diverged.length > 0) {
|
|
190
|
+
return {
|
|
191
|
+
error:
|
|
192
|
+
`${pkg.manifest.name}: ${CONFIG_FILE} 里的方言与 moduleLint() 产出的不一致 —— ` +
|
|
193
|
+
`${diverged.join(';')}。要开口子请走 moduleLint({ rules, ignores })(rules 落在 ` +
|
|
194
|
+
`${FINGERPRINT}/overrides 那块、ignores 加在全局 ignores 那块)或展开 moduleLint() ` +
|
|
195
|
+
'再追加自己的一块,别改方言那几块。',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return { config };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 跑一个模块包的 lint。**ESLint 只在这里构造**(判据与 `dsh-wb-module lint` 共用)。
|
|
203
|
+
*
|
|
204
|
+
* @param {{ dir: string, manifest: { name: string } }} pkg
|
|
205
|
+
* @param {{ fix?: boolean }} [options] `fix` 会写回文件;判据那条路永远不传它。
|
|
206
|
+
* @returns {Promise<object>} `{ name, files, errors, warnings, problems, warnLines, fixed }`
|
|
207
|
+
* 或 `{ name, error }`(配置跑不起来 / 不是 moduleLint 来的)
|
|
208
|
+
*/
|
|
209
|
+
export async function lintPackage(pkg, { fix = false } = {}) {
|
|
210
|
+
const name = pkg.manifest.name;
|
|
211
|
+
const loaded = await loadConfig(pkg);
|
|
212
|
+
if (loaded.error !== undefined) return { name, error: loaded.error };
|
|
213
|
+
|
|
214
|
+
const runOnce = (fixMode) =>
|
|
215
|
+
new ESLint({
|
|
216
|
+
cwd: pkg.dir,
|
|
217
|
+
// 不读目录里那份配置,用**这里 import 进来的**:指纹已经核过,`files` 相对 cwd 解析
|
|
218
|
+
// (Lead 探针实测这条路可行)。这样"判据跑的那份"与"模块文件里写的那份"是同一个对象。
|
|
219
|
+
overrideConfigFile: true,
|
|
220
|
+
// 头一块**只**补 `tsconfigRootDir`:typescript-eslint 的 parser 拿不到它时会去"猜",
|
|
221
|
+
// 猜法是看调用 `tseslint.configs` 的那段栈 —— 一次进程里跑两个模块包就累积出两个候选
|
|
222
|
+
// 根目录,于是**第二个包**每个文件都报 "No tsconfigRootDir was set, and multiple
|
|
223
|
+
// candidate TSConfigRootDirs are present"(实测:两个模板一起跑,mini 全红)。
|
|
224
|
+
// 判据知道自己在跑哪个包,就把这件事钉死;模块自己写的那份配置若声明了
|
|
225
|
+
// parserOptions,它在后面,仍然覆盖这一条。
|
|
226
|
+
overrideConfig: [
|
|
227
|
+
{
|
|
228
|
+
// 不写 `files`:typescript-eslint 的 base 块不带 `files`,于是**所有**文件(含
|
|
229
|
+
// `scripts/*.mjs` 与两份配置)都走 TS parser —— 只给 `*.ts` 补根目录,`.mjs` 那三个
|
|
230
|
+
// 照样报同一个错(实测)。
|
|
231
|
+
languageOptions: { parserOptions: { tsconfigRootDir: pkg.dir } },
|
|
232
|
+
},
|
|
233
|
+
...loaded.config,
|
|
234
|
+
],
|
|
235
|
+
errorOnUnmatchedPattern: false,
|
|
236
|
+
fix: fixMode,
|
|
237
|
+
}).lintFiles(['.']);
|
|
238
|
+
|
|
239
|
+
// `--fix` 的报数要"修了 N 条":fix 那一趟返回的结果是**修完之后**的(实测:`output` 有值、
|
|
240
|
+
// messages 只剩修不动的),所以前后各跑一趟普通轮,差值才是真修掉的那批。
|
|
241
|
+
//
|
|
242
|
+
// 整段包 try:一份"长得像方言但少了 plugin / 塞了坏东西"的配置会让 ESLint 在**建配置**那一步
|
|
243
|
+
// 抛(`Could not find plugin …`)—— 那是"跑不起来"这一类,要落成一行人话,不是把判据整条崩掉。
|
|
244
|
+
let results;
|
|
245
|
+
let before;
|
|
246
|
+
try {
|
|
247
|
+
before = fix ? await runOnce(false) : undefined;
|
|
248
|
+
if (fix) {
|
|
249
|
+
const fixed = await runOnce(true);
|
|
250
|
+
await ESLint.outputFixes(fixed);
|
|
251
|
+
}
|
|
252
|
+
results = await runOnce(false);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
return { name, error: `${name}: ${CONFIG_FILE} 跑不起来 —— ${messageOf(error)}` };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const problems = [];
|
|
258
|
+
const warnLines = [];
|
|
259
|
+
let files = 0;
|
|
260
|
+
let errors = 0;
|
|
261
|
+
let warnings = 0;
|
|
262
|
+
for (const result of results) {
|
|
263
|
+
files += 1;
|
|
264
|
+
errors += result.errorCount;
|
|
265
|
+
warnings += result.warningCount;
|
|
266
|
+
for (const message of result.messages) {
|
|
267
|
+
if (message.severity === 2) problems.push(lineOf(name, pkg.dir, result, message));
|
|
268
|
+
else warnLines.push(lineOf(name, pkg.dir, result, message));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const totalOf = (list) => list.reduce((sum, result) => sum + result.errorCount + result.warningCount, 0);
|
|
273
|
+
return {
|
|
274
|
+
name,
|
|
275
|
+
files,
|
|
276
|
+
errors,
|
|
277
|
+
warnings,
|
|
278
|
+
problems,
|
|
279
|
+
warnLines,
|
|
280
|
+
fixed: before === undefined ? 0 : totalOf(before) - (errors + warnings),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function run() {
|
|
285
|
+
const modulePackages = discoverModulePackages({ root: process.cwd() });
|
|
286
|
+
if (modulePackages.length === 0) {
|
|
287
|
+
return {
|
|
288
|
+
status: 'skip',
|
|
289
|
+
notes: ['没有模块包(没有声明 dsh.workbench 的包)—— 没有源码要判卫生'],
|
|
290
|
+
problems: [],
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const declared = modulePackages.filter((pkg) => existsSync(join(pkg.dir, CONFIG_FILE)));
|
|
295
|
+
if (declared.length === 0) {
|
|
296
|
+
return {
|
|
297
|
+
status: 'skip',
|
|
298
|
+
notes: [
|
|
299
|
+
`${modulePackages.length} 个模块包都没有 ${CONFIG_FILE} —— 没有这份配置就没有规则可跑(不是"没问题")。` +
|
|
300
|
+
`\n ${HOW_TO_JOIN}`,
|
|
301
|
+
],
|
|
302
|
+
problems: [],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const problems = [];
|
|
307
|
+
const warnLines = [];
|
|
308
|
+
let files = 0;
|
|
309
|
+
let errors = 0;
|
|
310
|
+
let warnings = 0;
|
|
311
|
+
|
|
312
|
+
for (const pkg of declared) {
|
|
313
|
+
const result = await lintPackage(pkg);
|
|
314
|
+
if (result.error !== undefined) {
|
|
315
|
+
problems.push(result.error);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
files += result.files;
|
|
319
|
+
errors += result.errors;
|
|
320
|
+
warnings += result.warnings;
|
|
321
|
+
problems.push(...result.problems);
|
|
322
|
+
warnLines.push(...result.warnLines);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (problems.length > 0) {
|
|
326
|
+
// 单跑时列全(你就是来看全的);聚合跑截断,把"还有 N 条"说清楚。
|
|
327
|
+
const only = process.env.DSH_WB_ONLY === 'lint';
|
|
328
|
+
const shown = only ? problems : problems.slice(0, MAX_PROBLEMS);
|
|
329
|
+
const notes = [
|
|
330
|
+
'红了怎么修:先看位置与规则名,再决定"改代码"还是"就地 eslint-disable-next-line";' +
|
|
331
|
+
'要整体放开就展开 moduleLint() 再追加一块(别把方言换掉)。',
|
|
332
|
+
];
|
|
333
|
+
if (shown.length < problems.length) {
|
|
334
|
+
notes.push(`还有 ${problems.length - shown.length} 条没列出来(跑 dsh-wb-module check --only lint 看全)。`);
|
|
335
|
+
}
|
|
336
|
+
return { status: 'fail', notes, problems: shown };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const notes = [
|
|
340
|
+
`${declared.length}/${modulePackages.length} 个模块包带 ${CONFIG_FILE} · ${files} 个文件 · ` +
|
|
341
|
+
`${errors} error · ${warnings} warn`,
|
|
342
|
+
];
|
|
343
|
+
if (warnLines.length > 0 && process.env.DSH_WB_ONLY === 'lint') {
|
|
344
|
+
notes.push('warn(不挡门禁):');
|
|
345
|
+
for (const line of warnLines) notes.push(` ${line}`);
|
|
346
|
+
}
|
|
347
|
+
if (declared.length < modulePackages.length) {
|
|
348
|
+
notes.push(`其余 ${modulePackages.length - declared.length} 个没有 ${CONFIG_FILE}(不适用)`);
|
|
349
|
+
}
|
|
350
|
+
return { status: 'pass', notes, problems: [] };
|
|
351
|
+
}
|
|
@@ -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
|
}
|