@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,103 @@
|
|
|
1
|
+
// kit 成员 → 类型文件:外壳那两个包的类型树(`@dsh-workbench/shared` 的 `dist` 与外壳自己的 `dist/types`)。
|
|
2
|
+
//
|
|
3
|
+
// 为什么要扫:kit 的成员是**运行期**从外壳产物 `client.js` 里跑出来的(见 `checks/kit.mjs`),
|
|
4
|
+
// 产物里没有"这个成员来自哪个模块";而类型树里每个成员的声明都躺在某份 `.d.ts` 里 ——
|
|
5
|
+
// 扫一遍就能把"名字"落到"文件",于是 agent「想问哪个直接去看那份文件」有确切路径。
|
|
6
|
+
//
|
|
7
|
+
// 为什么是名字扫描而不是真正的类型解析:这些 `.d.ts` 是 tsc 生成的、声明形状规整
|
|
8
|
+
// (`export declare const X` / `export interface X` / `export { X } from '…'`),而 kit 的成员
|
|
9
|
+
// 本来就是运行期对象上的键名。扫描不引 TypeScript 依赖、贵在稳;代价是"同名声明出现在多处"时
|
|
10
|
+
// 要挑一个(挑法见 `preferred`),以及极少数成员指到的是 barrel 而不是最终实现文件。
|
|
11
|
+
// 实测本机 190 个 kit 成员 190 个都落得到文件(见 `.scratch/dsh-primitives-face/verify.md` §2)。
|
|
12
|
+
|
|
13
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
const PATTERNS = [
|
|
18
|
+
/^export declare (?:const|function|class) (\w+)/gm,
|
|
19
|
+
/^export (?:interface|type) (\w+)/gm,
|
|
20
|
+
/^export \{([^}]*)\}/gm,
|
|
21
|
+
/^declare (?:const|function|class) (\w+)/gm,
|
|
22
|
+
/^(?:interface|type) (\w+)/gm,
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function walk(dir, acc = []) {
|
|
26
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
27
|
+
const full = join(dir, entry.name);
|
|
28
|
+
if (entry.isDirectory()) walk(full, acc);
|
|
29
|
+
else if (full.endsWith('.d.ts')) acc.push(full);
|
|
30
|
+
}
|
|
31
|
+
return acc;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function namesIn(clause) {
|
|
35
|
+
if (!clause.includes(',') && !clause.includes(' ')) return [clause];
|
|
36
|
+
return clause
|
|
37
|
+
.split(',')
|
|
38
|
+
.map((raw) => raw.trim().split(/\s+as\s+/).pop().trim())
|
|
39
|
+
.filter((name) => /^[A-Za-z_$][\w$]*$/.test(name));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 外壳那两个包的类型树(`shared` 没装就只给外壳那棵)。 */
|
|
43
|
+
export function shellTypeRoots(shellClientPath) {
|
|
44
|
+
const shellRoot = dirname(shellClientPath);
|
|
45
|
+
const roots = {};
|
|
46
|
+
const shellTypes = join(shellRoot, 'dist', 'types');
|
|
47
|
+
if (existsSync(shellTypes)) roots.shell = shellTypes;
|
|
48
|
+
// 打印用"从 node_modules 看"的那条路径(pnpm 的真身在 `.pnpm/` 哈希目录里);读文件两条都行。
|
|
49
|
+
const viaNodeModules = join(shellRoot, 'node_modules', '@dsh-workbench', 'shared', 'dist');
|
|
50
|
+
try {
|
|
51
|
+
// 解析 "." 那个入口(= `dist/index.js`)再取 dirname —— shared 的 `exports` 里**没有**
|
|
52
|
+
// `./package.json` 这一条,直接 resolve 它的 package.json 会 ERR_PACKAGE_PATH_NOT_EXPORTED。
|
|
53
|
+
const sharedEntry = createRequire(join(shellRoot, 'package.json')).resolve('@dsh-workbench/shared');
|
|
54
|
+
const sharedTypes = dirname(sharedEntry);
|
|
55
|
+
if (existsSync(sharedTypes)) roots.shared = existsSync(viaNodeModules) ? viaNodeModules : sharedTypes;
|
|
56
|
+
} catch {
|
|
57
|
+
// 没装 shared:只有外壳自己那棵,仍然能落一部分成员
|
|
58
|
+
}
|
|
59
|
+
return { shellRoot, roots };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 同名多处时挑哪个:组件 > hooks > 库 > 其他,同档挑路径短的。 */
|
|
63
|
+
function preferred(candidates) {
|
|
64
|
+
const rank = (file) =>
|
|
65
|
+
file.includes('/components/') ? 0 : file.includes('/hooks/') ? 1 : file.includes('/lib/') ? 2 : 3;
|
|
66
|
+
return [...candidates].sort((a, b) => rank(a.file) - rank(b.file) || a.file.length - b.file.length)[0];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 把 kit 的成员名落到类型文件。
|
|
71
|
+
* 返回 `{ roots, files: Map<名字, 绝对路径>, missing: 名字[] }` —— `roots` 是两棵类型树的绝对根。
|
|
72
|
+
*/
|
|
73
|
+
export function memberFilesOf(shellClientPath, names) {
|
|
74
|
+
const { roots } = shellTypeRoots(shellClientPath);
|
|
75
|
+
const index = new Map();
|
|
76
|
+
for (const root of Object.values(roots)) {
|
|
77
|
+
for (const file of walk(root)) {
|
|
78
|
+
let text;
|
|
79
|
+
try {
|
|
80
|
+
text = readFileSync(file, 'utf8');
|
|
81
|
+
} catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
for (const pattern of PATTERNS) {
|
|
85
|
+
for (const match of text.matchAll(pattern)) {
|
|
86
|
+
for (const name of namesIn(match[1])) {
|
|
87
|
+
if (!index.has(name)) index.set(name, []);
|
|
88
|
+
const found = index.get(name);
|
|
89
|
+
if (!found.some((one) => one.file === file)) found.push({ file });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const files = new Map();
|
|
96
|
+
const missing = [];
|
|
97
|
+
for (const name of names) {
|
|
98
|
+
const candidates = index.get(name);
|
|
99
|
+
if (candidates === undefined || candidates.length === 0) missing.push(name);
|
|
100
|
+
else files.set(name, preferred(candidates).file);
|
|
101
|
+
}
|
|
102
|
+
return { roots, files, missing };
|
|
103
|
+
}
|
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
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"typecheck": "tsc --noEmit",
|
|
15
15
|
"test": "vitest run",
|
|
16
16
|
"selfcheck": "dsh-wb-module build && node scripts/smoke.mjs",
|
|
17
|
+
"check": "dsh-wb-module check",
|
|
18
|
+
"lint": "dsh-wb-module lint",
|
|
17
19
|
"prepack": "dsh-wb-module build"
|
|
18
20
|
},
|
|
19
21
|
"dsh": {
|
|
@@ -21,7 +23,7 @@
|
|
|
21
23
|
"client": {
|
|
22
24
|
"platform": "web",
|
|
23
25
|
"inject": ["@dsh-workbench/core"],
|
|
24
|
-
"external": ["@dsh-workbench/core/client"]
|
|
26
|
+
"external": ["@dsh-workbench/core/client", "@deepseek-ai/dsh-client-ui-primitives"]
|
|
25
27
|
},
|
|
26
28
|
"workbench": {
|
|
27
29
|
"apiVersion": 1,
|
|
@@ -35,6 +37,7 @@
|
|
|
35
37
|
"peerDependencies": { "@dsh-workbench/core": "__SHELL_RANGE__", "react": "*" },
|
|
36
38
|
"peerDependenciesMeta": { "react": { "optional": true } },
|
|
37
39
|
"devDependencies": {
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-primitives": "__DSH_UI_VERSION__",
|
|
38
41
|
"@dsh-workbench/core": "__SHELL_RANGE__",
|
|
39
42
|
"@dsh-workbench/module-tools": "__SHELL_RANGE__",
|
|
40
43
|
"@types/node": "^24.19.0",
|
|
@@ -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
|
},
|
|
@@ -22,7 +24,7 @@
|
|
|
22
24
|
"client": {
|
|
23
25
|
"platform": "web",
|
|
24
26
|
"inject": ["@dsh-workbench/core"],
|
|
25
|
-
"external": ["@dsh-workbench/core/client"]
|
|
27
|
+
"external": ["@dsh-workbench/core/client", "@deepseek-ai/dsh-client-ui-primitives"]
|
|
26
28
|
},
|
|
27
29
|
"workbench": {
|
|
28
30
|
"apiVersion": 1,
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
"peerDependencies": { "@dsh-workbench/core": "__SHELL_RANGE__", "react": "*" },
|
|
37
39
|
"peerDependenciesMeta": { "react": { "optional": true } },
|
|
38
40
|
"devDependencies": {
|
|
41
|
+
"@deepseek-ai/dsh-client-ui-primitives": "__DSH_UI_VERSION__",
|
|
39
42
|
"@dsh-workbench/core": "__SHELL_RANGE__",
|
|
40
43
|
"@dsh-workbench/module-tools": "__SHELL_RANGE__",
|
|
41
44
|
"@types/node": "^24.19.0",
|
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import { OverviewCard } from './components/OverviewCard';
|
|
3
3
|
import { messages } from './messages';
|
|
4
4
|
|
|
5
5
|
const UNIT_ID = '__UNIT__';
|
|
6
|
-
const OVERVIEW_ROUTE = `${UNIT_ID}/overview`;
|
|
7
|
-
const LIST_LIMIT = 5;
|
|
8
|
-
const KB = 1024;
|
|
9
6
|
|
|
10
|
-
const { Card, CenteredState, EmptyState, Row, Rows, StateBlock, IconPanelScreen } = kit.components;
|
|
11
|
-
const { useApiResource } = kit.hooks;
|
|
12
|
-
|
|
13
|
-
type Rendered = ReturnType<typeof h>;
|
|
14
7
|
type Translate = (key: string, params?: Record<string, unknown>) => string;
|
|
15
8
|
|
|
16
9
|
interface WorkspaceView {
|
|
@@ -27,7 +20,7 @@ interface BoardProps {
|
|
|
27
20
|
interface UnitClientFace {
|
|
28
21
|
readonly unit: string;
|
|
29
22
|
readonly messages: { readonly zh: Readonly<Record<string, string>>; readonly en: Readonly<Record<string, string>> };
|
|
30
|
-
readonly card: (props: BoardProps) =>
|
|
23
|
+
readonly card: (props: BoardProps) => ReactElement;
|
|
31
24
|
}
|
|
32
25
|
|
|
33
26
|
interface UnitClientApi {
|
|
@@ -39,148 +32,16 @@ interface ClientContext {
|
|
|
39
32
|
effect(effect: () => void | (() => void), label?: string): () => void;
|
|
40
33
|
}
|
|
41
34
|
|
|
42
|
-
export interface OverviewFile {
|
|
43
|
-
readonly name: string;
|
|
44
|
-
readonly bytes: number;
|
|
45
|
-
readonly mtime: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface OverviewPayload {
|
|
49
|
-
readonly path: string;
|
|
50
|
-
readonly files: number;
|
|
51
|
-
readonly dirs: number;
|
|
52
|
-
readonly bytes: number;
|
|
53
|
-
readonly largest: readonly OverviewFile[];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
type OverviewReady = { readonly ok: true } & OverviewPayload;
|
|
57
|
-
|
|
58
|
-
interface Failure {
|
|
59
|
-
readonly code: string;
|
|
60
|
-
readonly message: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export type OverviewState =
|
|
64
|
-
| { readonly kind: 'idle' }
|
|
65
|
-
| { readonly kind: 'loading' }
|
|
66
|
-
| { readonly kind: 'ready'; readonly payload: OverviewPayload }
|
|
67
|
-
| { readonly kind: 'empty'; readonly payload: OverviewPayload }
|
|
68
|
-
| { readonly kind: 'error'; readonly detail: string };
|
|
69
|
-
|
|
70
|
-
export interface OverviewInput {
|
|
71
|
-
readonly phase: 'idle' | 'loading' | 'ready' | 'error';
|
|
72
|
-
readonly value: OverviewReady | undefined;
|
|
73
|
-
readonly error: Failure | undefined;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function formatBytes(bytes: number): string {
|
|
77
|
-
if (bytes < KB) return `${String(bytes)} B`;
|
|
78
|
-
if (bytes < KB * KB) return `${(bytes / KB).toFixed(1)} KB`;
|
|
79
|
-
if (bytes < KB * KB * KB) return `${(bytes / (KB * KB)).toFixed(1)} MB`;
|
|
80
|
-
return `${(bytes / (KB * KB * KB)).toFixed(1)} GB`;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function payloadState(payload: OverviewPayload): OverviewState {
|
|
84
|
-
const blank = payload.files === 0 && payload.dirs === 0;
|
|
85
|
-
return blank ? { kind: 'empty', payload } : { kind: 'ready', payload };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function overviewStateOf(input: OverviewInput): OverviewState {
|
|
89
|
-
if (input.phase === 'idle') return { kind: 'idle' };
|
|
90
|
-
if (input.phase === 'error') {
|
|
91
|
-
const error = input.error;
|
|
92
|
-
return { kind: 'error', detail: error === undefined ? '' : `${error.code}: ${error.message}` };
|
|
93
|
-
}
|
|
94
|
-
if (input.value !== undefined && (input.phase === 'ready' || input.phase === 'loading')) {
|
|
95
|
-
return payloadState(input.value);
|
|
96
|
-
}
|
|
97
|
-
return { kind: 'loading' };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function bodyOf(state: OverviewState, t: Translate): Rendered {
|
|
101
|
-
if (state.kind === 'idle') {
|
|
102
|
-
return h(CenteredState, {
|
|
103
|
-
icon: null,
|
|
104
|
-
title: t(`${UNIT_ID}.needProject`),
|
|
105
|
-
compact: true,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
if (state.kind === 'loading') {
|
|
109
|
-
return h(CenteredState, {
|
|
110
|
-
icon: null,
|
|
111
|
-
title: t(`${UNIT_ID}.loading`),
|
|
112
|
-
hint: t(`${UNIT_ID}.loadingHint`),
|
|
113
|
-
compact: true,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
if (state.kind === 'error') {
|
|
117
|
-
return h(CenteredState, {
|
|
118
|
-
icon: null,
|
|
119
|
-
title: t(`${UNIT_ID}.errorTitle`),
|
|
120
|
-
detail: state.detail,
|
|
121
|
-
hint: t(`${UNIT_ID}.errorHint`),
|
|
122
|
-
compact: true,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
if (state.kind === 'empty') {
|
|
126
|
-
return h(EmptyState, {
|
|
127
|
-
text: t(`${UNIT_ID}.empty`),
|
|
128
|
-
icon: h(IconPanelScreen, { size: 20 }),
|
|
129
|
-
compact: true,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
const payload = state.payload;
|
|
133
|
-
return h(
|
|
134
|
-
StateBlock,
|
|
135
|
-
{
|
|
136
|
-
title: payload.path,
|
|
137
|
-
hint:
|
|
138
|
-
`${String(payload.files)}${t(`${UNIT_ID}.fileUnit`)} · ` +
|
|
139
|
-
`${String(payload.dirs)}${t(`${UNIT_ID}.dirUnit`)} · ${formatBytes(payload.bytes)}`,
|
|
140
|
-
resolving: t(`${UNIT_ID}.largest`),
|
|
141
|
-
},
|
|
142
|
-
h(
|
|
143
|
-
Rows,
|
|
144
|
-
null,
|
|
145
|
-
...payload.largest.map((file) =>
|
|
146
|
-
h(Row, {
|
|
147
|
-
key: file.name,
|
|
148
|
-
label: file.name,
|
|
149
|
-
value: `${formatBytes(file.bytes)} · ${file.mtime}`,
|
|
150
|
-
}),
|
|
151
|
-
),
|
|
152
|
-
),
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
35
|
const face: UnitClientFace = {
|
|
157
36
|
unit: UNIT_ID,
|
|
158
37
|
messages,
|
|
159
|
-
card: (props) =>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
{ enabled: path !== '' },
|
|
167
|
-
);
|
|
168
|
-
const state = overviewStateOf({
|
|
169
|
-
phase: resource.phase,
|
|
170
|
-
value: resource.value,
|
|
171
|
-
error: resource.error,
|
|
172
|
-
});
|
|
173
|
-
return h(
|
|
174
|
-
Card,
|
|
175
|
-
{
|
|
176
|
-
t: props.t,
|
|
177
|
-
title: props.t(`${UNIT_ID}.cardTitle`),
|
|
178
|
-
refreshing: resource.phase === 'loading',
|
|
179
|
-
onRefresh: props.onRefresh,
|
|
180
|
-
},
|
|
181
|
-
bodyOf(state, props.t),
|
|
182
|
-
);
|
|
183
|
-
},
|
|
38
|
+
card: (props) =>
|
|
39
|
+
OverviewCard({
|
|
40
|
+
path: props.world.current.workspace?.path ?? '',
|
|
41
|
+
refreshToken: props.refreshToken,
|
|
42
|
+
onRefresh: props.onRefresh,
|
|
43
|
+
t: props.t,
|
|
44
|
+
}),
|
|
184
45
|
};
|
|
185
46
|
|
|
186
47
|
export const inject = ['workbenchUnits'];
|