@dsh-workbench/module-tools 0.3.2 → 0.3.3
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 +50 -4
- package/package.json +11 -3
- package/src/arch.mjs +234 -0
- package/src/build.mjs +4 -1
- package/src/check.mjs +2 -0
- package/src/checks/arch.mjs +127 -0
- package/src/checks/lint.mjs +351 -0
- package/src/design.mjs +46 -5
- package/src/lint.mjs +309 -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 +2 -0
- 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 +2 -0
- 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,127 @@
|
|
|
1
|
+
// 模块包的**架构判据**(`check` 的第十二条):把 arch-guard 跑在**模块自己的方言**上。
|
|
2
|
+
//
|
|
3
|
+
// 为什么模块包需要这一条:另外十一条判据全是"边界"(自述清单 / 产物 / 可达性 / 卡片渲染 / 样式归属),
|
|
4
|
+
// 没有一条在看**代码结构** —— 而模块包是用户自己写的、模板给的又是能一路长成几百行的起步形状。
|
|
5
|
+
// 结构退化(巨文件、反向依赖、宿主侧拖进组件、未登记的目录、模型层没有测试)以前只靠人自觉。
|
|
6
|
+
//
|
|
7
|
+
// 判据的口径(三条,都要守住):
|
|
8
|
+
// ① **只跑带 `arch.config.mjs` 的模块**:没有那份声明就 `skip`(不适用),不是红。
|
|
9
|
+
// 工坊里已经造好的模块(以及别的仓里已经发出去的模块)不会被这次升级炸掉 —— 补一行就上车。
|
|
10
|
+
// ② 方言住 `../arch.mjs`(`@dsh-workbench/module-tools/arch`),不在这条判据里 ——
|
|
11
|
+
// 判据只负责"跑起来 + 把结果翻成人话"。
|
|
12
|
+
// ③ `cache: false`:模块目录不一定有 node_modules,arch-guard 的 facts 缓存会落到模块根下
|
|
13
|
+
// (实测会打一句"记得加进 .gitignore")—— 门禁不该往用户的包里拉屎。
|
|
14
|
+
//
|
|
15
|
+
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
16
|
+
// 打印与退出码归 `../check.mjs` 那一层。
|
|
17
|
+
|
|
18
|
+
import { existsSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { builtinSourceForms, runGuard } from '@arch-guard/core';
|
|
21
|
+
import { discoverModulePackages } from '../discover.mjs';
|
|
22
|
+
|
|
23
|
+
export const title = '模块架构(分层 / 边界 / 设计系统)';
|
|
24
|
+
|
|
25
|
+
const CONFIG_FILE = 'arch.config.mjs';
|
|
26
|
+
|
|
27
|
+
// "怎么上车"那句话只写一遍:skip 的提示与 fail 的提示都要它。
|
|
28
|
+
const HOW_TO_JOIN =
|
|
29
|
+
`在模块包里放一份 ${CONFIG_FILE},内容一行就够:\n` +
|
|
30
|
+
` import { moduleArch } from '@dsh-workbench/module-tools/arch';\n` +
|
|
31
|
+
' export default moduleArch();\n' +
|
|
32
|
+
' 形状(层序表 / 两个运行时 / 阈值)见 packages/module-tools/src/arch.mjs;' +
|
|
33
|
+
'要开口子就从 moduleArch() 的参数进(deps / layers / roles / exceptions)。';
|
|
34
|
+
|
|
35
|
+
const severityIndex = (rules) => new Map(rules.map((rule) => [rule.id, rule.severity]));
|
|
36
|
+
|
|
37
|
+
const severityOf = (finding, index) => finding.severity ?? index.get(finding.rule) ?? 'error';
|
|
38
|
+
|
|
39
|
+
/** 一条 finding 翻成一行人话:位置 → 说明 → 规则与修法。 */
|
|
40
|
+
function lineOf(name, finding) {
|
|
41
|
+
const where = `${name}: ${finding.file}:${String(finding.line)}`;
|
|
42
|
+
const hint = typeof finding.hint === 'string' && finding.hint !== '' ? ` —— ${finding.hint}` : '';
|
|
43
|
+
return `${where} ${finding.text}(${finding.rule})${hint}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function run() {
|
|
47
|
+
const modulePackages = discoverModulePackages({ root: process.cwd() });
|
|
48
|
+
if (modulePackages.length === 0) {
|
|
49
|
+
return {
|
|
50
|
+
status: 'skip',
|
|
51
|
+
notes: ['没有模块包(没有声明 dsh.workbench 的包)—— 没有源码要判结构'],
|
|
52
|
+
problems: [],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const declared = modulePackages.filter((pkg) => existsSync(join(pkg.dir, CONFIG_FILE)));
|
|
57
|
+
if (declared.length === 0) {
|
|
58
|
+
return {
|
|
59
|
+
status: 'skip',
|
|
60
|
+
notes: [
|
|
61
|
+
`${modulePackages.length} 个模块包都没有 ${CONFIG_FILE} —— 没有架构声明就无从判定(不是"没问题")。` +
|
|
62
|
+
`\n ${HOW_TO_JOIN}`,
|
|
63
|
+
],
|
|
64
|
+
problems: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const problems = [];
|
|
69
|
+
let files = 0;
|
|
70
|
+
let warnings = 0;
|
|
71
|
+
// 真的跑过的规则 id(与 arch-guard 报告头那句"规则 58/107"同一个口径:`stats` 是逐规则执行统计)。
|
|
72
|
+
const ranRules = new Set();
|
|
73
|
+
|
|
74
|
+
for (const pkg of declared) {
|
|
75
|
+
const name = pkg.manifest.name;
|
|
76
|
+
let result;
|
|
77
|
+
try {
|
|
78
|
+
result = await runGuard({
|
|
79
|
+
cwd: pkg.dir,
|
|
80
|
+
// **相对**路径:arch-guard 把 configPath 拼在 cwd 后面(实测传绝对路径会拼成
|
|
81
|
+
// `<cwd>/<绝对路径>`,再报"找不到配置文件")。
|
|
82
|
+
configPath: CONFIG_FILE,
|
|
83
|
+
sourceForms: builtinSourceForms,
|
|
84
|
+
cache: false,
|
|
85
|
+
quiet: true,
|
|
86
|
+
format: 'json',
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
90
|
+
problems.push(
|
|
91
|
+
`${name}: ${CONFIG_FILE} 跑不起来 —— ${message}(先确认这份配置能 import、没有语法错,` +
|
|
92
|
+
'再确认它 import 的东西在工坊里解析得到)',
|
|
93
|
+
);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// `scopeFiles` 在 full 档是空的(实测)—— 解析过的文件数从 facts 缓存那份统计里拿
|
|
98
|
+
// (`cache: false` 时 `misses` 就是这一轮解析了几个文件,见 arch-guard 的 RunResult 注释)。
|
|
99
|
+
files += result.cache.misses;
|
|
100
|
+
for (const stat of result.stats) ranRules.add(stat.rule);
|
|
101
|
+
const index = severityIndex(result.config.rules);
|
|
102
|
+
for (const finding of result.active) {
|
|
103
|
+
if (severityOf(finding, index) === 'error') problems.push(lineOf(name, finding));
|
|
104
|
+
else warnings += 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (problems.length > 0) {
|
|
109
|
+
return {
|
|
110
|
+
status: 'fail',
|
|
111
|
+
// 提示放在 notes 里,判据层照旧只报"哪几条不过"。
|
|
112
|
+
notes: [`红了怎么修:先看位置与规则号,再决定"改代码"还是"在 ${CONFIG_FILE} 里开口子"。`],
|
|
113
|
+
problems,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
status: 'pass',
|
|
119
|
+
notes: [
|
|
120
|
+
`${declared.length}/${modulePackages.length} 个模块包带 ${CONFIG_FILE} · ${files} 个源文件 · ` +
|
|
121
|
+
`${ranRules.size} 条规则在跑` +
|
|
122
|
+
(warnings === 0 ? '' : ` · ${warnings} 条 warn(不挡门禁)`) +
|
|
123
|
+
(declared.length === modulePackages.length ? '' : `;其余 ${modulePackages.length - declared.length} 个没声明(不适用)`),
|
|
124
|
+
],
|
|
125
|
+
problems: [],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -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
|
+
}
|
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', '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,20 @@ 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?.failure)} |`,
|
|
103
|
+
`| 当初探明 / 预览用的那次调用(原文) | ${soft(write?.probeCommand)} |`,
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
|
|
83
107
|
function text(value) {
|
|
84
108
|
return typeof value === 'string' ? value.trim() : '';
|
|
85
109
|
}
|
|
@@ -121,6 +145,7 @@ function skeleton(manifest = {}) {
|
|
|
121
145
|
timeout: '',
|
|
122
146
|
probeCommand: '',
|
|
123
147
|
},
|
|
148
|
+
writes: [],
|
|
124
149
|
acceptance: [{ step: '' }],
|
|
125
150
|
};
|
|
126
151
|
}
|
|
@@ -178,6 +203,12 @@ export function emptyCells(design) {
|
|
|
178
203
|
for (const field of DATASOURCE_FIELDS) need(cells, read?.[field], `${label}.${field}`);
|
|
179
204
|
});
|
|
180
205
|
|
|
206
|
+
const writes = writesOf(design);
|
|
207
|
+
writes.forEach((write, index) => {
|
|
208
|
+
const label = writes.length === 1 ? 'write' : `writes[${index}]`;
|
|
209
|
+
for (const field of WRITE_FIELDS) need(cells, write?.[field], `${label}.${field}`);
|
|
210
|
+
});
|
|
211
|
+
|
|
181
212
|
return cells;
|
|
182
213
|
}
|
|
183
214
|
|
|
@@ -206,6 +237,7 @@ export function renderDesign(design) {
|
|
|
206
237
|
const acceptance = asRows(design?.acceptance);
|
|
207
238
|
const states = asRows(design?.states);
|
|
208
239
|
const reads = readsOf(design);
|
|
240
|
+
const writes = writesOf(design);
|
|
209
241
|
|
|
210
242
|
const stateRows = STATE_KEYS.map((key, index) => {
|
|
211
243
|
const row = states.find((one) => text(one?.key) === key) ?? {};
|
|
@@ -277,7 +309,16 @@ export function renderDesign(design) {
|
|
|
277
309
|
...(reads.length <= 1
|
|
278
310
|
? [...datasourceRowsOf(reads[0]), '']
|
|
279
311
|
: reads.flatMap((one, index) => [`#### 读 ${String(index + 1)}`, '', ...datasourceRowsOf(one), ''])),
|
|
280
|
-
|
|
312
|
+
...(writes.length === 0
|
|
313
|
+
? []
|
|
314
|
+
: [
|
|
315
|
+
'### 写通道探明记录',
|
|
316
|
+
'',
|
|
317
|
+
...(writes.length <= 1
|
|
318
|
+
? [...writeRowsOf(writes[0]), '']
|
|
319
|
+
: writes.flatMap((one, index) => [`#### 写 ${String(index + 1)}`, '', ...writeRowsOf(one), ''])),
|
|
320
|
+
]),
|
|
321
|
+
'### 验收(自己在真卡片上逐条走通后打勾:`dsh-wb-module design accept`)',
|
|
281
322
|
'',
|
|
282
323
|
...acceptanceRows,
|
|
283
324
|
'',
|
|
@@ -351,7 +392,7 @@ function checkDesign(root) {
|
|
|
351
392
|
console.error('✖ README 里那一段落后于 design.json(或者没有标记)—— 跑 dsh-wb-module design render');
|
|
352
393
|
}
|
|
353
394
|
if (cells.length === 0 && fresh) {
|
|
354
|
-
console.log(`✔ 三张表填全了,README 也是最新的${accepted === acceptance.length ? ',验收也打完了' : `;验收还差 ${acceptance.length - accepted}
|
|
395
|
+
console.log(`✔ 三张表填全了,README 也是最新的${accepted === acceptance.length ? ',验收也打完了' : `;验收还差 ${acceptance.length - accepted} 条(自己在真卡片上逐条走通后跑 design accept)`}`);
|
|
355
396
|
}
|
|
356
397
|
process.exit(cells.length === 0 && fresh ? 0 : 1);
|
|
357
398
|
}
|
|
@@ -399,7 +440,7 @@ export async function design({ root, args = [] }) {
|
|
|
399
440
|
design0.acceptedAt = new Date().toISOString();
|
|
400
441
|
writeDesign(root, design0);
|
|
401
442
|
applyToReadme(root, renderDesign(design0));
|
|
402
|
-
console.log(`✔ ${acceptance.length}
|
|
443
|
+
console.log(`✔ ${acceptance.length} 条验收全部打勾(只在真的在真卡片上逐条走通过之后再跑这条)`);
|
|
403
444
|
return;
|
|
404
445
|
}
|
|
405
446
|
|