@dsh-workbench/module-tools 0.3.10 → 1.0.0-beta.0
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 +18 -3
- package/src/arch.mjs +158 -129
- package/src/build.mjs +6 -30
- package/src/changelog.mjs +167 -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 +61 -222
- 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 +163 -29
- package/src/checks/standalone.mjs +4 -14
- package/src/checks/style.mjs +71 -241
- 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 +6 -24
- 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 +80 -0
- package/src/release.mjs +1251 -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/messages.ts.tpl +4 -4
- 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/src/build.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
2
|
import { createHash } from 'node:crypto';
|
|
4
3
|
import { builtinModules } from 'node:module';
|
|
@@ -11,9 +10,6 @@ const SHELL_CLIENT = '@dsh-workbench/core/client';
|
|
|
11
10
|
const RESOLVE_EXTENSIONS = ['.ts', '.tsx', '.js', '.json'];
|
|
12
11
|
const SINGLETON = /^@\/shared\/(hooks|components)\//;
|
|
13
12
|
|
|
14
|
-
// dsh 的客户端模块表里"天生就有"的包(seed 词)。模块的浏览器产物**默认**把它们标成外部:
|
|
15
|
-
// 这些是外壳那一份实例,内联进来就是第二份 —— 两份 React 组件树、两套样式、hover 与浮层失联。
|
|
16
|
-
// 名单在 `../seed.mjs`(唯一真相);判据侧由 `checks/source.mjs` 与 `checks/primitives.mjs` 守。
|
|
17
13
|
const SEED_EXTERNAL = SEED_SPECIFIERS;
|
|
18
14
|
|
|
19
15
|
function pickEntry(root, names) {
|
|
@@ -48,8 +44,7 @@ function aliasResolver({ root, unit }) {
|
|
|
48
44
|
`模块自己的文件用 @/modules/${unit ?? '<unit>'}/… 或相对路径`,
|
|
49
45
|
);
|
|
50
46
|
}
|
|
51
|
-
|
|
52
|
-
// 不要按 .ts/.tsx 去找 —— 否则别名插件的候选表把样式文件挡在外面。
|
|
47
|
+
|
|
53
48
|
if (base.endsWith('.css') && existsSync(base)) {
|
|
54
49
|
return { path: base, namespace: 'dsh-wb-css' };
|
|
55
50
|
}
|
|
@@ -71,15 +66,7 @@ function cssInjector({ root }) {
|
|
|
71
66
|
namespace: 'dsh-wb-css',
|
|
72
67
|
}));
|
|
73
68
|
context.onLoad({ filter: /.*/, namespace: 'dsh-wb-css' }, (args) => {
|
|
74
|
-
|
|
75
|
-
// 那一份,产物里再带一份就变成"两份 kit 样式按选择器混合"(2026-10-18 实测:模块内联了 145
|
|
76
|
-
// 条外壳规则、冻结构建时的版本,与运行的外壳混用 ⇒ 折行 / 塌列 / 标签压值)。见 ADR-0028 §5.1。
|
|
77
|
-
// 两条路径都要认:
|
|
78
|
-
// · 装了依赖的模块仓 —— `node_modules/@dsh-workbench/{shared,core}/…`(pnpm 的 store 里也含这段);
|
|
79
|
-
// · 本仓自己开发时 —— `packages/{shared,workbench}/dist/…`。**是 dist 不是 src**:esbuild 默认
|
|
80
|
-
// 跟随软链(`preserveSymlinks:false`),workspace 的 `node_modules/@dsh-workbench/shared`
|
|
81
|
-
// 软链会被解成真实源码包路径,而 `exports."."` 指的是编译产物 `dist/index.js`,组件样式
|
|
82
|
-
// 与它同住 `dist/components/**`(只写 `src` 会在本仓场景漏收:实测 148 条规则照进产物)。
|
|
69
|
+
|
|
83
70
|
const outsourcedKitCss =
|
|
84
71
|
/node_modules\/@dsh-workbench\/(shared|core)\//.test(args.path) ||
|
|
85
72
|
/\/packages\/(shared|workbench)\/(src|dist)\//.test(args.path);
|
|
@@ -87,16 +74,12 @@ function cssInjector({ root }) {
|
|
|
87
74
|
const raw = readFileSync(args.path, 'utf8');
|
|
88
75
|
const key = relative(root, args.path).split('/').reverse()[0] + ':' + args.path;
|
|
89
76
|
const scoped = /\.module\.css$/.test(args.path);
|
|
90
|
-
|
|
91
|
-
// (`import styles from './X.module.css'` → `styles.foo`)。哈希取"模块内相对路径",
|
|
92
|
-
// 同一个类在同一次构建里稳定,跨模块不同名。非 module 的 .css 原样注入(依赖里的就是这种)。
|
|
77
|
+
|
|
93
78
|
let css = raw;
|
|
94
79
|
let map = '{}';
|
|
95
80
|
if (scoped) {
|
|
96
81
|
const hash = createHash('sha1').update(relative(root, args.path)).digest('hex').slice(0, 6);
|
|
97
|
-
|
|
98
|
-
// 模块借用它时只能这样写:`.wb-envRefresh:global(.wb-iconButton)`。先把这些片段挖出来存着,
|
|
99
|
-
// 哈希完再放回去。
|
|
82
|
+
|
|
100
83
|
const globals = [];
|
|
101
84
|
const guarded = raw.replace(/:global\([^)]*\)/g, (piece) => {
|
|
102
85
|
globals.push(piece.slice(':global('.length, -1));
|
|
@@ -105,9 +88,7 @@ function cssInjector({ root }) {
|
|
|
105
88
|
const names = [
|
|
106
89
|
...new Set([...guarded.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)].map((match) => match[1])),
|
|
107
90
|
].sort((a, b) => b.length - a.length);
|
|
108
|
-
|
|
109
|
-
// 非法选择器 —— 浏览器会把整条规则丢掉(实测:20 条规则只解析出 1 条,界面整屏掉样式)。
|
|
110
|
-
// 前面加一个下划线,保持 CSS Modules 的老习惯(`._3a0f86_x`)。
|
|
91
|
+
|
|
111
92
|
const pairs = names.map((name) => [name, `_${hash}_${name}`]);
|
|
112
93
|
css = guarded;
|
|
113
94
|
for (const [from, to] of pairs) css = css.split(`.${from}`).join(`.${to}`);
|
|
@@ -169,9 +150,6 @@ export async function build({ root }) {
|
|
|
169
150
|
logLevel: 'warning',
|
|
170
151
|
});
|
|
171
152
|
|
|
172
|
-
// 物证:metafile 列出**这次真的被打进包里**的每个输入文件。dsh 的包只要出现,
|
|
173
|
-
// 就说明它没走外部化(声明漏了 / 名字写错 / 从 shared 内联进来)—— 那正是"第二份实例"。
|
|
174
|
-
// 不靠产物里的字符串去猜(tree-shaking 之后标记可能根本不进包),也不靠它自己报错。
|
|
175
153
|
const inlinedDsh = Object.keys(client.metafile.inputs).filter((path) => /@deepseek-ai[\\/]/.test(path));
|
|
176
154
|
if (inlinedDsh.length > 0) {
|
|
177
155
|
throw new Error(
|
|
@@ -188,8 +166,6 @@ export async function build({ root }) {
|
|
|
188
166
|
const wrapped = `// ⚠️ 本文件由 dsh-wb-module build 生成,请勿手改。改 src/ 后跑:dsh-wb-module build\nwindow.__ModuleLoader__.load({\n id: ${JSON.stringify(id)},\n factory(require) {\n const module = { exports: {} };\n const exports = module.exports;\n${indented}\n return module.exports;\n },\n});\n`;
|
|
189
167
|
|
|
190
168
|
writeFileSync(join(root, 'client.js'), wrapped);
|
|
191
|
-
|
|
192
|
-
// 中文文案,报出来会比真实文件小(实测 `module-hello/client.js`:3095 字符 / 3131 字节)。
|
|
193
|
-
// 报字节就按字节算。
|
|
169
|
+
|
|
194
170
|
console.log(`${id}:client.js 已更新(${Buffer.byteLength(wrapped, 'utf8')} 字节)`);
|
|
195
171
|
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CHANGELOG_FILE,
|
|
6
|
+
CHANGELOG_KINDS,
|
|
7
|
+
appendChangelogEntry,
|
|
8
|
+
changelogDate,
|
|
9
|
+
changelogHasEntry,
|
|
10
|
+
changelogSkeleton,
|
|
11
|
+
changelogVersion,
|
|
12
|
+
} from '@dsh-workbench/shared/host';
|
|
13
|
+
|
|
14
|
+
const SEMVER = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
15
|
+
|
|
16
|
+
const USAGE = `用法:
|
|
17
|
+
changelog init 建一份 ${CHANGELOG_FILE}(已有就不动)
|
|
18
|
+
changelog add <版本> [--type ${CHANGELOG_KINDS.join('|')}] [--note <文字>] [--date <日期>]
|
|
19
|
+
changelog check [版本] 查这一版有没有记录(省略版本用 package.json 的)
|
|
20
|
+
版本也可以省略:用 package.json 里的 version。发布时这一版必须有记录,否则拦下。`;
|
|
21
|
+
|
|
22
|
+
function argValue(args, flag) {
|
|
23
|
+
const at = args.indexOf(flag);
|
|
24
|
+
return at === -1 ? undefined : args[at + 1];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function positional(args, at) {
|
|
28
|
+
const value = args[at];
|
|
29
|
+
return value === undefined || value.startsWith('--') ? undefined : value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function changelogFileOf(root) {
|
|
33
|
+
return join(root, CHANGELOG_FILE);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function readChangelogText(root) {
|
|
37
|
+
try {
|
|
38
|
+
return readFileSync(changelogFileOf(root), 'utf8');
|
|
39
|
+
} catch {
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function manifestVersionOf(root) {
|
|
45
|
+
try {
|
|
46
|
+
const version = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version;
|
|
47
|
+
return typeof version === 'string' ? version.trim() : '';
|
|
48
|
+
} catch {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function changelogProblemOf(root, version) {
|
|
54
|
+
const text = readChangelogText(root);
|
|
55
|
+
if (text.trim() === '') {
|
|
56
|
+
return {
|
|
57
|
+
reason: `包里还没有 ${CHANGELOG_FILE} —— 更新日志要随包发布`,
|
|
58
|
+
command: 'dsh-wb-module changelog init',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (!changelogHasEntry(text, version)) {
|
|
62
|
+
return {
|
|
63
|
+
reason: `${CHANGELOG_FILE} 里没有 ${version} 这一版的内容 —— 发布前先把这一版写进去`,
|
|
64
|
+
command: `dsh-wb-module changelog add ${version} --note "这一版改了什么"`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function initChangelog(root) {
|
|
71
|
+
const file = changelogFileOf(root);
|
|
72
|
+
if (existsSync(file)) return { file, created: false };
|
|
73
|
+
writeFileSync(file, changelogSkeleton());
|
|
74
|
+
return { file, created: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class ChangelogError extends Error {
|
|
78
|
+
constructor(message, code = 2) {
|
|
79
|
+
super(message);
|
|
80
|
+
this.name = 'ChangelogError';
|
|
81
|
+
this.code = code;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function addChangelog(root, { version, kind = 'changed', note, date }) {
|
|
86
|
+
const text = (note ?? '').trim();
|
|
87
|
+
if (text === '') throw new ChangelogError('changelog add 要一句 --note:这一版改了什么');
|
|
88
|
+
if (!CHANGELOG_KINDS.includes(kind)) {
|
|
89
|
+
throw new ChangelogError(`--type 只认 ${CHANGELOG_KINDS.join(' / ')}(收到「${kind}」)`);
|
|
90
|
+
}
|
|
91
|
+
const wanted = changelogVersion(version === undefined || version === '' ? manifestVersionOf(root) : version);
|
|
92
|
+
if (!SEMVER.test(wanted)) {
|
|
93
|
+
throw new ChangelogError(`版本要写成 x.y.z(收到「${String(version)}」)—— 也可以省略,用 package.json 里的`);
|
|
94
|
+
}
|
|
95
|
+
const file = changelogFileOf(root);
|
|
96
|
+
const body = appendChangelogEntry(readChangelogText(root), { version: wanted, kind, note: text, date });
|
|
97
|
+
writeFileSync(file, body);
|
|
98
|
+
return { file, version: wanted, kind };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function runInit(root, args) {
|
|
102
|
+
const result = initChangelog(root);
|
|
103
|
+
if (args.includes('--json')) {
|
|
104
|
+
console.log(JSON.stringify({ ok: true, ...result }));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!result.created) {
|
|
108
|
+
console.log(`${CHANGELOG_FILE} 已经在:${result.file} —— 没动它`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.log(`✔ 建好了 ${result.file}`);
|
|
112
|
+
console.log(` 下一步:dsh-wb-module changelog add ${manifestVersionOf(root) || '<版本>'} --note "这一版改了什么"`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function runAdd(root, args) {
|
|
116
|
+
const version = positional(args, 1) ?? manifestVersionOf(root);
|
|
117
|
+
const kind = argValue(args, '--type') ?? 'changed';
|
|
118
|
+
const note = argValue(args, '--note') ?? '';
|
|
119
|
+
const date = (argValue(args, '--date') ?? changelogDate()).trim();
|
|
120
|
+
let result;
|
|
121
|
+
try {
|
|
122
|
+
result = addChangelog(root, { version, kind, note, date });
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (!(error instanceof ChangelogError)) throw error;
|
|
125
|
+
console.error(error.message);
|
|
126
|
+
process.exit(error.code);
|
|
127
|
+
}
|
|
128
|
+
if (args.includes('--json')) {
|
|
129
|
+
console.log(JSON.stringify({ ok: true, ...result, date }));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
console.log(`✔ ${CHANGELOG_FILE} 已记下 ${result.version}(${result.kind})`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function runCheck(root, args) {
|
|
136
|
+
const version = changelogVersion(positional(args, 1) ?? manifestVersionOf(root));
|
|
137
|
+
if (!SEMVER.test(version)) {
|
|
138
|
+
console.error('changelog check 要知道查哪一版:给一个 x.y.z,或先让 package.json 有 version');
|
|
139
|
+
process.exit(2);
|
|
140
|
+
}
|
|
141
|
+
const problem = changelogProblemOf(root, version);
|
|
142
|
+
if (args.includes('--json')) {
|
|
143
|
+
console.log(
|
|
144
|
+
JSON.stringify(
|
|
145
|
+
problem === undefined
|
|
146
|
+
? { ok: true, version }
|
|
147
|
+
: { ok: false, version, reason: problem.reason, command: problem.command },
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
} else if (problem === undefined) {
|
|
151
|
+
console.log(`✔ ${CHANGELOG_FILE} 里有 ${version} 的更新日志`);
|
|
152
|
+
} else {
|
|
153
|
+
console.error(`✖ ${problem.reason}`);
|
|
154
|
+
console.error(` 补上:${problem.command}`);
|
|
155
|
+
}
|
|
156
|
+
if (problem !== undefined) process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function changelog({ root, args = [] }) {
|
|
160
|
+
const sub = args[0];
|
|
161
|
+
if (sub === 'init') return runInit(root, args);
|
|
162
|
+
if (sub === 'add') return runAdd(root, args);
|
|
163
|
+
if (sub === 'check') return runCheck(root, args);
|
|
164
|
+
console.error(`不认识的 changelog 子命令:${String(sub)}`);
|
|
165
|
+
console.error(USAGE);
|
|
166
|
+
process.exit(2);
|
|
167
|
+
}
|
package/src/check-flags.mjs
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
// `dsh-wb-module check --only <名字>` 单跑时透传的开关。
|
|
2
|
-
//
|
|
3
|
-
// 聚合层只认 `--only` / `--cwd`,其余开关(`--all` / `--files` / `--json`)原样经
|
|
4
|
-
// `DSH_WB_ONLY_ARGS`(JSON 数组)交给**单跑的那一条** —— 这是 `check.mjs` 的加法之一
|
|
5
|
-
// (给 `run()` 加参数会让"所有判据都是无参 `run()`"这条统一形状多一个例外)。
|
|
6
|
-
// 名字只有这一处写,别把环境变量名抄成第二份真相。
|
|
7
1
|
export function flagsOf() {
|
|
8
2
|
try {
|
|
9
3
|
const parsed = JSON.parse(process.env.DSH_WB_ONLY_ARGS ?? '[]');
|
|
@@ -13,7 +7,6 @@ export function flagsOf() {
|
|
|
13
7
|
}
|
|
14
8
|
}
|
|
15
9
|
|
|
16
|
-
/** `--file <名字>` 那种带值的开关取它的值(没给值返回 undefined)。 */
|
|
17
10
|
export function flagValue(flags, flag) {
|
|
18
11
|
const at = flags.indexOf(flag);
|
|
19
12
|
if (at === -1) return undefined;
|
package/src/check.mjs
CHANGED
|
@@ -1,55 +1,116 @@
|
|
|
1
|
-
// 判据的**聚合层**:跑、收集三态、打印、决定退出码。有几条以 `CHECKS` 表为准。
|
|
2
|
-
// ── 为什么判据改成"导出函数",打印与退出码都搬到这里 ──────────────────────────
|
|
3
|
-
//
|
|
4
|
-
// 在这之前,每条 `checks/*.mjs` 都是"import 即执行 + 自己 console.log + 自己 process.exit"
|
|
5
|
-
// 的脚本。那样"不适用"就没有一等形状 —— 只能靠"这条判据不许 exit(0)"这条**纪律**维持:
|
|
6
|
-
// 谁哪天写个 `process.exit(0)`,排在它后面的判据就静默不跑,而屏幕看着还是全绿
|
|
7
|
-
// (standalone 在仓外工作区就是这么吞掉 markers / style / alias 三条的,见
|
|
8
|
-
// `.scratch/workbench-diy/spec.md` §20)。
|
|
9
|
-
//
|
|
10
|
-
// 另一件后果是聚合层拿不到"跑了哪些、跳了哪些、为什么跳",只能看判据自己打的字。
|
|
11
|
-
//
|
|
12
|
-
// 新形状:每条判据导出 `run()`,返回
|
|
13
|
-
// { status: 'pass' | 'fail' | 'skip', notes: string[], problems: string[] }
|
|
14
|
-
// ——"不适用"于是是一等结果,而**截断在形状上就不可能**:
|
|
15
|
-
// 判据手里没有 exit,退出码只有这一层按汇总给。
|
|
16
|
-
//
|
|
17
|
-
// 打印形状(三态,稳定):
|
|
18
|
-
// 通过 ✔ <title>:<notes 拼起来>
|
|
19
|
-
// 不适用 ○ <title>:(不适用)<notes>
|
|
20
|
-
// 失败 ✖ <title> <N> 条不过: 然后逐条 ` · <problem>`
|
|
21
|
-
// 最后一行是汇总;有任何 fail 就 exit 1。
|
|
22
|
-
//
|
|
23
|
-
// ── 两条加法(2026-10-19,dsh-primitives-face)────────────────────────────────
|
|
24
|
-
//
|
|
25
|
-
// ① 开关透传:聚合层只认 `--only` / `--cwd`,其余开关(`--all` / `--json`)原样经
|
|
26
|
-
// `DSH_WB_ONLY_ARGS`(JSON 数组)交给**单跑的那一条**。为什么走 env 而不是给 `run()`
|
|
27
|
-
// 加参数:所有判据的形状都是 `run()` 无参,加参数会让"统一形状"这条纪律多一个例外。
|
|
28
|
-
// ② 可选 `payload`:判据可以额外返回 `payload`;带着 `--json` 跑时,聚合层打它、**跳过**
|
|
29
|
-
// 人话版 report 与最后的汇总行(stdout 只有一份 JSON)。老判据不返回它,行为一字不变 ——
|
|
30
|
-
// 这是加法,不是新形状。
|
|
31
|
-
|
|
32
|
-
// 名字与 title 一字不变 —— selfcheck 会把它与各判据模块导出的 `title` 逐个对一遍。
|
|
33
1
|
export const CHECKS = {
|
|
34
|
-
contracts: {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
2
|
+
contracts: {
|
|
3
|
+
title: '模块契约',
|
|
4
|
+
summary: '单元 id、两个面、四个脚本、files、peer 下限,以及清单与代码注册对账',
|
|
5
|
+
layer: 'manifest',
|
|
6
|
+
load: () => import('./checks/contracts.mjs'),
|
|
7
|
+
},
|
|
8
|
+
protocol: {
|
|
9
|
+
title: '模块声明协议',
|
|
10
|
+
summary: 'dsh.workbench 的 apiVersion、静态半形状与未知键',
|
|
11
|
+
layer: 'manifest',
|
|
12
|
+
load: () => import('./checks/protocol.mjs'),
|
|
13
|
+
},
|
|
14
|
+
bundles: {
|
|
15
|
+
title: '模块产物边界',
|
|
16
|
+
summary: '宿主产物不含浏览器侧、浏览器产物不内联外壳 CSS 与夹具',
|
|
17
|
+
layer: 'build',
|
|
18
|
+
load: () => import('./checks/bundles.mjs'),
|
|
19
|
+
},
|
|
20
|
+
source: {
|
|
21
|
+
title: '模块源码边界与零注释',
|
|
22
|
+
summary: '零注释、包与运行时入口分开、不手写进程/锁/原子写',
|
|
23
|
+
layer: 'source',
|
|
24
|
+
load: () => import('./checks/source.mjs'),
|
|
25
|
+
},
|
|
26
|
+
arch: {
|
|
27
|
+
title: '模块架构(分层 / 边界 / 设计系统)',
|
|
28
|
+
summary: '模块自己的层序、运行时边界与设计系统配置',
|
|
29
|
+
layer: 'source',
|
|
30
|
+
load: () => import('./checks/arch.mjs'),
|
|
31
|
+
},
|
|
32
|
+
lint: {
|
|
33
|
+
title: '模块代码卫生(eslint)',
|
|
34
|
+
summary: '模块自己的 eslint.config.mjs 与规则集',
|
|
35
|
+
layer: 'source',
|
|
36
|
+
load: () => import('./checks/lint.mjs'),
|
|
37
|
+
},
|
|
38
|
+
primitives: {
|
|
39
|
+
title: 'dsh 原语只从模块表拿',
|
|
40
|
+
summary: 'dsh 包只从 kit 取,模块自己声明 external',
|
|
41
|
+
layer: 'build',
|
|
42
|
+
load: () => import('./checks/primitives.mjs'),
|
|
43
|
+
},
|
|
44
|
+
kit: {
|
|
45
|
+
title: '外壳 kit 面',
|
|
46
|
+
summary: '模块产物对得上所装外壳的 kit 面与 peer 下限',
|
|
47
|
+
layer: 'build',
|
|
48
|
+
load: () => import('./checks/kit.mjs'),
|
|
49
|
+
},
|
|
50
|
+
cards: {
|
|
51
|
+
title: '卡片渲染',
|
|
52
|
+
summary: '八态 × zh/en 渲染:不抛、不空白、不冒裸键名',
|
|
53
|
+
layer: 'render',
|
|
54
|
+
load: () => import('./checks/cards.mjs'),
|
|
55
|
+
},
|
|
56
|
+
standalone: {
|
|
57
|
+
title: '独立构建',
|
|
58
|
+
summary: '外壳源码不在场时仍能 build + typecheck',
|
|
59
|
+
layer: 'build',
|
|
60
|
+
load: () => import('./checks/standalone.mjs'),
|
|
61
|
+
},
|
|
62
|
+
markers: {
|
|
63
|
+
title: '公开包无内部标识',
|
|
64
|
+
summary: '公开包不含私有源、私网 IP、内部域名后缀',
|
|
65
|
+
layer: 'meta',
|
|
66
|
+
load: () => import('./checks/markers.mjs'),
|
|
67
|
+
},
|
|
68
|
+
style: {
|
|
69
|
+
title: '样式归属',
|
|
70
|
+
summary: '不定义 wb-* 规则;裸标签控件改走 kit;令牌名对现读的清单校验',
|
|
71
|
+
layer: 'source',
|
|
72
|
+
load: () => import('./checks/style.mjs'),
|
|
73
|
+
},
|
|
74
|
+
alias: {
|
|
75
|
+
title: '模块不指外壳',
|
|
76
|
+
summary: '模块源码里没有指向外壳 @/ 的引用',
|
|
77
|
+
layer: 'source',
|
|
78
|
+
load: () => import('./checks/alias.mjs'),
|
|
79
|
+
},
|
|
47
80
|
};
|
|
48
81
|
|
|
49
|
-
|
|
82
|
+
export function checkList() {
|
|
83
|
+
return Object.entries(CHECKS).map(([name, entry]) => ({
|
|
84
|
+
name,
|
|
85
|
+
title: entry.title,
|
|
86
|
+
summary: entry.summary,
|
|
87
|
+
layer: entry.layer,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function renderCheckTable(rows = checkList()) {
|
|
92
|
+
return [
|
|
93
|
+
'| 判据 | 层 | 摘要 |',
|
|
94
|
+
'|---|---|---|',
|
|
95
|
+
...rows.map((row) => `| \`${row.name}\` | ${row.layer} | ${row.summary} |`),
|
|
96
|
+
].join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function list(args) {
|
|
100
|
+
const rows = checkList();
|
|
101
|
+
if (args.includes('--json')) {
|
|
102
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (args.includes('--markdown')) {
|
|
106
|
+
console.log(renderCheckTable(rows));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
for (const row of rows) console.log(`${row.name}\t${row.layer}\t${row.title}\t${row.summary}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
50
112
|
const STATES = ['pass', 'fail', 'skip'];
|
|
51
113
|
|
|
52
|
-
/** 一屏一条:三态各一种前缀,notes 的第一条跟在标题后面,其余各占一行。 */
|
|
53
114
|
function report(title, result) {
|
|
54
115
|
const notes = Array.isArray(result.notes) ? result.notes : [];
|
|
55
116
|
if (result.status === 'fail') {
|
|
@@ -68,6 +129,10 @@ function report(title, result) {
|
|
|
68
129
|
}
|
|
69
130
|
|
|
70
131
|
export async function check({ only, args = [] } = {}) {
|
|
132
|
+
if (args.includes('--list')) {
|
|
133
|
+
list(args);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
71
136
|
const names = only === undefined ? Object.keys(CHECKS) : [only];
|
|
72
137
|
const jsonMode = args.includes('--json');
|
|
73
138
|
const tally = { pass: 0, skip: 0, fail: 0 };
|
|
@@ -81,12 +146,11 @@ export async function check({ only, args = [] } = {}) {
|
|
|
81
146
|
console.error(`不认识的判据:${name}(有:${Object.keys(CHECKS).join(' / ')})`);
|
|
82
147
|
process.exit(2);
|
|
83
148
|
}
|
|
84
|
-
|
|
85
|
-
// 这份外壳的 kit 成员列出来),聚合跑时那些清单属于噪音,不该倒。
|
|
149
|
+
|
|
86
150
|
process.env.DSH_WB_ONLY = only === undefined ? '' : name;
|
|
87
151
|
const module = await entry.load();
|
|
88
152
|
const result = await module.run();
|
|
89
|
-
|
|
153
|
+
|
|
90
154
|
const title = typeof module.title === 'string' && module.title !== '' ? module.title : entry.title;
|
|
91
155
|
ran += 1;
|
|
92
156
|
if (!STATES.includes(result.status)) {
|
|
@@ -108,15 +172,13 @@ export async function check({ only, args = [] } = {}) {
|
|
|
108
172
|
process.env.DSH_WB_ONLY_ARGS = '';
|
|
109
173
|
}
|
|
110
174
|
|
|
111
|
-
// `--json` 的 stdout 只有那几份 payload:汇总行与"不适用"那几段都是人话,不进机器档。
|
|
112
175
|
if (jsonMode) {
|
|
113
176
|
if (tally.fail > 0) process.exit(1);
|
|
114
177
|
return;
|
|
115
178
|
}
|
|
116
179
|
|
|
117
180
|
console.log(`${ran} 条判据:${tally.pass} 通过 · ${tally.skip} 不适用 · ${tally.fail} 不过`);
|
|
118
|
-
|
|
119
|
-
// "8 通过 · 2 不适用" 念成"全过了" —— 而工坊里那两条是**结构性**跑不了,不是这次没问题。
|
|
181
|
+
|
|
120
182
|
if (skipped.length > 0) {
|
|
121
183
|
console.log(` 这 ${skipped.length} 条这次一条都没查(不是"没问题"):`);
|
|
122
184
|
for (const one of skipped) console.log(` ○ ${one}`);
|
package/src/checks/alias.mjs
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
// 模块的源码只许引**自己的** `@/modules/<unit>/*`,不许出现任何指向外壳的 `@/`。
|
|
2
|
-
//
|
|
3
|
-
// 为什么:模块出包后用户手上没有外壳源码,`@/app/...` 这类别名只在**本仓的构建配置**里
|
|
4
|
-
// 解析得到 —— 装到别人那里就是构建期找不到文件(`check:standalone` 会把它变成红灯,
|
|
5
|
-
// 这条判据则直接指到源码那一行)。
|
|
6
|
-
//
|
|
7
|
-
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
8
|
-
// 打印与退出码归 `../check.mjs` 那一层。
|
|
9
|
-
|
|
10
1
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
11
2
|
import { join, relative } from 'node:path';
|
|
12
3
|
import { discoverModulePackages } from '../discover.mjs';
|
package/src/checks/arch.mjs
CHANGED
|
@@ -1,20 +1,3 @@
|
|
|
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
1
|
import { existsSync } from 'node:fs';
|
|
19
2
|
import { join } from 'node:path';
|
|
20
3
|
import { builtinSourceForms, runGuard } from '@arch-guard/core';
|
|
@@ -24,7 +7,6 @@ export const title = '模块架构(分层 / 边界 / 设计系统)';
|
|
|
24
7
|
|
|
25
8
|
const CONFIG_FILE = 'arch.config.mjs';
|
|
26
9
|
|
|
27
|
-
// "怎么上车"那句话只写一遍:skip 的提示与 fail 的提示都要它。
|
|
28
10
|
const HOW_TO_JOIN =
|
|
29
11
|
`在模块包里放一份 ${CONFIG_FILE},内容一行就够:\n` +
|
|
30
12
|
` import { moduleArch } from '@dsh-workbench/module-tools/arch';\n` +
|
|
@@ -36,7 +18,6 @@ const severityIndex = (rules) => new Map(rules.map((rule) => [rule.id, rule.seve
|
|
|
36
18
|
|
|
37
19
|
const severityOf = (finding, index) => finding.severity ?? index.get(finding.rule) ?? 'error';
|
|
38
20
|
|
|
39
|
-
/** 一条 finding 翻成一行人话:位置 → 说明 → 规则与修法。 */
|
|
40
21
|
function lineOf(name, finding) {
|
|
41
22
|
const where = `${name}: ${finding.file}:${String(finding.line)}`;
|
|
42
23
|
const hint = typeof finding.hint === 'string' && finding.hint !== '' ? ` —— ${finding.hint}` : '';
|
|
@@ -68,7 +49,7 @@ export async function run() {
|
|
|
68
49
|
const problems = [];
|
|
69
50
|
let files = 0;
|
|
70
51
|
let warnings = 0;
|
|
71
|
-
|
|
52
|
+
|
|
72
53
|
const ranRules = new Set();
|
|
73
54
|
|
|
74
55
|
for (const pkg of declared) {
|
|
@@ -77,8 +58,7 @@ export async function run() {
|
|
|
77
58
|
try {
|
|
78
59
|
result = await runGuard({
|
|
79
60
|
cwd: pkg.dir,
|
|
80
|
-
|
|
81
|
-
// `<cwd>/<绝对路径>`,再报"找不到配置文件")。
|
|
61
|
+
|
|
82
62
|
configPath: CONFIG_FILE,
|
|
83
63
|
sourceForms: builtinSourceForms,
|
|
84
64
|
cache: false,
|
|
@@ -94,8 +74,6 @@ export async function run() {
|
|
|
94
74
|
continue;
|
|
95
75
|
}
|
|
96
76
|
|
|
97
|
-
// `scopeFiles` 在 full 档是空的(实测)—— 解析过的文件数从 facts 缓存那份统计里拿
|
|
98
|
-
// (`cache: false` 时 `misses` 就是这一轮解析了几个文件,见 arch-guard 的 RunResult 注释)。
|
|
99
77
|
files += result.cache.misses;
|
|
100
78
|
for (const stat of result.stats) ranRules.add(stat.rule);
|
|
101
79
|
const index = severityIndex(result.config.rules);
|
|
@@ -108,7 +86,7 @@ export async function run() {
|
|
|
108
86
|
if (problems.length > 0) {
|
|
109
87
|
return {
|
|
110
88
|
status: 'fail',
|
|
111
|
-
|
|
89
|
+
|
|
112
90
|
notes: [`红了怎么修:先看位置与规则号,再决定"改代码"还是"在 ${CONFIG_FILE} 里开口子"。`],
|
|
113
91
|
problems,
|
|
114
92
|
};
|
package/src/checks/bundles.mjs
CHANGED
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
// 模块包的**运行时边界**(S2 第二块:门禁跟着自述清单走,覆盖到模块包)。
|
|
2
|
-
//
|
|
3
|
-
// 判据与外壳 `packages/workbench/scripts/check-runtimes.mjs` 是同一条:**能力,不是目录名**。
|
|
4
|
-
// 外壳那份走的是 vite 的 import 图;这里走**产物** —— 模块的两个入口各打成一个文件,
|
|
5
|
-
// 越界的依赖一定会被 inline 进来,所以:
|
|
6
|
-
//
|
|
7
|
-
// 浏览器产物里出现 node 内置 = 宿主传输层混进来了(例如某个视图 import 了 host/opencli)
|
|
8
|
-
// 宿主产物里出现 DOM / react = 浏览器侧混进来了
|
|
9
|
-
//
|
|
10
|
-
// 判据要挑**只有宿主侧才有的能力**:node 内置、`process.env`、`Buffer`。
|
|
11
|
-
// 这类错误在源码里看目录名是看不出来的(`model/blockers.ts` 这种纯函数浏览器当然可以用),
|
|
12
|
-
// 所以只能按能力判 —— 而产物就是能力的物证。
|
|
13
|
-
//
|
|
14
|
-
// 覆盖范围跟着**自述清单**走:凡 `package.json` 里声明了 `dsh.workbench` 的包都被检查,
|
|
15
|
-
// 新加一个模块不用改这个脚本。
|
|
16
|
-
//
|
|
17
|
-
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
18
|
-
// 打印与退出码归 `../check.mjs` 那一层。
|
|
19
|
-
|
|
20
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
21
2
|
import { join } from 'node:path';
|
|
22
3
|
import { inlinedShellRules } from '../css-shell.mjs';
|
|
@@ -36,13 +17,6 @@ const HOST_FORBIDDEN = [
|
|
|
36
17
|
|
|
37
18
|
export const title = '模块产物边界';
|
|
38
19
|
|
|
39
|
-
/**
|
|
40
|
-
* 模块产物里的夹具标记 —— 这几串一出现就说明"夹具又长回模块里了"。
|
|
41
|
-
*
|
|
42
|
-
* 为什么是"串"而不是"符号":模块产物是打包后的 JS,符号名会被内联/保留,用固定串更稳。
|
|
43
|
-
* `-mock'` 那一条故意只匹配**带引号的模块路径尾巴**(`from './cmdb-mock'` 这类),
|
|
44
|
-
* 避免误伤 vitest 的 `vi.mock`(那在测试里,不进产物)。
|
|
45
|
-
*/
|
|
46
20
|
const FIXTURE_MARKERS = [
|
|
47
21
|
'wbMock',
|
|
48
22
|
'wbCrash',
|
|
@@ -88,10 +62,7 @@ export async function run() {
|
|
|
88
62
|
);
|
|
89
63
|
}
|
|
90
64
|
}
|
|
91
|
-
|
|
92
|
-
// 把它整棵树挪出发布构建,kit 面也不再提供那几个成员。模块带着夹具只有坏处:
|
|
93
|
-
// 取不到旧成员会当场抛(2026-10-17 实测三个模块无条件渲染的包装组件就是这么崩的),
|
|
94
|
-
// 而且夹具数据会跟真取数抢同一条渲染路径。这条盯**产物**:出现任何一个标记就红。
|
|
65
|
+
|
|
95
66
|
if (fixtureMarkersIn(source).length > 0) {
|
|
96
67
|
const hits = fixtureMarkersIn(source);
|
|
97
68
|
problems.push(
|
|
@@ -100,10 +71,7 @@ export async function run() {
|
|
|
100
71
|
'模块自己的 vitest 把几态各喂一遍就够。',
|
|
101
72
|
);
|
|
102
73
|
}
|
|
103
|
-
|
|
104
|
-
// 取运行期值(`h` / `Fragment` / `useState` / 纯函数…)就会把整份组件 barrel 连 CSS 一起打进
|
|
105
|
-
// 自己的产物,注入后按顺序覆盖外壳那一份 —— 用户看到的是旧样式,而且外壳改了也不生效。
|
|
106
|
-
// 判断看复合选择器:`._hash_row.wb-iconButton` 这种「自己的类 + 外壳的槽类」是合规的,不算。
|
|
74
|
+
|
|
107
75
|
if (artifact.file === 'client.js') {
|
|
108
76
|
const inlined = inlinedShellRules(source);
|
|
109
77
|
if (inlined.length > 0) {
|