@dsh-workbench/module-tools 0.3.11 → 1.0.0-beta.1
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/LICENSE +21 -0
- package/README.md +223 -177
- package/bin/dsh-wb-module.mjs +89 -34
- package/package.json +22 -3
- package/src/arch.mjs +158 -129
- package/src/build.mjs +6 -30
- package/src/changelog.mjs +166 -0
- package/src/check-flags.mjs +0 -7
- package/src/check.mjs +115 -53
- package/src/checks/alias.mjs +0 -9
- package/src/checks/arch.mjs +3 -25
- package/src/checks/bundles.mjs +2 -34
- package/src/checks/cards.mjs +26 -30
- package/src/checks/contracts.mjs +72 -219
- package/src/checks/kit.mjs +2 -42
- package/src/checks/lint.mjs +9 -82
- package/src/checks/markers.mjs +48 -17
- package/src/checks/primitives.mjs +0 -39
- package/src/checks/protocol.mjs +0 -18
- package/src/checks/source.mjs +103 -51
- package/src/checks/standalone.mjs +4 -14
- package/src/checks/style.mjs +84 -230
- package/src/css-injection.mjs +0 -11
- package/src/css-shell.mjs +3 -31
- package/src/datasource-file.mjs +104 -0
- package/src/declared-tools.mjs +0 -24
- package/src/design.mjs +116 -38
- package/src/discover.mjs +2 -10
- package/src/font-scale.mjs +1 -61
- package/src/init.mjs +2 -17
- package/src/lint.mjs +24 -144
- package/src/market-index.mjs +569 -0
- package/src/market-init.mjs +80 -0
- package/src/primitives-surface.mjs +2 -28
- package/src/publish.mjs +11 -25
- package/src/release-changelog.mjs +29 -0
- package/src/release-journal.mjs +131 -0
- package/src/release-report.mjs +99 -0
- package/src/release-util.mjs +87 -0
- package/src/release.mjs +1250 -0
- package/src/seed.mjs +0 -25
- package/src/shell-type-files.mjs +3 -30
- package/src/sync.mjs +41 -24
- package/template/AGENTS.md.tpl +33 -0
- package/template/CHANGELOG.md.tpl +7 -0
- package/template/README.md.tpl +6 -33
- package/template/package.json.tpl +2 -2
- package/template/src/client.tsx.tpl +1 -18
- package/template/src/host.ts.tpl +2 -15
- package/template/types/globals.d.ts +1 -14
- package/template-diy/AGENTS.md.tpl +33 -0
- package/template-diy/CHANGELOG.md.tpl +7 -0
- package/template-diy/README.md.tpl +4 -133
- package/template-diy/package.json.tpl +2 -2
- package/template-diy/src/client.tsx.tpl +1 -29
- package/template-diy/src/host.ts.tpl +1 -15
- package/template-diy/src/plugin.test.ts.tpl +1 -1
- package/template-diy/types/globals.d.ts +1 -14
- package/template-diy/types/styles.d.ts +0 -10
package/bin/dsh-wb-module.mjs
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
|
+
import { Command } from 'commander';
|
|
4
5
|
import { build } from '../src/build.mjs';
|
|
5
6
|
import { CHECKS, check } from '../src/check.mjs';
|
|
7
|
+
import { changelog } from '../src/changelog.mjs';
|
|
6
8
|
import { design } from '../src/design.mjs';
|
|
7
9
|
import { init } from '../src/init.mjs';
|
|
10
|
+
import { marketInit } from '../src/market-init.mjs';
|
|
8
11
|
import { publish } from '../src/publish.mjs';
|
|
12
|
+
import { release } from '../src/release.mjs';
|
|
9
13
|
import { sync } from '../src/sync.mjs';
|
|
10
14
|
|
|
11
|
-
// `dsh-wb-module lint [--fix]`:跑模块**自己那份** `eslint.config.mjs`(方言住在 `src/lint.mjs`)。
|
|
12
|
-
// ESLint 的构造只有一处 —— `src/checks/lint.mjs` 的 `lintPackage`,判据与这条命令共用它;
|
|
13
|
-
// 这里只管打印与退出码(与 `check` 那条路同一分工)。
|
|
14
|
-
//
|
|
15
|
-
// 为什么 eslint 是**懒 import**:`build` / `init` 是工坊里的热路径,而 eslint 那一坨加载要几百毫秒 ——
|
|
16
|
-
// 静态 import 会让每条命令都替这条 lint 付一次。写成命令里的动态 import,只有真跑 lint 才付。
|
|
17
15
|
async function lint({ root, args }) {
|
|
18
16
|
const [{ CONFIG_FILE, lintPackage }, { discoverModulePackages }] = await Promise.all([
|
|
19
17
|
import('../src/checks/lint.mjs'),
|
|
@@ -47,39 +45,96 @@ async function lint({ root, args }) {
|
|
|
47
45
|
if (failed > 0) process.exit(1);
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
const
|
|
51
|
-
const cwd = process.env.DSH_WB_MODULE_CWD ?? process.cwd();
|
|
48
|
+
const commands = { build, check, init, 'market-init': marketInit, design, publish, release, sync, lint, changelog };
|
|
52
49
|
|
|
53
|
-
const
|
|
50
|
+
const CHECK_HELP = `
|
|
51
|
+
查询口:
|
|
52
|
+
check --only kit [--file <名字>] 这份外壳的 kit 里有什么
|
|
53
|
+
check --only primitives [--all|--files|--json] [--file <名字>]
|
|
54
|
+
这份外壳的 dsh 原语里有什么
|
|
55
|
+
check --list [--json|--markdown] 只列判据元数据`;
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
57
|
+
const RELEASE_HELP = `
|
|
58
|
+
选项:
|
|
59
|
+
--target team|public|tarball|git|market|local 出口;省略按配置默认(可写索引优先 market)
|
|
60
|
+
--version patch|minor|major|x.y.z 版本;省略时已存在就自动 patch
|
|
61
|
+
--registry <url> --access restricted|public --npmrc <文件>
|
|
62
|
+
--license <id> --out <目录> --git <remote>
|
|
63
|
+
--index auto|none|<目录|git 地址> 发布后要更新的索引源
|
|
64
|
+
--index-ref <分支> --index-path <索引路径> --index-policy artifacts-in-repo|catalog-only
|
|
65
|
+
--resume 上次中断后接着发(用记录里的出口与版本)
|
|
66
|
+
--dry-run --verify --yes --json`;
|
|
67
|
+
|
|
68
|
+
const MARKET_INIT_HELP = `
|
|
69
|
+
用法:
|
|
70
|
+
market-init <git 地址|目录> [--ref <分支>] [--path <索引路径>] [--no-readme] [--json]
|
|
71
|
+
|
|
72
|
+
空仓库:用仓库自己声明的默认分支,读不到时用 --ref 指定(github.com 兜底 main)。
|
|
73
|
+
已有 commit 的仓库:只补 market.json / README.md,不改已有文件。`;
|
|
68
74
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
const CHANGELOG_HELP = `
|
|
76
|
+
用法:
|
|
77
|
+
changelog init
|
|
78
|
+
changelog add [版本] [--type added|changed|fixed|removed|security|deprecated] [--note <文字>] [--date <日期>]
|
|
79
|
+
changelog check [版本] [--json]
|
|
80
|
+
|
|
81
|
+
版本省略时用 package.json 里的 version。发布时这一版必须在 CHANGELOG.md 里有记录,否则 release 会拦下。`;
|
|
82
|
+
|
|
83
|
+
const COMMANDS = {
|
|
84
|
+
build: '出 dist/host.js(单文件 ESM)与 client.js(套 loader 壳)',
|
|
85
|
+
check: '十三条判据;--only 单跑,也当查询口用;--list 只列判据元数据',
|
|
86
|
+
init: '建模块骨架(--dir / --template minimal|diy / --description)',
|
|
87
|
+
'market-init': '初始化市场索引仓库(写 market.json + README,推首个 commit)',
|
|
88
|
+
design: '设计底稿:init 填 / check 查空格 / render 生成 README / accept 打勾',
|
|
89
|
+
publish: '发货前清单:门禁 + files + npm pack(只 dry run)',
|
|
90
|
+
release: '真发布:全门禁 + 验收闸 + 预检接管 + 发到出口 + 发后核包 + 更新市场索引(中断可 --resume)',
|
|
91
|
+
sync: '按清单生成 src/declared-tools.ts,按 design.json 生成 src/datasource.ts',
|
|
92
|
+
lint: '用模块自己那份 eslint.config.mjs;--fix 写回文件',
|
|
93
|
+
changelog: '更新日志:init 建 / add 记一版 / check 查这一版有没有(发布前必须有)',
|
|
72
94
|
};
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
|
|
96
|
+
function flagValue(raw, flag) {
|
|
97
|
+
const at = raw.indexOf(flag);
|
|
98
|
+
return at === -1 ? undefined : raw[at + 1];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function dispatch(name, raw) {
|
|
102
|
+
const cwd = process.env.DSH_WB_MODULE_CWD ?? process.cwd();
|
|
103
|
+
const args = [];
|
|
104
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
105
|
+
if (raw[index] === '--cwd' || raw[index] === '--only') {
|
|
106
|
+
index += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
args.push(raw[index]);
|
|
78
110
|
}
|
|
79
|
-
|
|
111
|
+
const root = resolve(flagValue(raw, '--cwd') ?? cwd);
|
|
112
|
+
if (root !== process.cwd()) process.chdir(root);
|
|
113
|
+
await commands[name]({ root, only: flagValue(raw, '--only'), args });
|
|
80
114
|
}
|
|
81
115
|
|
|
82
|
-
const
|
|
83
|
-
|
|
116
|
+
const program = new Command('dsh-wb-module')
|
|
117
|
+
.description(`模块包的工具链:${Object.keys(COMMANDS).join(' / ')}`)
|
|
118
|
+
.helpOption('-h, --help', '打印帮助')
|
|
119
|
+
.addHelpText(
|
|
120
|
+
'after',
|
|
121
|
+
'\n --cwd <目录> 指定包目录(默认当前目录;也可用 DSH_WB_MODULE_CWD)\n 任何命令后跟 --help 都只打印帮助。\n',
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
for (const [name, description] of Object.entries(COMMANDS)) {
|
|
125
|
+
const sub = program
|
|
126
|
+
.command(name)
|
|
127
|
+
.description(description)
|
|
128
|
+
.helpOption('-h, --help', '打印帮助')
|
|
129
|
+
.allowUnknownOption(true)
|
|
130
|
+
.allowExcessArguments(true);
|
|
131
|
+
if (name === 'check') sub.addHelpText('after', `\n${CHECK_HELP}\n --only 可选:${Object.keys(CHECKS).join('|')}\n`);
|
|
132
|
+
if (name === 'release') sub.addHelpText('after', `${RELEASE_HELP}\n`);
|
|
133
|
+
if (name === 'market-init') sub.addHelpText('after', `${MARKET_INIT_HELP}\n`);
|
|
134
|
+
if (name === 'changelog') sub.addHelpText('after', `${CHANGELOG_HELP}\n`);
|
|
135
|
+
sub.action(async (_options, command) => {
|
|
136
|
+
await dispatch(name, command.args);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
84
139
|
|
|
85
|
-
await
|
|
140
|
+
await program.parseAsync(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-workbench/module-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
4
|
"description": "模块包的构建与门禁工具:build(单文件 ESM 宿主侧 + 套 loader 壳的浏览器侧)、check(契约/产物边界/源码边界/卡片渲染)。模块包不再各带一份构建脚本。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -22,16 +22,35 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@arch-guard/core": "^0.11.
|
|
25
|
+
"@arch-guard/core": "^0.11.1",
|
|
26
26
|
"@eslint/js": "^10.0.1",
|
|
27
|
+
"commander": "^15.0.0",
|
|
27
28
|
"esbuild": "^0.28.2",
|
|
28
29
|
"eslint": "^10.11.0",
|
|
29
30
|
"eslint-plugin-react": "^7.37.5",
|
|
30
31
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
32
|
+
"execa": "^10.2.0",
|
|
33
|
+
"semver": "^7.8.5",
|
|
34
|
+
"typescript": "5.9.3",
|
|
31
35
|
"typescript-eslint": "^8.71.0",
|
|
32
|
-
"@dsh-workbench/shared": "0.
|
|
36
|
+
"@dsh-workbench/shared": "1.0.0-beta.1"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/novaeth/dsh-workbench.git",
|
|
41
|
+
"directory": "packages/module-tools"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/novaeth/dsh-workbench#readme",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/novaeth/dsh-workbench/issues"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/semver": "^7.8.0"
|
|
33
49
|
},
|
|
34
50
|
"scripts": {
|
|
51
|
+
"lint": "eslint src bin scripts",
|
|
52
|
+
"arch": "arch-guard",
|
|
53
|
+
"check": "pnpm run lint && pnpm run arch && pnpm run selfcheck",
|
|
35
54
|
"selfcheck": "node scripts/selfcheck/index.mjs"
|
|
36
55
|
}
|
|
37
56
|
}
|
package/src/arch.mjs
CHANGED
|
@@ -1,42 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// 为什么要有这一份:模块包是"用户自己写"的代码,而模板给的是一个**单文件**起步的形状 ——
|
|
4
|
-
// 卡片一复杂(多路取数、多态渲染、几个子件、几条文案)那个文件就会长到几百行,而门禁
|
|
5
|
-
// (`check` 原本那十一条)只看产物边界与声明形状,**没有一条在看代码结构**。arch-guard 正好补这一层,
|
|
6
|
-
// 但它认识的是"应用 / 库 / FSD"三种范式,模块包(一个插件包、两个面的小库)不在其中:
|
|
7
|
-
// 直接把库范式套上去,`src/messages.ts` 这种根文件会报 S01「无处安放」,而两个运行时的
|
|
8
|
-
// 互相 import 它也不管。所以这里把模块包的方言**写死一处**:层序表、两个运行时、阈值、
|
|
9
|
-
// 允许的依赖、样式与文案的口径。
|
|
10
|
-
//
|
|
11
|
-
// 为什么不是让每个模板抄一份 arch.config.mjs:那样 ① 每个模块的方言会各自漂,
|
|
12
|
-
// ② 方言跟着**工具链**走才对 —— 工坊那三个包本来就"与外壳同代"(seed-workshop.sh 的
|
|
13
|
-
// 代际自愈),把方言放在 module-tools 里,它随代际一起更新,用户不用重抄一份配置。
|
|
14
|
-
// 模板里那份 `arch.config.mjs` 只留一行 `moduleArch()`,要改(加目录、加依赖、放宽阈值)
|
|
15
|
-
// 就在**这一处**改。
|
|
16
|
-
//
|
|
17
|
-
// 层序(数字越小越底层,S21:只许依赖层号 ≤ 自己的文件):
|
|
18
|
-
//
|
|
19
|
-
// src/model/** 1 纯逻辑:类型 / 换算 / 状态机。两个运行时都可达,不碰 react / kit / node:fs
|
|
20
|
-
// src/messages.ts 1 文案(zh / en)。同样是"谁都能用的数据"
|
|
21
|
-
// src/declared-tools.ts 1 `dsh-wb-module sync` 生成的工具名(工具链自己的产物,见下)
|
|
22
|
-
// src/components/** 2 浏览器侧的视图件;样式与它同目录(`*.module.css`)
|
|
23
|
-
// src/host.ts 10 宿主面(`dsh.workbenchFaces.host`)
|
|
24
|
-
// src/client.tsx 10 浏览器面(`dsh.workbenchFaces.client`)
|
|
25
|
-
// **/*.test.ts(x) 99 测试(库范式自带这条排他角色,S21 直接跳过;S16 的体积阈值也不管它)
|
|
26
|
-
// src/**/__tests__/** 99 测试的另一个家(方言自己声明的,见 TESTS_DIR_ROLE 那段)
|
|
27
|
-
//
|
|
28
|
-
// 两个运行时**互相隔离**:`structure.runtimes` + `faces` 让 S23 认得出"宿主侧文件 import 了
|
|
29
|
-
// 浏览器面"(反过来也一样)。这一条以前没有任何判据管 —— `check:source` 只管 dsh 的包在不在
|
|
30
|
-
// 客户端侧可达。
|
|
31
|
-
//
|
|
32
|
-
// 阈值刻意**严于** arch-guard 的默认(500 行 / 150 行函数):模块包是"一个人写的一块面板",
|
|
33
|
-
// 一个文件 200 行、一个函数 80 行就该拆了 —— 这条不是审美,是"复杂模块不会退化成一个巨文件"
|
|
34
|
-
// 的可判定下限。要更宽就在自己那份配置里传 `fileLines`。
|
|
35
|
-
//
|
|
36
|
-
// 三条内置豁免条目 / 两条规则(都要写 reason,arch-guard 的硬要求):D04 的 `--dsw-*` 令牌
|
|
37
|
-
// (我们**不拥有**主题,见 STYLE_EXCEPTION 那段)与 S16 的测试文件(脚手架不是产品代码,
|
|
38
|
-
// 见 TEST_LINES_EXCEPTIONS 那段 —— 后缀式与 `__tests__/**` 各一条)。
|
|
39
|
-
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
40
2
|
import {
|
|
41
3
|
cssModulesKit,
|
|
42
4
|
deps,
|
|
@@ -48,41 +10,21 @@ import {
|
|
|
48
10
|
noneI18nKit,
|
|
49
11
|
styles,
|
|
50
12
|
} from '@arch-guard/core/presets';
|
|
13
|
+
import { createRule } from '@arch-guard/core';
|
|
51
14
|
import { DECLARED_TOOLS_FILE } from './declared-tools.mjs';
|
|
52
15
|
import { SEED_SPECIFIERS } from './seed.mjs';
|
|
53
16
|
|
|
54
|
-
// 两个面**就是** `dsh.workbenchFaces` 指的那两个文件(协议要求,见 ADR-0038)。改这两个名字
|
|
55
|
-
// 等于改声明,判据(contracts / source)先红 —— 所以这里是常量,不是选项。
|
|
56
17
|
const HOST_FACE = 'src/host.ts';
|
|
57
18
|
const CLIENT_FACE = 'src/client.tsx';
|
|
58
19
|
|
|
59
|
-
// 内部目录表(`library()` 的 modules)。目录名是约定:模型 / 视图,别的一个都不许有 ——
|
|
60
|
-
// 多出来的目录会被 S01 报"不在目录契约内",那正是要的:要加一层就明写在配置里。
|
|
61
20
|
const LAYERS = { model: 1, components: 2 };
|
|
62
21
|
|
|
63
|
-
// `src/messages.ts` 是文件不是目录,而 `modules` 只认目录前缀(文件级 key 实测仍报 S01),
|
|
64
|
-
// 所以它走 `addRoles`。层号与 model 同层:文案不依赖任何东西,两端都能读。
|
|
65
22
|
const MESSAGES_ROLE = { id: 'lib:messages', pattern: 'src/messages.ts', layer: 1 };
|
|
66
23
|
|
|
67
|
-
// `src/declared-tools.ts` 是 `dsh-wb-module sync` 按清单生成的工具名(`tools[].key` 那一档,
|
|
68
|
-
// 见 declared-tools.mjs)—— 与 messages.ts 一样是"谁都能读的常量",所以同层、同样走 `addRoles`
|
|
69
|
-
// (它也是文件不是目录)。这一条必须内置:那是**工具链自己的产物**,不认它等于"照我们推荐的路走
|
|
70
|
-
// 就会撞 S01"(实测:作者得翻源码才知道 roles 的形状,再手写一行补上)。
|
|
71
|
-
// 路径直接取 declared-tools 那份常量,不在这里抄一遍字符串(工具名只有一处写者,这条也一样)。
|
|
72
24
|
const DECLARED_TOOLS_ROLE = { id: 'lib:declared-tools', pattern: DECLARED_TOOLS_FILE, layer: 1 };
|
|
73
25
|
|
|
74
26
|
const DEFAULT_THRESHOLDS = { fileLines: 200, viewLines: 200, functionLines: 80 };
|
|
75
27
|
|
|
76
|
-
// 测试文件不吃 S16 的体积阈值(类级豁免:arch-guard 的 exceptions 只认"某条规则对某类文件不适用")。
|
|
77
|
-
// 它与产品代码不是同一种东西 —— 夹具、逐条断言、一条用例十来行,200 行对它是错的口径;而"拆成两个
|
|
78
|
-
// 测试文件"只会把脚手架摞进 src/(作者反馈:一个模块被这条逼出 `src/host/testing.ts`,测试脚手架住进
|
|
79
|
-
// 了宿主层目录)。它本来就有一条排他角色(layer 99,S21 直接跳过),这条豁免与那条同一个理由。
|
|
80
|
-
// 为什么不给"测试单列一个阈值":S16 只读 thresholds 的 fileLines / viewLines / functionLines
|
|
81
|
-
// (arch-guard structure.js),要按角色分档得改上游;先用类级豁免把口径说清楚。
|
|
82
|
-
//
|
|
83
|
-
// 两条覆盖面合起来 = 方言对"什么算测试"的**全部**声明,一份口径:后缀式的 `.test.` / `.spec.`
|
|
84
|
-
// (库范式那条排他角色收的七种扩展,这里一种不落)与 `__tests__/**` 这个家(`metrics.testGlobs`
|
|
85
|
-
// 里写着的那一条)。少于这个并集就会冒出第二份口径:同一个方言里"什么算测试"有两套答案。
|
|
86
28
|
const TEST_LINES_REASON =
|
|
87
29
|
'测试文件是脚手架,不是产品代码:夹具、逐条断言、一条用例十来行,200 行这个阈值对它是错的口径;' +
|
|
88
30
|
'按它拆文件只会把测试脚手架摞进 src/ 的层里(作者反馈:一个模块被逼出过 src/host/testing.ts)。' +
|
|
@@ -91,16 +33,10 @@ const TEST_LINES_REASON =
|
|
|
91
33
|
|
|
92
34
|
const TEST_LINES_EXCEPTIONS = [
|
|
93
35
|
{ rule: 'S16', glob: '**/*.{test,spec}.{ts,tsx,mts,cts,js,mjs,cjs}', reason: TEST_LINES_REASON },
|
|
94
|
-
|
|
95
|
-
// 但库范式那条排他角色只认后缀式命名 —— 不豁免的话,把测试放进 `__tests__/` 反而吃 S16。
|
|
36
|
+
|
|
96
37
|
{ rule: 'S16', glob: 'src/**/__tests__/**', reason: TEST_LINES_REASON },
|
|
97
38
|
];
|
|
98
39
|
|
|
99
|
-
// `__tests__/**` 里的文件(夹具 / 用例)也要有个角色:方言已经在 `metrics.testGlobs` 里把这里
|
|
100
|
-
// 声明成测试的家,而库范式只给了后缀式命名一条角色 —— 于是 `src/__tests__/x.ts` 会撞 S01,
|
|
101
|
-
// `src/model/__tests__/x.ts` 会被当成模型层(S16 与 S21 都按产品代码判它)。`exclusive` 是必须的:
|
|
102
|
-
// 它排在库范式的目录角色后面,命中即清掉前面的命中,否则 `src/model/__tests__/**` 会"同时命中两个角色"
|
|
103
|
-
// 再撞一次 S01。层号跟后缀式测试同一条(99 ⇒ S21 跳过):脚手架能引产品代码,反过来不行。
|
|
104
40
|
const TESTS_DIR_ROLE = {
|
|
105
41
|
id: 'lib:tests-dir',
|
|
106
42
|
pattern: 'src/**/__tests__/**',
|
|
@@ -108,9 +44,6 @@ const TESTS_DIR_ROLE = {
|
|
|
108
44
|
exclusive: true,
|
|
109
45
|
};
|
|
110
46
|
|
|
111
|
-
// 我们**不拥有**主题:`--dsw-*` 那套令牌由 dsh 在运行时给(与外壳同一口径,
|
|
112
|
-
// 见 ADR-0028 / .scratch/dsh-ui-alignment/spec.md)。所以这里声明的样式目录里,
|
|
113
|
-
// "引用的令牌查不闭合"是正常的 —— 理由就写在下面这条里(与上面那条 S16 一起,是两条内置豁免)。
|
|
114
47
|
const STYLE_EXCEPTION = {
|
|
115
48
|
rule: 'D04',
|
|
116
49
|
glob: 'src/components/**',
|
|
@@ -120,32 +53,25 @@ const STYLE_EXCEPTION = {
|
|
|
120
53
|
'dsh 改名会由外壳升级暴露,而不是由模块本地的表兜住。',
|
|
121
54
|
};
|
|
122
55
|
|
|
123
|
-
// 模块包**运行时**允许的依赖:react(外壳经模块表提供)+ 外壳自己那两个包 + **平台种子词那一整族**。
|
|
124
|
-
//
|
|
125
|
-
// 种子词为什么要一起放进来(2026-10-19 补,用户口径「arch 没加依赖」):那些包是 dsh 的模块表
|
|
126
|
-
// 天生就提供的(`seed.mjs` 是唯一真相),模块引它们走 `dsh.client.external` 是**被允许的路**
|
|
127
|
-
// (ADR-0028 §5 / ADR-0063)。但作者把它们写进 `dependencies` 时,P01 会报「未登记的运行时依赖」,
|
|
128
|
-
// 而 arch-guard 给的那句修法是「加进 `deps({ allow })`」—— 对平台件这是**误导**:它逼每个模块
|
|
129
|
-
// 为一条本来就合法的事开口子。所以这里按 `seed.mjs` 推导(dsh 加词,这张表自己跟着长),
|
|
130
|
-
// 表外的包(真正的第三方 SDK)照旧要明着登记。
|
|
131
|
-
//
|
|
132
|
-
// 推荐写法仍是 **devDependency(类型)+ `dsh.client.external`**:放进 `dependencies` 不会报错,
|
|
133
|
-
// 但 pnpm 会给模块装一份**运行时用不到的**副本(产物里是 `require`,实例由外壳给)。
|
|
134
|
-
// 那是"浪费"不是"错误",所以不由 arch 判;由 `check:source` / `check:primitives` 守真正的错误
|
|
135
|
-
// (宿主侧碰它、漏声明 external、产物被内联)。
|
|
136
|
-
// `@dsh-workbench/module-tools` 是构建/门禁工具,是 devDependency,不进这张表。
|
|
137
56
|
const SEED_PACKAGES = [
|
|
138
57
|
...new Set(
|
|
139
58
|
SEED_SPECIFIERS.map((name) => (name.startsWith('@') ? name.split('/').slice(0, 2).join('/') : name.split('/')[0])),
|
|
140
59
|
),
|
|
141
60
|
];
|
|
142
61
|
const ALLOWED_DEPS = [
|
|
143
|
-
...new Set([
|
|
62
|
+
...new Set([
|
|
63
|
+
...SEED_PACKAGES,
|
|
64
|
+
'@dsh-workbench/core',
|
|
65
|
+
'@dsh-workbench/shared',
|
|
66
|
+
'execa',
|
|
67
|
+
'which',
|
|
68
|
+
'proper-lockfile',
|
|
69
|
+
'write-file-atomic',
|
|
70
|
+
'dayjs',
|
|
71
|
+
'zod',
|
|
72
|
+
]),
|
|
144
73
|
];
|
|
145
74
|
|
|
146
|
-
// 我们**不拥有**主题:颜色 / 圆角 / 字号 / 时长一律引用 dsh 的 `--dsw-*` 令牌,
|
|
147
|
-
// 所以这里没有 tokenDir / palette(声明一个自己的令牌表就是编造一处不存在的真相,
|
|
148
|
-
// 与外壳那份 arch.config.mjs 同一个理由)。留白的三族白名单 = 一个手搓数值都不收(`0` 本来就被跳过)。
|
|
149
75
|
const DESIGN = {
|
|
150
76
|
styleDir: 'src/components',
|
|
151
77
|
tokenDir: '',
|
|
@@ -159,32 +85,124 @@ const DESIGN = {
|
|
|
159
85
|
],
|
|
160
86
|
};
|
|
161
87
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
88
|
+
const MODEL_FILE = /^src\/(model\/|messages\.ts$)/;
|
|
89
|
+
const MODEL_BANNED = [
|
|
90
|
+
/^react(?:\/|$)/,
|
|
91
|
+
/^react-dom(?:\/|$)/,
|
|
92
|
+
/^@deepseek-ai\//,
|
|
93
|
+
/^@dsh-workbench\/core(?:\/client)?$/,
|
|
94
|
+
/^@dsh-workbench\/shared\/host$/,
|
|
95
|
+
/^node:/,
|
|
96
|
+
/^opencli(?:\/|$)/,
|
|
97
|
+
/^@\/components(?:\/|$)/,
|
|
98
|
+
/^(?:\.\.?\/)+(?:components|host|client)(?:\/|$)/,
|
|
99
|
+
/^(?:@\/|\.\.?\/)(?:host|client)$/,
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const COMPONENT_FILE = /^src\/components\//;
|
|
103
|
+
const COMPONENT_BANNED = [
|
|
104
|
+
/^@dsh-workbench\/shared\/host$/,
|
|
105
|
+
/^@dsh-workbench\/core$/,
|
|
106
|
+
/^node:/,
|
|
107
|
+
/^opencli(?:\/|$)/,
|
|
108
|
+
/^(?:\.\.?\/)+(?:host|client)$/,
|
|
109
|
+
/^(?:@\/)(?:host|client)$/,
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const modelStaysPure = createRule({
|
|
113
|
+
id: 'S71',
|
|
114
|
+
domain: 'structure',
|
|
115
|
+
level: 'L2',
|
|
116
|
+
severity: 'error',
|
|
117
|
+
title: '模块核心(model / messages)不碰宿主、UI 与运行时',
|
|
118
|
+
hint:
|
|
119
|
+
'model / messages 是模块里最底的一层:只放纯逻辑、状态机、文案。' +
|
|
120
|
+
'React、kit、dsh、opencli、node:*、组件与两个面都不许 import;要用类型就从 core/contract 取 import type。',
|
|
121
|
+
run(ctx) {
|
|
122
|
+
const findings = [];
|
|
123
|
+
for (const record of ctx.records) {
|
|
124
|
+
if (!MODEL_FILE.test(record.rel)) continue;
|
|
125
|
+
for (const fact of ctx.facts.get(record.rel)?.imports ?? []) {
|
|
126
|
+
if (fact.typeOnly) continue;
|
|
127
|
+
if (!MODEL_BANNED.some((pattern) => pattern.test(fact.spec))) continue;
|
|
128
|
+
findings.push({
|
|
129
|
+
rule: 'S71',
|
|
130
|
+
file: record.rel,
|
|
131
|
+
line: fact.line,
|
|
132
|
+
...(fact.column === undefined ? {} : { column: fact.column }),
|
|
133
|
+
text: `模块核心 import 了 ${fact.spec}`,
|
|
134
|
+
hint: '把宿主 / UI / 运行时的依赖推到 components 或两个面;核心只依赖纯类型与纯函数',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return findings;
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const componentsStayBrowser = createRule({
|
|
143
|
+
id: 'S72',
|
|
144
|
+
domain: 'structure',
|
|
145
|
+
level: 'L2',
|
|
146
|
+
severity: 'error',
|
|
147
|
+
title: '模块视图不碰宿主面',
|
|
148
|
+
hint:
|
|
149
|
+
'components 只消费 props:可以拿 kit 与纯数据,不许 import 宿主入口、shared/host、dsh 原语或 node:*。' +
|
|
150
|
+
'取数、写、路由都在两个面与 model 里。',
|
|
151
|
+
run(ctx) {
|
|
152
|
+
const findings = [];
|
|
153
|
+
for (const record of ctx.records) {
|
|
154
|
+
if (!COMPONENT_FILE.test(record.rel)) continue;
|
|
155
|
+
for (const fact of ctx.facts.get(record.rel)?.imports ?? []) {
|
|
156
|
+
if (fact.typeOnly) continue;
|
|
157
|
+
if (!COMPONENT_BANNED.some((pattern) => pattern.test(fact.spec))) continue;
|
|
158
|
+
findings.push({
|
|
159
|
+
rule: 'S72',
|
|
160
|
+
file: record.rel,
|
|
161
|
+
line: fact.line,
|
|
162
|
+
...(fact.column === undefined ? {} : { column: fact.column }),
|
|
163
|
+
text: `模块视图 import 了 ${fact.spec}`,
|
|
164
|
+
hint: '组件只接收数据与回调;宿主侧能力放 src/host.ts,浏览器侧取数放面的 hooks',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return findings;
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const LOCAL_PROTOCOL =
|
|
173
|
+
/\b(?:interface|type)\s+(UnitModuleFace|UnitMountResult|UnitClientFace|UnitClientApi|UnitModuleRegistry|UnitClientRegistry|UnitRegistry|HostContext|ClientContext|BoardProps)\b/g;
|
|
174
|
+
|
|
175
|
+
const lineOf = (text, index) => text.slice(0, index).split('\n').length;
|
|
176
|
+
|
|
177
|
+
const protocolsStayCanonical = createRule({
|
|
178
|
+
id: 'S73',
|
|
179
|
+
domain: 'structure',
|
|
180
|
+
level: 'L2',
|
|
181
|
+
severity: 'error',
|
|
182
|
+
title: '模块不本地重声明外壳协议',
|
|
183
|
+
hint:
|
|
184
|
+
'协议类型只有一份:`@dsh-workbench/core/contract`。删掉本地 interface / type,改成 `import type`;' +
|
|
185
|
+
'本地重声明会让外壳升级时模块静默对不上。',
|
|
186
|
+
run(ctx) {
|
|
187
|
+
const findings = [];
|
|
188
|
+
for (const record of ctx.records) {
|
|
189
|
+
const text = readFileSync(record.abs, 'utf8');
|
|
190
|
+
for (const match of text.matchAll(LOCAL_PROTOCOL)) {
|
|
191
|
+
findings.push({
|
|
192
|
+
rule: 'S73',
|
|
193
|
+
file: record.rel,
|
|
194
|
+
line: lineOf(text, match.index),
|
|
195
|
+
text: `本地声明 ${match[1]}`,
|
|
196
|
+
hint: '删掉,改 import type from @dsh-workbench/core/contract',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return findings;
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const MODULE_RULES = [modelStaysPure, componentsStayBrowser, protocolsStayCanonical];
|
|
205
|
+
|
|
188
206
|
export function moduleArch(options = {}) {
|
|
189
207
|
const {
|
|
190
208
|
fileLines,
|
|
@@ -207,20 +225,32 @@ export function moduleArch(options = {}) {
|
|
|
207
225
|
return {
|
|
208
226
|
sourceForm: 'react',
|
|
209
227
|
presets: [
|
|
210
|
-
|
|
228
|
+
|
|
211
229
|
library({ src: 'src', entry: ['host.ts', 'client.tsx'], modules: { ...LAYERS, ...layers } }),
|
|
212
|
-
|
|
213
|
-
deps({
|
|
214
|
-
|
|
230
|
+
|
|
231
|
+
deps({
|
|
232
|
+
allow: [...ALLOWED_DEPS, ...extraDeps],
|
|
233
|
+
deny: ['shell-quote', 'shelljs', 'zx'],
|
|
234
|
+
capabilities: {
|
|
235
|
+
'unique-id': 'crypto.randomUUID',
|
|
236
|
+
'number-format': 'Intl.NumberFormat',
|
|
237
|
+
'deep-equal': 'node:util.isDeepStrictEqual',
|
|
238
|
+
datetime: 'dayjs',
|
|
239
|
+
'deep-clone': 'structuredClone',
|
|
240
|
+
'query-string': 'URLSearchParams',
|
|
241
|
+
validation: 'zod',
|
|
242
|
+
},
|
|
243
|
+
fingerprints: [{ capability: 'query-string', allowOwn: false }],
|
|
244
|
+
}),
|
|
245
|
+
|
|
215
246
|
designSystem({ ...DESIGN }),
|
|
216
|
-
|
|
247
|
+
|
|
217
248
|
styles(cssModulesKit()),
|
|
218
|
-
|
|
249
|
+
|
|
219
250
|
hygiene(),
|
|
220
|
-
|
|
251
|
+
|
|
221
252
|
i18n(noneI18nKit()),
|
|
222
|
-
|
|
223
|
-
// 门禁链不在这里查(`check` 不跑 vitest,那是 `pnpm test` 的事,与外壳同一条口径)。
|
|
253
|
+
|
|
224
254
|
metrics({
|
|
225
255
|
tests: {
|
|
226
256
|
requireTestsFor: ['src/model/**'],
|
|
@@ -228,14 +258,15 @@ export function moduleArch(options = {}) {
|
|
|
228
258
|
checkChain: { script: 'check', require: [] },
|
|
229
259
|
},
|
|
230
260
|
}),
|
|
261
|
+
|
|
262
|
+
{ enable: ['S71', 'S72', 'S73'] },
|
|
231
263
|
],
|
|
232
264
|
overrides: {
|
|
233
265
|
include: ['src/**'],
|
|
234
266
|
addRoles: [MESSAGES_ROLE, DECLARED_TOOLS_ROLE, TESTS_DIR_ROLE, ...roles],
|
|
235
267
|
thresholds,
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// 那是 dsh 插件的契约,不是我们在这里能收敛成某个前缀的东西。
|
|
268
|
+
customRules: MODULE_RULES,
|
|
269
|
+
|
|
239
270
|
structure: structure ?? {
|
|
240
271
|
runtimes: [
|
|
241
272
|
{ name: 'host', entries: [HOST_FACE] },
|
|
@@ -244,9 +275,7 @@ export function moduleArch(options = {}) {
|
|
|
244
275
|
faces: [
|
|
245
276
|
{ pattern: HOST_FACE, runtime: 'host' },
|
|
246
277
|
{ pattern: CLIENT_FACE, runtime: 'client' },
|
|
247
|
-
|
|
248
|
-
// (层号拦不住它 —— components 在第 2 层、host 面在第 10 层,S21 认为"向下依赖"合法)。
|
|
249
|
-
// 那正是最典型的一种错:宿主产物里拖进一个只会渲染的东西,`dist/host.js` 在 Node 里起不来。
|
|
278
|
+
|
|
250
279
|
{ pattern: 'src/components/**', runtime: 'client' },
|
|
251
280
|
],
|
|
252
281
|
},
|