@dsh-workbench/module-tools 0.1.17 → 0.2.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/bin/dsh-wb-module.mjs +3 -2
- package/package.json +2 -2
- package/src/checks/cards.mjs +45 -2
- package/src/checks/contracts.mjs +93 -0
- package/src/checks/style.mjs +82 -32
- package/src/declared-tools.mjs +85 -0
- package/src/sync.mjs +51 -0
- package/template/README.md.tpl +10 -0
- package/template-diy/README.md.tpl +4 -1
package/bin/dsh-wb-module.mjs
CHANGED
|
@@ -5,14 +5,15 @@ import { check } from '../src/check.mjs';
|
|
|
5
5
|
import { design } from '../src/design.mjs';
|
|
6
6
|
import { init } from '../src/init.mjs';
|
|
7
7
|
import { publish } from '../src/publish.mjs';
|
|
8
|
+
import { sync } from '../src/sync.mjs';
|
|
8
9
|
|
|
9
10
|
const [command, ...rest] = process.argv.slice(2);
|
|
10
11
|
const cwd = process.env.DSH_WB_MODULE_CWD ?? process.cwd();
|
|
11
12
|
|
|
12
|
-
const commands = { build, check, init, design, publish };
|
|
13
|
+
const commands = { build, check, init, design, publish, sync };
|
|
13
14
|
|
|
14
15
|
if (!command || !commands[command]) {
|
|
15
|
-
console.error('用法:dsh-wb-module <build|check|init|design|publish> [--cwd <目录>]\n dsh-wb-module check [--only contracts|bundles|source|primitives|kit|cards|standalone|markers|style|alias]\n dsh-wb-module init <unit> [--dir <目录>] [--template minimal|diy] [--description <文字>]\n dsh-wb-module design <init|check|render|accept> # 三张表:填、查空格、生成 README、验收打勾\n dsh-wb-module publish # 发货前的清单:门禁 + npm pack 清单 + files 漏项(只 dry run)');
|
|
16
|
+
console.error('用法:dsh-wb-module <build|check|init|design|publish|sync> [--cwd <目录>]\n dsh-wb-module check [--only contracts|bundles|source|primitives|kit|cards|standalone|markers|style|alias]\n dsh-wb-module init <unit> [--dir <目录>] [--template minimal|diy] [--description <文字>]\n dsh-wb-module design <init|check|render|accept> # 三张表:填、查空格、生成 README、验收打勾\n dsh-wb-module sync # 按清单生成 src/declared-tools.ts(工具名只有清单一处写者)\n dsh-wb-module publish # 发货前的清单:门禁 + npm pack 清单 + files 漏项(只 dry run)');
|
|
16
17
|
process.exit(2);
|
|
17
18
|
}
|
|
18
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-workbench/module-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "模块包的构建与门禁工具:build(单文件 ESM 宿主侧 + 套 loader 壳的浏览器侧)、check(契约/产物边界/源码边界/卡片渲染)。模块包不再各带一份构建脚本。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"esbuild": "^0.28.2",
|
|
24
|
-
"@dsh-workbench/shared": "0.
|
|
24
|
+
"@dsh-workbench/shared": "0.2.0"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
27
|
"selfcheck": "node scripts/selfcheck.mjs"
|
package/src/checks/cards.mjs
CHANGED
|
@@ -218,11 +218,50 @@ export async function run() {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
const card = registered?.card;
|
|
221
|
+
// 清单说"没有卡片"(`"card": null`)与产物说"没有卡片"必须是同一件事 —— 两处说法不一致就是
|
|
222
|
+
// 声明与代码打架:登记了的那张卡没人会画(外壳只认清单),它只会一直烂在产物里。
|
|
223
|
+
// 这条判据的名字就是它防的事:gitops 当初为了哄旧判据挂了一张"我这张卡片就是说我没有卡片"的占位卡。
|
|
224
|
+
const declared = pkg.manifest.dsh?.workbench?.card ?? null;
|
|
225
|
+
const declaresCard =
|
|
226
|
+
declared !== null && typeof declared.titleKey === 'string' && declared.titleKey !== '';
|
|
227
|
+
if (!declaresCard) {
|
|
228
|
+
if (typeof card === 'function') {
|
|
229
|
+
problems.push(
|
|
230
|
+
`${name}: 清单写的是 "card": null,产物却登记了一张卡片 —— 两处说法不一致:` +
|
|
231
|
+
'要么把清单补成一张卡(titleKey + order),要么把这张卡从产物里去掉',
|
|
232
|
+
);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
rows.push(`${name}: 清单写的是 "card": null —— 这个模块没有卡片,产物里也确实没登记(跳过渲染判定)`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
221
238
|
if (typeof card !== 'function') {
|
|
222
|
-
problems.push(`${name}: apply
|
|
239
|
+
problems.push(`${name}: 清单写了 card.titleKey,apply 之后却没登记卡片(registerUnit 没拿到 card)`);
|
|
223
240
|
continue;
|
|
224
241
|
}
|
|
225
242
|
|
|
243
|
+
// 清单的 `card.titleKey` 是**面板那一格的标题**,它的文案住在模块自己交的字典里
|
|
244
|
+
// (`registerUnit` 那个面的 `messages`,`zh` / `en` 都必填)。键拼错、或字典漏了那一条 ⇒
|
|
245
|
+
// 面板上显示的就是原始键名(`demo.cardTitle`),而且一声不响 —— 这条判据盯它。
|
|
246
|
+
const titleKey = pkg.manifest.dsh?.workbench?.card?.titleKey;
|
|
247
|
+
if (typeof titleKey === 'string' && titleKey !== '') {
|
|
248
|
+
const messages = registered?.messages;
|
|
249
|
+
if (messages === undefined || messages === null || typeof messages !== 'object') {
|
|
250
|
+
problems.push(
|
|
251
|
+
`${name}: 清单的 card.titleKey「${titleKey}」没有落点 —— registerUnit 交的面里没有 messages,` +
|
|
252
|
+
'面板上那一格会显示原始键名',
|
|
253
|
+
);
|
|
254
|
+
} else {
|
|
255
|
+
const missing = ['zh', 'en'].filter((one) => typeof messages[one]?.[titleKey] !== 'string');
|
|
256
|
+
if (missing.length > 0) {
|
|
257
|
+
problems.push(
|
|
258
|
+
`${name}: 清单的 card.titleKey「${titleKey}」在模块字典的 ${missing.join(' / ')} 里没有 —— ` +
|
|
259
|
+
'面板上那一格会显示原始键名。清单写了 titleKey,字典就得有这一条(zh / en 都要)',
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
226
265
|
const props = {
|
|
227
266
|
world: { current: {} },
|
|
228
267
|
t: (key) => key,
|
|
@@ -288,7 +327,11 @@ export async function run() {
|
|
|
288
327
|
}
|
|
289
328
|
return {
|
|
290
329
|
status: 'pass',
|
|
291
|
-
notes: [
|
|
330
|
+
notes: [
|
|
331
|
+
`${rows.length} 个模块过了卡片这一关:有卡片的都在**空数据**下画得出来(不许抛)、清单的 titleKey 在字典里都有落点;` +
|
|
332
|
+
'清单写 "card": null 的,产物里也确实没登记卡片(声明与代码同一句话)',
|
|
333
|
+
...rows,
|
|
334
|
+
],
|
|
292
335
|
problems: [],
|
|
293
336
|
};
|
|
294
337
|
}
|
package/src/checks/contracts.mjs
CHANGED
|
@@ -24,6 +24,12 @@ import { createRequire } from 'node:module';
|
|
|
24
24
|
import { join, relative } from 'node:path';
|
|
25
25
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
26
26
|
import { readDeclaration } from '@dsh-workbench/shared/host';
|
|
27
|
+
import {
|
|
28
|
+
DECLARED_TOOLS_FILE,
|
|
29
|
+
declaredToolsProblemOf,
|
|
30
|
+
declaredToolsWithoutKey,
|
|
31
|
+
declaredToolsApplies,
|
|
32
|
+
} from '../declared-tools.mjs';
|
|
27
33
|
import { discoverPackages, workspaceRootOf } from '../discover.mjs';
|
|
28
34
|
import { SEED_SHORT } from '../seed.mjs';
|
|
29
35
|
|
|
@@ -191,6 +197,38 @@ function productsTextOf(dir) {
|
|
|
191
197
|
return text;
|
|
192
198
|
}
|
|
193
199
|
|
|
200
|
+
function sourceFilesOf(dir) {
|
|
201
|
+
const srcDir = join(dir, 'src');
|
|
202
|
+
if (!existsSync(srcDir)) return [];
|
|
203
|
+
const found = [];
|
|
204
|
+
const walk = (at) => {
|
|
205
|
+
for (const entry of readdirSync(at, { withFileTypes: true })) {
|
|
206
|
+
const full = join(at, entry.name);
|
|
207
|
+
if (entry.isDirectory()) {
|
|
208
|
+
if (entry.name === '__tests__' || entry.name === 'node_modules') continue;
|
|
209
|
+
walk(full);
|
|
210
|
+
} else if (/\.tsx?$/.test(entry.name) && !/\.test\.tsx?$/.test(entry.name)) {
|
|
211
|
+
found.push(full);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
walk(srcDir);
|
|
216
|
+
return found;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// `plugin: '<字面量>'` 与同一行里 `?? '<字面量>'` 的兜底:都该是**清单里声明过**的那个插件名。
|
|
220
|
+
// 插件名只该有一处真相(能拿到 `deps.manifest` 的地方就用它),但模块级代码拿不到 manifest ⇒ 只能写字面量。
|
|
221
|
+
// 这条判据拦的是"改了清单没改代码"(或反过来):那样外壳会按错的名字去装 / 去探那个插件,而用户只看到
|
|
222
|
+
// "缺命令"这种没头没尾的状态。只查 `plugin:` 那几行,别的字面量不管(不猜)。
|
|
223
|
+
function pluginLiteralsOf(line) {
|
|
224
|
+
if (!/\bplugin\s*:/.test(line)) return [];
|
|
225
|
+
const found = [];
|
|
226
|
+
const direct = /plugin\s*:\s*['"]([^'"]+)['"]/.exec(line);
|
|
227
|
+
if (direct !== null) found.push(direct[1]);
|
|
228
|
+
for (const match of line.matchAll(/\?\?\s*['"]([^'"]+)['"]/g)) found.push(match[1]);
|
|
229
|
+
return found;
|
|
230
|
+
}
|
|
231
|
+
|
|
194
232
|
function hostProductOf(dir) {
|
|
195
233
|
try {
|
|
196
234
|
return readFileSync(join(dir, 'dist/host.js'), 'utf8');
|
|
@@ -455,6 +493,40 @@ export async function run() {
|
|
|
455
493
|
}
|
|
456
494
|
}
|
|
457
495
|
|
|
496
|
+
// 清单里的工具名与「代码里那份名字」的关系(ADR-0038 的 `tools[].key` 一节):
|
|
497
|
+
// 给了 key 就走生成物那一档 —— `src/declared-tools.ts` 必须存在、且与清单一致(过期 = 红),
|
|
498
|
+
// 并且源码里不许再有工具名的字面量(名字只写在清单里,代码引用 `DECLARED_TOOLS.<key>.name`,
|
|
499
|
+
// 键写错是编译错)。一个 key 都没写的模块不适用(那是不用生成物那一档)。
|
|
500
|
+
const declaredProblem = declaredToolsProblemOf(pkg.dir, dsh.workbench);
|
|
501
|
+
if (declaredProblem !== undefined) {
|
|
502
|
+
problems.push(`${name}: ${declaredProblem}`);
|
|
503
|
+
} else if (declaredToolsApplies(dsh.workbench)) {
|
|
504
|
+
const bare = declaredToolsWithoutKey(dsh.workbench);
|
|
505
|
+
const literals = [];
|
|
506
|
+
const generated = join(pkg.dir, DECLARED_TOOLS_FILE);
|
|
507
|
+
for (const file of sourceFilesOf(pkg.dir)) {
|
|
508
|
+
if (file === generated) continue;
|
|
509
|
+
const text = readFileSync(file, 'utf8');
|
|
510
|
+
for (const tool of dsh.workbench.tools) {
|
|
511
|
+
const one = `'${tool.name}'`;
|
|
512
|
+
const two = `"${tool.name}"`;
|
|
513
|
+
if (text.includes(one) || text.includes(two)) {
|
|
514
|
+
literals.push(`${relative(pkg.dir, file)} 里的 ${tool.name}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (literals.length > 0) {
|
|
519
|
+
problems.push(
|
|
520
|
+
`${name}: 工具名的字面量又写回了源码 —— ${literals.join(' / ')}。` +
|
|
521
|
+
`清单里已经有 key,代码该引用生成物(\`DECLARED_TOOLS.<key>.name\`):` +
|
|
522
|
+
`名字只有清单一处写者,写两遍就会漂,而漂了没有任何编译错`,
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
if (bare.length > 0) {
|
|
526
|
+
problems.push(`${name}: 清单里 ${bare.join(' / ')} 没有 key`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
458
530
|
for (const face of ['host', 'client']) {
|
|
459
531
|
const rel = dsh.workbenchFaces?.[face];
|
|
460
532
|
if (typeof rel !== 'string') {
|
|
@@ -552,6 +624,27 @@ export async function run() {
|
|
|
552
624
|
);
|
|
553
625
|
});
|
|
554
626
|
|
|
627
|
+
const declaredPlugins = new Set(
|
|
628
|
+
(env.plugins ?? []).map((one) => text(one?.name)).filter((one) => one !== ''),
|
|
629
|
+
);
|
|
630
|
+
if (declaredPlugins.size > 0) {
|
|
631
|
+
for (const file of sourceFilesOf(pkg.dir)) {
|
|
632
|
+
readFileSync(file, 'utf8')
|
|
633
|
+
.split('\n')
|
|
634
|
+
.forEach((line, index) => {
|
|
635
|
+
for (const literal of pluginLiteralsOf(line)) {
|
|
636
|
+
if (declaredPlugins.has(literal)) continue;
|
|
637
|
+
problems.push(
|
|
638
|
+
`${name}: ${relative(pkg.dir, file)}:${index + 1} 里的插件名「${literal}」不在清单的 ` +
|
|
639
|
+
`env.plugins[] 里(清单里是 ${[...declaredPlugins].join(' / ')})—— 改了清单没改代码` +
|
|
640
|
+
'(或反过来)会让外壳按错的名字去装 / 去探那个插件。插件名只有一处真相:' +
|
|
641
|
+
'能拿到 deps.manifest 的地方就用它,拿不到的地方至少与清单保持一致',
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
555
648
|
if ((env.libs ?? []).length > 0 && !products.includes('libInstallCommand')) {
|
|
556
649
|
problems.push(
|
|
557
650
|
`${name}: 声明了 env.libs 却没用 libInstallCommand(@dsh-workbench/shared/host 里现成的)—— ` +
|
package/src/checks/style.mjs
CHANGED
|
@@ -22,17 +22,60 @@ const CLASS_TOKEN = /(^|[^-\w])\.?(wb-[a-zA-Z0-9-]+)/g;
|
|
|
22
22
|
// 契约 = 外壳**所有**声明了 `wb-*` 的样式表,不只是那份全局 `board.css`:
|
|
23
23
|
// ADR-0029 要求组件的样式跟组件走,所以 `wb-card` / `wb-rows` / `wb-note` 这些已经搬进
|
|
24
24
|
// `packages/shared/src/components/*.css`(谁渲染谁拥有)。契约跟着搬,模块借用它们才不误报。
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
// 契约按**两种安装形态**解析,两边必须给同一份名单(实测过差别,见 spec §24.4):
|
|
26
|
+
// · 在外壳仓里跑:源码就在隔壁(`workbench/src/shared/styles/board.css` + `shared/src/components/*.css`);
|
|
27
|
+
// · 在模块仓里跑:只能从**已装的外壳**读 —— `@dsh-workbench/core/dist/board.css`(构建时随包出货的那份
|
|
28
|
+
// 契约副本,见 `packages/workbench/scripts/build.mjs`)与 `@dsh-workbench/shared/src/components/*.css`
|
|
29
|
+
// (shared 本来就发源码)。
|
|
30
|
+
// 从前只写死了相对 module-tools 自己的路径 ⇒ 装进模块仓后 `board.css` 找不到,契约退化成"只有 shared 组件
|
|
31
|
+
// 那一份",于是 `board.css` 里的合法借用(例如 `wb-iconButton`)被**误报**;而且借用检查在契约缺失时
|
|
32
|
+
// 会 `contract.has` 抛异常("契约文件不在"那条 note 根本到不了)。
|
|
33
|
+
const SOURCE_BOARD = fileURLToPath(new URL('../../../workbench/src/shared/styles/board.css', import.meta.url));
|
|
34
|
+
const SOURCE_COMPONENTS = fileURLToPath(new URL('../../../shared/src/components', import.meta.url));
|
|
35
|
+
const INSTALLED_BOARD = join('node_modules', '@dsh-workbench', 'core', 'dist', 'board.css');
|
|
36
|
+
const INSTALLED_COMPONENTS = join('node_modules', '@dsh-workbench', 'shared', 'src', 'components');
|
|
37
|
+
|
|
38
|
+
function installedUnder(dir, tail) {
|
|
39
|
+
let at = resolve(dir);
|
|
40
|
+
for (;;) {
|
|
41
|
+
const candidate = join(at, tail);
|
|
42
|
+
if (existsSync(candidate)) return candidate;
|
|
43
|
+
const parent = dirname(at);
|
|
44
|
+
if (parent === at) return undefined;
|
|
45
|
+
at = parent;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function cssFilesIn(dir) {
|
|
50
|
+
try {
|
|
51
|
+
return readdirSync(dir)
|
|
52
|
+
.filter((name) => name.endsWith('.css'))
|
|
53
|
+
.map((name) => join(dir, name));
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function contractFilesFor(moduleDir) {
|
|
60
|
+
const board = existsSync(SOURCE_BOARD) ? SOURCE_BOARD : installedUnder(moduleDir, INSTALLED_BOARD);
|
|
61
|
+
const components = existsSync(SOURCE_COMPONENTS)
|
|
62
|
+
? SOURCE_COMPONENTS
|
|
63
|
+
: installedUnder(moduleDir, INSTALLED_COMPONENTS);
|
|
64
|
+
return [
|
|
65
|
+
...(board === undefined ? [] : [board]),
|
|
66
|
+
...(components === undefined ? [] : cssFilesIn(components)),
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function contractOf(moduleDir) {
|
|
71
|
+
const files = contractFilesFor(moduleDir);
|
|
72
|
+
if (files.length === 0) return null;
|
|
73
|
+
return new Set(
|
|
74
|
+
files.flatMap((file) =>
|
|
75
|
+
Array.from(readFileSync(file, 'utf8').matchAll(CLASS_TOKEN), (match) => match[2]),
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
36
79
|
|
|
37
80
|
export const title = '样式归属';
|
|
38
81
|
|
|
@@ -51,19 +94,13 @@ function isTestSource(file) {
|
|
|
51
94
|
}
|
|
52
95
|
|
|
53
96
|
export async function run() {
|
|
54
|
-
const contract = CONTRACT_FILES.some((file) => existsSync(file))
|
|
55
|
-
? new Set(
|
|
56
|
-
CONTRACT_FILES.filter((file) => existsSync(file)).flatMap((file) =>
|
|
57
|
-
Array.from(readFileSync(file, 'utf8').matchAll(CLASS_TOKEN), (match) => match[2]),
|
|
58
|
-
),
|
|
59
|
-
)
|
|
60
|
-
: null;
|
|
61
|
-
|
|
62
97
|
const problems = [];
|
|
63
98
|
const borrowed = [];
|
|
64
99
|
const anchors = [];
|
|
65
100
|
const modules = discoverModulePackages({ root: process.cwd() });
|
|
101
|
+
const contracts = new Map(modules.map((pkg) => [pkg.dir, contractOf(pkg.dir)]));
|
|
66
102
|
for (const pkg of modules) {
|
|
103
|
+
const contract = contracts.get(pkg.dir);
|
|
67
104
|
for (const file of walk(pkg.dir)) {
|
|
68
105
|
if (!/\.(css|ts|tsx)$/.test(file)) continue;
|
|
69
106
|
let text;
|
|
@@ -84,15 +121,14 @@ export async function run() {
|
|
|
84
121
|
|
|
85
122
|
if (isTestSource(file)) continue;
|
|
86
123
|
if (!/\.tsx?$/.test(file)) continue;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
124
|
+
lines.forEach((line, index) => {
|
|
125
|
+
if (!BARE_ANCHOR.test(line)) return;
|
|
126
|
+
anchors.push(
|
|
127
|
+
`${pkg.manifest.name}: ${relative(pkg.dir, file)}:${index + 1} 手写了裸 <a> —— ` +
|
|
128
|
+
`外链一律用 kit 的 Link(新窗口、noopener、自带那个外链小箭头;样式只有这一份):${line.trim().slice(0, 80)}`,
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
if (contract === null) continue;
|
|
96
132
|
lines.forEach((line, index) => {
|
|
97
133
|
for (const match of line.matchAll(CLASS_TOKEN)) {
|
|
98
134
|
const name = match[2];
|
|
@@ -149,7 +185,8 @@ export async function run() {
|
|
|
149
185
|
if (borrowed.length > 0) {
|
|
150
186
|
notes.push(
|
|
151
187
|
`${borrowed.length} 处是「模块借用了契约里没有的 wb-* 类」:(契约是本仓 board.css:模块只许借它里面有的类。` +
|
|
152
|
-
'没有的那个要么是拼错/改了名漏改,要么本该进 board.css ——
|
|
188
|
+
'没有的那个要么是拼错/改了名漏改,要么本该进 board.css —— 空类名在真机上就是没样式,而且没人会报错。' +
|
|
189
|
+
'名单是**外壳出货的那份**(`@dsh-workbench/core/dist/board.css` + 外壳组件自己的样式表))',
|
|
153
190
|
);
|
|
154
191
|
}
|
|
155
192
|
if (anchors.length > 0) {
|
|
@@ -170,10 +207,23 @@ export async function run() {
|
|
|
170
207
|
};
|
|
171
208
|
}
|
|
172
209
|
|
|
210
|
+
const noContract = modules
|
|
211
|
+
.filter((pkg) => contracts.get(pkg.dir) === null)
|
|
212
|
+
.map((pkg) => pkg.manifest.name);
|
|
213
|
+
if (noContract.length === modules.length) {
|
|
214
|
+
return {
|
|
215
|
+
status: 'skip',
|
|
216
|
+
notes: [
|
|
217
|
+
'找不到外壳出货的那份契约(`@dsh-workbench/core/dist/board.css` 与 `@dsh-workbench/shared` 的组件样式表都不在)' +
|
|
218
|
+
'—— 模块借的 wb-* 类这一项这次一条都没查,不是"没问题"',
|
|
219
|
+
],
|
|
220
|
+
problems: [],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
173
223
|
const contractNote =
|
|
174
|
-
|
|
175
|
-
? '
|
|
176
|
-
:
|
|
224
|
+
noContract.length === 0
|
|
225
|
+
? '借用的类都在契约里(外壳出货的 board.css + 外壳组件自己的样式表)'
|
|
226
|
+
: `${noContract.join(' / ')} 那一边找不到外壳的契约,借用这一项没查`;
|
|
177
227
|
return {
|
|
178
228
|
status: 'pass',
|
|
179
229
|
notes: [`${modules.length} 个模块都没有定义外壳前缀(wb-*)的 CSS 规则;${contractNote}`],
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// 工具名只有**一处写者**:清单(`dsh.workbench.tools[].name`)。
|
|
2
|
+
//
|
|
3
|
+
// 代码里注册一个工具必须报它的名字,所以从前那份字面量在清单与代码里各写一遍 —— 只能靠判据对账
|
|
4
|
+
// (`check:contracts` 的 reconcileTools 就是这么干的)。`tools[].key` + `dsh-wb-module sync` 把
|
|
5
|
+
// 代码那一半**生成**出来:
|
|
6
|
+
//
|
|
7
|
+
// dsh-wb-module sync → src/declared-tools.ts
|
|
8
|
+
// export const DECLARED_TOOLS = {
|
|
9
|
+
// sync: { name: 'workbench_argocd_sync', writes: true },
|
|
10
|
+
// } as const;
|
|
11
|
+
//
|
|
12
|
+
// 于是:改名只动清单一处;代码引用的是**键**(`DECLARED_TOOLS.sync.name`),键写错是编译错;
|
|
13
|
+
// 生成物落后于清单由判据当场判红。
|
|
14
|
+
//
|
|
15
|
+
// 方向是**单向**的:绝不回写 `package.json`(ADR-0038 §2.5 已经拒了"生成式回写":`package.json`
|
|
16
|
+
// 有两个写者(人与生成器)就会静默覆盖)。
|
|
17
|
+
//
|
|
18
|
+
// 输出与比对都按"各家 prettier 都可能重排"来设计:字面量用单引号(本生态的 TS 风格),
|
|
19
|
+
// 比对时**忽略空白、把两种引号当同一件事** —— 重排不算过期,值变了才算。
|
|
20
|
+
|
|
21
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
|
|
24
|
+
export const DECLARED_TOOLS_FILE = 'src/declared-tools.ts';
|
|
25
|
+
|
|
26
|
+
export function declaredToolsPathOf(dir) {
|
|
27
|
+
return join(dir, DECLARED_TOOLS_FILE);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toolsOf(raw) {
|
|
31
|
+
return Array.isArray(raw?.tools) ? raw.tools : [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function keyOf(tool) {
|
|
35
|
+
return typeof tool?.key === 'string' && tool.key.trim() !== '' ? tool.key.trim() : '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 一个 key 都没写的模块不适用(那是不用生成物那一档,判据也不找它)。 */
|
|
39
|
+
export function declaredToolsApplies(raw) {
|
|
40
|
+
return toolsOf(raw).some((tool) => keyOf(tool) !== '');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 还没写 key 的那些工具(有 key 就得每个都有:半写会让生成物少几条,而代码里还在写字面量)。 */
|
|
44
|
+
export function declaredToolsWithoutKey(raw) {
|
|
45
|
+
return toolsOf(raw)
|
|
46
|
+
.filter((tool) => keyOf(tool) === '')
|
|
47
|
+
.map((tool) => JSON.stringify(tool?.name));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 生成物的正文。**不给注释**:它落在模块的 `src/**`(零注释那一条管到这里),改它无效、过期会红。 */
|
|
51
|
+
export function renderDeclaredTools(raw) {
|
|
52
|
+
const rows = toolsOf(raw).map(
|
|
53
|
+
(tool) =>
|
|
54
|
+
` ${keyOf(tool)}: { name: '${tool.name}', writes: ${tool.writes === true} },`,
|
|
55
|
+
);
|
|
56
|
+
return [
|
|
57
|
+
'export const DECLARED_TOOLS = {',
|
|
58
|
+
...rows,
|
|
59
|
+
'} as const;',
|
|
60
|
+
'',
|
|
61
|
+
'export type DeclaredToolKey = keyof typeof DECLARED_TOOLS;',
|
|
62
|
+
'',
|
|
63
|
+
].join('\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 生成物与清单对不上的原因(`undefined` = 新鲜,或不适用)。 */
|
|
67
|
+
export function declaredToolsProblemOf(dir, raw) {
|
|
68
|
+
if (!declaredToolsApplies(raw)) return undefined;
|
|
69
|
+
const missing = declaredToolsWithoutKey(raw);
|
|
70
|
+
if (missing.length > 0) {
|
|
71
|
+
return `清单里 ${missing.join(' / ')} 没有 key —— 有 key 就走生成物那一档,那就每个工具都得有一个`;
|
|
72
|
+
}
|
|
73
|
+
const at = declaredToolsPathOf(dir);
|
|
74
|
+
if (!existsSync(at)) {
|
|
75
|
+
return `${DECLARED_TOOLS_FILE} 不在 —— 清单里给了 key 就该有生成物,跑 \`dsh-wb-module sync\` 生成`;
|
|
76
|
+
}
|
|
77
|
+
const strip = (text) => text.replace(/\s+/g, '').replace(/"/g, "'");
|
|
78
|
+
if (strip(readFileSync(at, 'utf8')) !== strip(renderDeclaredTools(raw))) {
|
|
79
|
+
return (
|
|
80
|
+
`${DECLARED_TOOLS_FILE} 与清单对不上(生成物过期,或有人手改了它)—— ` +
|
|
81
|
+
'跑 `dsh-wb-module sync` 刷新。别手改:它是生成的'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
package/src/sync.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// `dsh-wb-module sync`:按清单生成 `src/declared-tools.ts`(单向,清单 → 代码)。
|
|
2
|
+
//
|
|
3
|
+
// 为什么需要它、生成物长什么样、为什么绝不回写 package.json:见 `declared-tools.mjs` 开头与
|
|
4
|
+
// ADR-0038 的 `tools[].key` 一节。
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import {
|
|
9
|
+
DECLARED_TOOLS_FILE,
|
|
10
|
+
declaredToolsApplies,
|
|
11
|
+
declaredToolsPathOf,
|
|
12
|
+
declaredToolsProblemOf,
|
|
13
|
+
declaredToolsWithoutKey,
|
|
14
|
+
renderDeclaredTools,
|
|
15
|
+
} from './declared-tools.mjs';
|
|
16
|
+
|
|
17
|
+
export async function sync({ root }) {
|
|
18
|
+
const manifestPath = join(root, 'package.json');
|
|
19
|
+
if (!existsSync(manifestPath)) throw new Error(`${manifestPath} 不在 —— sync 要在模块包目录里跑`);
|
|
20
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
21
|
+
const raw = manifest.dsh?.workbench;
|
|
22
|
+
const unit = typeof raw?.unit === 'string' ? raw.unit : manifest.name;
|
|
23
|
+
if (raw === undefined) throw new Error(`${unit}: package.json 里没有 dsh.workbench —— 这不是模块包`);
|
|
24
|
+
|
|
25
|
+
const tools = Array.isArray(raw.tools) ? raw.tools : [];
|
|
26
|
+
if (tools.length === 0) {
|
|
27
|
+
console.log(`${unit}: 清单里没有工具,不用生成物`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const missing = declaredToolsWithoutKey(raw);
|
|
32
|
+
if (missing.length > 0) {
|
|
33
|
+
const hint = declaredToolsApplies(raw)
|
|
34
|
+
? '有 key 就走生成物那一档,那就每个工具都得有一个'
|
|
35
|
+
: '给每个工具加一个 lowerCamelCase 的 key,代码里就不再写工具名的字面量';
|
|
36
|
+
throw new Error(
|
|
37
|
+
`${unit}: 清单里 ${missing.join(' / ')} 没有 key —— ${hint}。例如:\n` +
|
|
38
|
+
`${tools.map((tool) => ` "key": "…", // ${JSON.stringify(tool.name)}`).join('\n')}\n` +
|
|
39
|
+
'(key 是代码里那个短名:`DECLARED_TOOLS.<key>.name`。见 ADR-0038 的 `tools[].key` 一节)',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const at = declaredToolsPathOf(root);
|
|
44
|
+
const had = existsSync(at);
|
|
45
|
+
const stale = declaredToolsProblemOf(root, raw);
|
|
46
|
+
writeFileSync(at, renderDeclaredTools(raw));
|
|
47
|
+
console.log(
|
|
48
|
+
`${unit}: 写了 ${DECLARED_TOOLS_FILE}(${tools.length} 个工具,按清单生成)` +
|
|
49
|
+
(had && stale !== undefined ? ' —— 原来那份和清单对不上,已刷新' : ''),
|
|
50
|
+
);
|
|
51
|
+
}
|
package/template/README.md.tpl
CHANGED
|
@@ -23,6 +23,16 @@ pnpm selfcheck # 构建 + 把 client.js 当浏览器加载器跑一遍,看
|
|
|
23
23
|
dsh plugin --profile workbench add <本目录>
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
要加能力:`package.json` 的 `dsh.workbench.tools[]` 里每个工具写 `name` / `writes` / `key`
|
|
27
|
+
(`key` 是代码里那个短名,lowerCamelCase),然后跑一次
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
dsh-wb-module sync # 按清单生成 src/declared-tools.ts
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
代码里写 `DECLARED_TOOLS.<key>.name` —— **工具名的字面量只留在清单这一处**。生成物过期、半写 `key`、
|
|
34
|
+
或源码里又写了一遍工具名字面量,`check` 都会红(`sync` 会告诉你缺哪个工具的 `key`)。
|
|
35
|
+
|
|
26
36
|
面板只读、写入只走 agent 工具;共享代码从 `@dsh-workbench/shared` 拿,
|
|
27
37
|
单例机制(hooks / 容器组件 / 读缝)只能从 `@dsh-workbench/core/client` 的 kit 拿。
|
|
28
38
|
dsh 已有的组件与样式**先从 kit 拿**,别从 DOM + CSS 重画。
|
|
@@ -155,8 +155,11 @@ GET /api/__UNIT__/overview?path=<绝对路径>&limit=<1..50>
|
|
|
155
155
|
|
|
156
156
|
### 会写:清单里标出来 + 认领审批
|
|
157
157
|
|
|
158
|
+
> 每个工具都写一个 `key`(代码里那个短名,lowerCamelCase),然后 `dsh-wb-module sync` 生成
|
|
159
|
+
> `src/declared-tools.ts`:代码写 `DECLARED_TOOLS.rebuild.name` —— 工具名的字面量只留在清单里。
|
|
160
|
+
|
|
158
161
|
```ts
|
|
159
|
-
tools: [{ name: 'workbench___UNIT___rebuild', writes: true }],
|
|
162
|
+
tools: [{ name: 'workbench___UNIT___rebuild', writes: true, key: 'rebuild' }],
|
|
160
163
|
|
|
161
164
|
needsApproval: (call) => ({
|
|
162
165
|
kind: 'ask',
|