@dsh-workbench/module-tools 0.3.1 → 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 +135 -8
- package/bin/dsh-wb-module.mjs +50 -4
- package/package.json +12 -4
- 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
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
|
+
|
package/template/README.md.tpl
CHANGED
|
@@ -4,19 +4,34 @@
|
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
dsh-wb-module build # 出 dist/host.js 与套了 loader 壳的 client.js
|
|
7
|
-
dsh-wb-module check # 契约 / 产物边界 / 源码边界 / dsh 原语 / kit 面 / 卡片渲染 / 独立构建 / 内部标识 / 样式归属 / 不指外壳
|
|
8
|
-
dsh-wb-module check --only cards #
|
|
7
|
+
dsh-wb-module check # 契约 / 声明协议 / 产物边界 / 源码边界 / 架构 / dsh 原语 / kit 面 / 卡片渲染 / 独立构建 / 内部标识 / 样式归属 / 不指外壳 / 代码卫生(eslint)
|
|
8
|
+
dsh-wb-module check --only cards # 十三条可以单跑
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
包自带的六个脚本(`build` / `typecheck` / `test` / `selfcheck` 是 `check` 的契约判据要求的那四个;`check` 跑全部判据,`lint` 只跑源码卫生那一条):
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
pnpm build # 构建产物
|
|
15
15
|
pnpm typecheck # tsc --noEmit
|
|
16
16
|
pnpm test # vitest(src/**/*.test.ts)
|
|
17
17
|
pnpm selfcheck # 构建 + 把 client.js 当浏览器加载器跑一遍,看卡片真进没进外壳
|
|
18
|
+
pnpm check # dsh-wb-module check
|
|
19
|
+
pnpm lint # dsh-wb-module lint(`--fix` 就地改文件;`check` 永不动手)
|
|
18
20
|
```
|
|
19
21
|
|
|
22
|
+
形状是**分层多文件**:`src/components/HelloCard.tsx` 是这张卡(卡壳 + 两句正文),
|
|
23
|
+
`src/client.tsx` 只剩面(注册 + 把 props 交给组件),`src/host.ts` 是宿主面,
|
|
24
|
+
`src/messages.ts` 是文案。目录**按需要出现** —— 这份没有纯逻辑,所以没有 `src/model/`;
|
|
25
|
+
一旦有了(换算 / 状态机 / 类型),就放 `src/model/**` 并配一份同目录测试。
|
|
26
|
+
两个面在第 10 层、`components/` 第 2 层、`model/` 与 `messages.ts` 第 1 层:
|
|
27
|
+
上层只许 import 下层的,`arch.config.mjs` 那份方言 + `check` 的 `arch` 判据会当场报
|
|
28
|
+
(宿主侧 import `components/**` 是跨运行时,也红)。
|
|
29
|
+
|
|
30
|
+
包根还有两份**只有两行**的方言配置,各自一条判据按"文件在不在"上车:
|
|
31
|
+
`arch.config.mjs`(`moduleArch()`:层序 / 反向依赖 / 跨运行时)与
|
|
32
|
+
`eslint.config.mjs`(`moduleLint()`:`console` / `as any` / hooks 那几条 / 手搓颜色字面量);
|
|
33
|
+
要开口子都从工厂参数进,别手写一份自己的配置。
|
|
34
|
+
|
|
20
35
|
装进本地工作台:
|
|
21
36
|
|
|
22
37
|
```bash
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import { HelloCard } from './components/HelloCard';
|
|
3
3
|
import { messages } from './messages';
|
|
4
4
|
|
|
5
5
|
const UNIT_ID = '__UNIT__';
|
|
6
6
|
|
|
7
|
-
type Rendered = ReturnType<typeof h>;
|
|
8
7
|
type Translate = (key: string, params?: Record<string, unknown>) => string;
|
|
9
8
|
|
|
10
9
|
interface UnitClientFace {
|
|
11
10
|
readonly unit: string;
|
|
12
11
|
readonly messages: { readonly zh: Readonly<Record<string, string>>; readonly en: Readonly<Record<string, string>> };
|
|
13
|
-
readonly card: (props: { readonly t: Translate; readonly onRefresh: () => void }) =>
|
|
12
|
+
readonly card: (props: { readonly t: Translate; readonly onRefresh: () => void }) => ReactElement;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
interface UnitClientApi {
|
|
@@ -25,20 +24,7 @@ interface ClientContext {
|
|
|
25
24
|
const face: UnitClientFace = {
|
|
26
25
|
unit: UNIT_ID,
|
|
27
26
|
messages,
|
|
28
|
-
card: (props) => {
|
|
29
|
-
const { Card } = kit.components;
|
|
30
|
-
return h(
|
|
31
|
-
Card,
|
|
32
|
-
{
|
|
33
|
-
t: props.t,
|
|
34
|
-
title: props.t(`${UNIT_ID}.cardTitle`),
|
|
35
|
-
refreshing: false,
|
|
36
|
-
onRefresh: props.onRefresh,
|
|
37
|
-
},
|
|
38
|
-
h('p', null, props.t(`${UNIT_ID}.heading`)),
|
|
39
|
-
h('p', null, props.t(`${UNIT_ID}.body`)),
|
|
40
|
-
);
|
|
41
|
-
},
|
|
27
|
+
card: (props) => HelloCard({ t: props.t, onRefresh: props.onRefresh }),
|
|
42
28
|
};
|
|
43
29
|
|
|
44
30
|
export const inject = ['workbenchUnits'];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createElement as h } from 'react';
|
|
2
|
+
import { kit } from '@dsh-workbench/core/client';
|
|
3
|
+
|
|
4
|
+
const UNIT_ID = '__UNIT__';
|
|
5
|
+
|
|
6
|
+
const { Card } = kit.components;
|
|
7
|
+
|
|
8
|
+
type Rendered = ReturnType<typeof h>;
|
|
9
|
+
type Translate = (key: string, params?: Record<string, unknown>) => string;
|
|
10
|
+
|
|
11
|
+
export interface HelloCardProps {
|
|
12
|
+
readonly t: Translate;
|
|
13
|
+
readonly onRefresh: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function HelloCard(props: HelloCardProps): Rendered {
|
|
17
|
+
return h(
|
|
18
|
+
Card,
|
|
19
|
+
{
|
|
20
|
+
t: props.t,
|
|
21
|
+
title: props.t(`${UNIT_ID}.cardTitle`),
|
|
22
|
+
refreshing: false,
|
|
23
|
+
onRefresh: props.onRefresh,
|
|
24
|
+
},
|
|
25
|
+
h('p', null, props.t(`${UNIT_ID}.heading`)),
|
|
26
|
+
h('p', null, props.t(`${UNIT_ID}.body`)),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -8,22 +8,24 @@
|
|
|
8
8
|
```bash
|
|
9
9
|
dsh-wb-module init <unit> --dir <目录> --template diy # 生成这份骨架(默认模板是没有数据源的空壳)
|
|
10
10
|
dsh-wb-module build # 出 dist/host.js 与套了 loader 壳的 client.js
|
|
11
|
-
dsh-wb-module check #
|
|
12
|
-
dsh-wb-module check --only cards #
|
|
11
|
+
dsh-wb-module check # 十三条判据:契约 / 声明协议 / 产物边界 / 源码边界 / 架构 / dsh 原语 / kit 面 / 卡片渲染 / 独立构建 / 内部标识 / 样式归属 / 不指外壳 / 代码卫生(eslint)
|
|
12
|
+
dsh-wb-module check --only cards # 十三条可以单跑
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
包自带的六个脚本(`build` / `typecheck` / `test` / `selfcheck` 是 `check` 的契约判据要求的那四个;`check` 跑全部判据,`lint` 只跑源码卫生那一条):
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
pnpm build # 构建产物
|
|
19
19
|
pnpm typecheck # tsc --noEmit
|
|
20
20
|
pnpm test # vitest:路由真读数 + 卡片五态
|
|
21
21
|
pnpm selfcheck # 构建 + 把 client.js 当浏览器加载器跑一遍 + 调一次 <unit>/overview 看它真读盘
|
|
22
|
+
pnpm check # dsh-wb-module check
|
|
23
|
+
pnpm lint # dsh-wb-module lint(`--fix` 就地改文件;`check` 永不动手)
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
##
|
|
26
|
+
## 需求底稿(做之前填,做完自己逐条走一遍)
|
|
25
27
|
|
|
26
|
-
三张表既是**要问清的需求**,也是**最后的验收单** ——
|
|
28
|
+
三张表既是**要问清的需求**,也是**最后的验收单** —— 表里每一格 agent 都要在真卡片上见过或点过。
|
|
27
29
|
**空着的格就是还没定的事**,别带着空格往下做:落点是同目录的 `design.json`
|
|
28
30
|
(`dsh-wb-module design check` 会把空格一格一格列出来),**下面标了 start/end 的那一段就是由它生成的**
|
|
29
31
|
—— 要改三张表就改 `design.json`,再 `dsh-wb-module design render`;手改那一段会被 next 一次 check 判红。
|
|
@@ -86,17 +88,28 @@ pnpm selfcheck # 构建 + 把 client.js 当浏览器加载器跑一遍 + 调
|
|
|
86
88
|
|
|
87
89
|
## 生成出来的形状
|
|
88
90
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
|
93
|
-
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
91
|
+
**分层多文件**,不是单文件:数字越小越底层,上层只许 import 下层的(`arch.config.mjs` 那条
|
|
92
|
+
`arch` 判据盯着,`dsh-wb-module check` 会跑)。目录**按需要出现**:这份有状态逻辑,所以有 `model/`。
|
|
93
|
+
|
|
94
|
+
| 文件 | 是什么 | 层 |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `package.json` | 自述清单:`dsh.workbench.unit` = `__UNIT__`、两个面(`src/host.ts` / `src/client.tsx`)、六个脚本、peer 下限 | — |
|
|
97
|
+
| `arch.config.mjs` | 本模块的架构方言,**只有两行**(`import { moduleArch }` + `export default moduleArch()`)—— 层序表 / 两个运行时 / 阈值只有 `@dsh-workbench/module-tools/arch` 那一份,随工具链同代 | — |
|
|
98
|
+
| `eslint.config.mjs` | 本模块的**语法卫生方言**,同样两行:`moduleLint()` —— 规则表只有 `@dsh-workbench/module-tools/lint` 那一份(`console` / `as any` / hooks 那几条 / 手搓颜色与 `!important`);要开口子就 `moduleLint({ rules })` | — |
|
|
99
|
+
| `cordis.patch.yml` | 插进 dsh 组合的补丁 | — |
|
|
100
|
+
| `design.json` | 三张表 + 状态矩阵 + 数据源记录 + 验收清单(**唯一真相**,上面那段由它生成;`dsh-wb-module design check/render/accept` 读写它) | — |
|
|
101
|
+
| `src/model/overview.ts` | 纯逻辑:类型 + 五态状态机 + `formatBytes`。两个运行时都读得到,不碰 react / kit / dsh / `node:*` | 1 |
|
|
102
|
+
| `src/model/overview.test.ts` | 模型层的配对测试(有 `src/model/**` 就必须有 —— `arch` 的 M08) | 99 |
|
|
103
|
+
| `src/messages.ts` | 卡片那几句文案(zh / en),键带 `<unit>.` 前缀 | 1 |
|
|
104
|
+
| `src/components/OverviewCard.tsx` | 卡片:`Card` 壳 + 五态(待机 / 加载 / 就绪 / 出错,其中"就绪但空"单列一态)+ 最大的几个文件 | 2 |
|
|
105
|
+
| `src/components/OverviewCard.module.css` | 这个组件自己的样式(CSS Modules,构建时类名按路径哈希)—— **样式跟组件同目录** | 2 |
|
|
106
|
+
| `src/components/OverviewCard.test.ts` | 这张卡五态各画得出来(测试跟着被测的东西住) | 99 |
|
|
107
|
+
| `src/host.ts` | 宿主面:`readOverview()`(真读目录)+ 一条只读路由 `<unit>/overview`;类型从 `./model/overview` 引 | 10 |
|
|
108
|
+
| `src/client.tsx` | 浏览器面:**只剩面** —— 注册 + 把 props 交给 `components/OverviewCard` | 10 |
|
|
109
|
+
| `src/plugin.test.ts` | 面注册 + 路由真读的断言 | 99 |
|
|
110
|
+
| `scripts/smoke.mjs` | 接线自检:卡片进没进外壳、路由真读没真读 | — |
|
|
111
|
+
| `tsconfig.json` / `vitest.config.ts` / `types/globals.d.ts` / `types/styles.d.ts` | 经典 JSX 运行时(`jsxFactory: h`)、vitest 的别名、浏览器半侧的运行时垫片、`*.module.css` 的类名表声明 | — |
|
|
112
|
+
| `README.md` | 就是本文 | — |
|
|
100
113
|
|
|
101
114
|
## 数据是在哪算出来的
|
|
102
115
|
|
|
@@ -227,8 +240,9 @@ env: {
|
|
|
227
240
|
|
|
228
241
|
## 卡片画哪五态
|
|
229
242
|
|
|
230
|
-
`src/
|
|
231
|
-
每个分支各对应一屏(`src/
|
|
243
|
+
`src/model/overview.ts` 的 `overviewStateOf({ phase, value, error })` 是一条纯函数,
|
|
244
|
+
每个分支各对应一屏(`src/model/overview.test.ts` 直接断言这条函数,
|
|
245
|
+
`src/components/OverviewCard.test.ts` 再把每一屏画一遍):
|
|
232
246
|
|
|
233
247
|
| 态 | 什么时候 | 画什么 |
|
|
234
248
|
|---|---|---|
|
|
@@ -254,13 +268,24 @@ dsh plugin --profile workbench add <本目录>
|
|
|
254
268
|
取了旧外壳没有的成员会在用户机器上首帧渲染就炸(peer 下限因此不许比 devDependency 旧)。
|
|
255
269
|
* **两个运行时不许互引**:宿主侧不许碰 DOM / react,浏览器侧不许碰 `node:` / `process.env` / `Buffer`
|
|
256
270
|
(`check:bundles` 对着产物查)。所以目录读在 `src/host.ts`,卡片只管画。
|
|
271
|
+
跨运行时那一条**层号拦不住**(`components/` 第 2 层、`host.ts` 第 10 层,"向下依赖"看着合法)——
|
|
272
|
+
是 `arch` 判据按面判的:`src/components/**` 整片算 client 面,宿主侧 import 它当场红。
|
|
273
|
+
* **层序**:`src/model/**`(纯逻辑,第 1 层)谁都能读,但它**不碰** react / kit / dsh / `node:*`;
|
|
274
|
+
视图在 `src/components/**`(第 2 层),只许 import 第 1 层;两个面在第 10 层。
|
|
275
|
+
要加一层就在 `arch.config.mjs` 里明写(`moduleArch({ layers: … })`)—— 多出来的目录默认是 S01 红。
|
|
276
|
+
* **语法卫生**(第十三条):`eslint.config.mjs` 那份方言管 `console.log` / `as any` / 手搓颜色字面量 /
|
|
277
|
+
`!important` / hooks 那几条(`rules-of-hooks`、渲染期写 ref、漏依赖是 **error**,其余 recommended 是 **warn**)。
|
|
278
|
+
`check` 把 error 算红、`warn` 只计数;要动手就先 `dsh-wb-module lint --fix`(`check` 一个字节都不改)。
|
|
279
|
+
个别地方真要破例就在 `eslint.config.mjs` 里 `moduleLint({ rules })` 开口子,别悄悄换成手写配置
|
|
280
|
+
(判据按方言指纹认它)。
|
|
257
281
|
* `@/` 只许指自己(`@/modules/__UNIT__/*`);共享代码走包名;`@deepseek-ai/dsh-*` 只在客户端侧用,
|
|
258
282
|
且必须同时写进 `package.json` 的 `dsh.client.external`。
|
|
259
283
|
* **自己的样式走 CSS Modules**(`src/x.module.css` + `import styles from './x.module.css'`):
|
|
260
284
|
类名照常写就行 —— 构建会**按文件路径哈希**(跨模块不会撞),但**别带 `wb-` 前缀**:
|
|
261
285
|
`wb-*` 是外壳的全局类名,模块定义它、或借契约里没有的那一个,`check:style` 都会判红。
|
|
262
286
|
要借外壳契约里的类,写 `:global(.wb-…)`,而且那个类必须在契约里。
|
|
263
|
-
|
|
287
|
+
这份骨架带了一份最小的 `src/components/OverviewCard.module.css`(就一档列间距),
|
|
288
|
+
演示"样式与组件同目录";`types/styles.d.ts` 是它给 `tsc` 的类名表声明(少了它 `styles.largest` 直接红)。
|
|
264
289
|
* **源码零注释**(`src/**`):要说的都写在这里。
|
|
265
290
|
|
|
266
291
|
要发布:把 `package.json` 里的 `private` 去掉,并显式写上发布目标
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"timeout": "路径不存在 / 没权限 / 不是目录 → HTTP 200 + ok:false,码 overview-unreadable",
|
|
30
30
|
"probeCommand": "node -e \"require('node:fs').readdirSync(process.cwd())\""
|
|
31
31
|
},
|
|
32
|
+
"writes": [],
|
|
32
33
|
"acceptance": [
|
|
33
34
|
{ "step": "打开面板 → 看到这张卡 → 路径 + N 文件 / M 目录 / 总字节", "checked": false },
|
|
34
35
|
{ "step": "换一个空目录 → 看到「这个目录是空的」(不是空白、也不是 0)", "checked": false },
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"typecheck": "tsc --noEmit",
|
|
15
15
|
"test": "vitest run",
|
|
16
16
|
"selfcheck": "dsh-wb-module build && dsh-wb-module design check && node scripts/smoke.mjs",
|
|
17
|
+
"check": "dsh-wb-module check",
|
|
18
|
+
"lint": "dsh-wb-module lint",
|
|
17
19
|
"design": "dsh-wb-module design check",
|
|
18
20
|
"prepack": "dsh-wb-module build"
|
|
19
21
|
},
|