@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/checks/source.mjs
CHANGED
|
@@ -1,30 +1,139 @@
|
|
|
1
|
-
// 模块包的**源码边界**(S2 第三块:门禁跟着自述清单走到模块包)。
|
|
2
|
-
//
|
|
3
|
-
// 外壳那份 `arch/rules/units.mjs` 是按**路径**发现套件的(`src/modules/<id>/unit.ts`),
|
|
4
|
-
// 模块搬出 `src/` 之后它照跑但不报。那几条规则守的多是「外壳不许认识套件」——对模块包没意义;
|
|
5
|
-
// 模块包自己要守的是另外几条,写在下面。判据全部来自源码里的 import 说明符,覆盖范围跟着
|
|
6
|
-
// `dsh.workbench` 自述清单走(新加模块不用改这里)。
|
|
7
|
-
//
|
|
8
|
-
// 关于 dsh 的包:「哪一半」**不靠目录名猜**,而是从两个入口(`dsh.workbenchFaces.host` /
|
|
9
|
-
// `dsh.workbenchFaces.client`)做一次本包内可达性分析。种子词(模块表里天生就有的那几个)在
|
|
10
|
-
// **只被客户端入口可达**的文件里可以用(ADR-0028);宿主侧一个都不许碰 —— 那些包只在浏览器侧存在。
|
|
11
|
-
//
|
|
12
|
-
// 这两个入口是 dsh.workbenchFaces 的**唯一**用途(工具链字段,不是协议的一部分,见 ADR-0038 §7 第 7 条):
|
|
13
|
-
// 装机预检与外壳都不读它。所以这里必须拿**源码**入口 —— 从产物 dist/host.js 反推不出是哪一份源码
|
|
14
|
-
// 把 dsh 的包拖进来的。
|
|
15
|
-
//
|
|
16
|
-
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
17
|
-
// 打印与退出码归 `../check.mjs` 那一层。
|
|
18
|
-
|
|
19
1
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import ts from 'typescript';
|
|
20
3
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
21
4
|
import { discoverModulePackages } from '../discover.mjs';
|
|
22
5
|
import { isSeedSpecifier } from '../seed.mjs';
|
|
6
|
+
import { LIB_CAPABILITIES, importsPreferred } from '@dsh-workbench/shared/host';
|
|
23
7
|
|
|
24
|
-
// 静态 `from '…'`、副作用 `import '…'`、动态 `import('…')` —— 动态那条不能漏,
|
|
25
|
-
// 模块入口常见写法是 `await import('./views/…')`,漏了就会把可达文件判成"死代码"。
|
|
26
8
|
const IMPORT = /from\s*'([^']+)'|import\s*\(?\s*'([^']+)'/g;
|
|
27
9
|
|
|
10
|
+
const SHARED_FROM =
|
|
11
|
+
/(?:^|[;}\s])(import|export)\s+(type\s+)?([^'";]{0,600}?)\s*from\s*['"](@dsh-workbench\/shared(?:\/host)?)['"]/g;
|
|
12
|
+
|
|
13
|
+
const SHARED_DYNAMIC = /import\s*\(\s*['"](@dsh-workbench\/shared(?:\/host)?)['"]/g;
|
|
14
|
+
|
|
15
|
+
function typeOnlyOf(keyword, clause) {
|
|
16
|
+
if (keyword !== undefined) return true;
|
|
17
|
+
const braces = /\{([\s\S]*)\}/.exec(clause);
|
|
18
|
+
if (braces === null) return false;
|
|
19
|
+
if (clause.slice(0, braces.index).replace(/[,\s]/g, '') !== '') return false;
|
|
20
|
+
const parts = braces[1]
|
|
21
|
+
.split(',')
|
|
22
|
+
.map((one) => one.trim())
|
|
23
|
+
.filter((one) => one !== '');
|
|
24
|
+
return parts.length > 0 && parts.every((one) => one.startsWith('type '));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const SIDE_TEXT = {
|
|
28
|
+
client: '客户端侧可达',
|
|
29
|
+
host: '宿主侧可达',
|
|
30
|
+
both: '两个入口都到得了',
|
|
31
|
+
orphan: '两个入口都到不了(可能是死代码)',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const COMMENT_EXT = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.css']);
|
|
35
|
+
const COMMENT_DIRECTIVE = /^(?:\/\/|\/\*)\s*(?:!?\s*@(?:license|preserve)\b|eslint-(?:disable|enable)\b|@ts-(?:ignore|expect-error|nocheck|check)\b|@vitest-|@jest-|@flow\b|@jsx|prettier-ignore\b|global\b|exported\b|c8\b|v8\b|istanbul\b|#\s*sourceMappingURL\b|@__PURE__|#__PURE__|\/?\s*<reference\b)/u;
|
|
36
|
+
|
|
37
|
+
function codeFiles(dir, acc = []) {
|
|
38
|
+
if (!existsSync(dir)) return acc;
|
|
39
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
40
|
+
if (entry.name === 'node_modules' || entry.name === 'dist') continue;
|
|
41
|
+
const full = join(dir, entry.name);
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
codeFiles(full, acc);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const ext = entry.name.slice(entry.name.lastIndexOf('.'));
|
|
47
|
+
if (COMMENT_EXT.has(ext)) acc.push(full);
|
|
48
|
+
}
|
|
49
|
+
return acc;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tsCommentRanges(text) {
|
|
53
|
+
const source = ts.createSourceFile('scan.tsx', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
54
|
+
const found = new Map();
|
|
55
|
+
const add = (ranges) => {
|
|
56
|
+
for (const range of ranges ?? []) found.set(range.pos, range);
|
|
57
|
+
};
|
|
58
|
+
const visit = (node) => {
|
|
59
|
+
add(ts.getLeadingCommentRanges(text, node.getFullStart()));
|
|
60
|
+
add(ts.getTrailingCommentRanges(text, node.getEnd()));
|
|
61
|
+
ts.forEachChild(node, visit);
|
|
62
|
+
};
|
|
63
|
+
visit(source);
|
|
64
|
+
add(ts.getLeadingCommentRanges(text, 0));
|
|
65
|
+
add(ts.getTrailingCommentRanges(text, text.length));
|
|
66
|
+
return [...found.values()].sort((a, b) => a.pos - b.pos);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function cssCommentRanges(text) {
|
|
70
|
+
const found = [];
|
|
71
|
+
let at = 0;
|
|
72
|
+
let quote = null;
|
|
73
|
+
while (at < text.length) {
|
|
74
|
+
const ch = text[at];
|
|
75
|
+
if (quote !== null) {
|
|
76
|
+
if (ch === '\\') at += 2;
|
|
77
|
+
else {
|
|
78
|
+
if (ch === quote) quote = null;
|
|
79
|
+
at += 1;
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (ch === '"' || ch === "'") {
|
|
84
|
+
quote = ch;
|
|
85
|
+
at += 1;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (ch === '/' && text[at + 1] === '*') {
|
|
89
|
+
const end = text.indexOf('*/', at + 2);
|
|
90
|
+
if (end === -1) break;
|
|
91
|
+
found.push({ pos: at, end: end + 2 });
|
|
92
|
+
at = end + 2;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
at += 1;
|
|
96
|
+
}
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function commentViolationsOf(text, { css = false } = {}) {
|
|
101
|
+
const ranges = css ? cssCommentRanges(text) : tsCommentRanges(text);
|
|
102
|
+
const bad = [];
|
|
103
|
+
for (const range of ranges) {
|
|
104
|
+
const raw = text.slice(range.pos, range.end);
|
|
105
|
+
if (!css && COMMENT_DIRECTIVE.test(raw)) continue;
|
|
106
|
+
bad.push(text.slice(0, range.pos).split('\n').length);
|
|
107
|
+
}
|
|
108
|
+
return bad;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function sharedProblem(name, rel, specifier, side, dynamic) {
|
|
112
|
+
const what = dynamic ? `动态 import 了 ${specifier}` : `从 ${specifier} 取了运行值`;
|
|
113
|
+
if (specifier === '@dsh-workbench/shared') {
|
|
114
|
+
const fix =
|
|
115
|
+
side === 'host'
|
|
116
|
+
? '宿主侧改从 @dsh-workbench/shared/host 引'
|
|
117
|
+
: side === 'client'
|
|
118
|
+
? '浏览器侧改从 kit(@dsh-workbench/core/client)引;kit 里没有的,先看它是不是该补进 kit'
|
|
119
|
+
: side === 'both'
|
|
120
|
+
? '这个文件两个入口都到得了:宿主要的走 /host、浏览器要的走 kit —— 两边的运行值挤在一个文件里就拆开'
|
|
121
|
+
: '这个文件两个入口都到不了(可能是死代码),证明不了它在哪一侧;挪进真正的面再引';
|
|
122
|
+
return (
|
|
123
|
+
`${name}: ${rel} 里${what} —— 根入口是浏览器 barrel` +
|
|
124
|
+
`(组件 + 它们自己的样式),两侧都不该从这里取运行值:浏览器侧会打进第二份实例、宿主侧会让 ` +
|
|
125
|
+
`dist/host.js 在 Node 里 import 不进来(实测:一路拉到 dsh 的浏览器件)。${fix}。` +
|
|
126
|
+
`只取类型就写 \`import type\`(编译期擦除、两侧都行)`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (side === 'host') return undefined;
|
|
130
|
+
return (
|
|
131
|
+
`${name}: ${rel} 里${what} —— 宿主入口里有 node:crypto / node:fs,` +
|
|
132
|
+
`只有宿主侧能引(这个文件${SIDE_TEXT[side]});浏览器产物装不下它。` +
|
|
133
|
+
`浏览器侧要的纯函数走 kit(@dsh-workbench/core/client)或自己写一个;只取类型就写 \`import type\``
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
28
137
|
function sourceFiles(dir, acc = []) {
|
|
29
138
|
for (const entry of readdirSync(dir)) {
|
|
30
139
|
const full = join(dir, entry);
|
|
@@ -38,11 +147,6 @@ function sourceFiles(dir, acc = []) {
|
|
|
38
147
|
return acc;
|
|
39
148
|
}
|
|
40
149
|
|
|
41
|
-
/**
|
|
42
|
-
* 把一条 import 说明符解析成本包内的文件;解析不到就是包说明符(不外追)。
|
|
43
|
-
* 别名两条(与 `build.mjs` 的 aliasResolver 同一条约定):
|
|
44
|
-
* `@/modules/<unit>/X` → `src/X` · `@/platforms/X` → `src/platforms/X`
|
|
45
|
-
*/
|
|
46
150
|
function resolveLocal(pkgDir, unit, fromFile, specifier) {
|
|
47
151
|
let base;
|
|
48
152
|
if (specifier.startsWith('@/')) {
|
|
@@ -69,7 +173,6 @@ function resolveLocal(pkgDir, unit, fromFile, specifier) {
|
|
|
69
173
|
return undefined;
|
|
70
174
|
}
|
|
71
175
|
|
|
72
|
-
/** 从某个入口出发,本包内可达的源文件。 */
|
|
73
176
|
function reachableFrom(pkgDir, unit, entry) {
|
|
74
177
|
const seen = new Set();
|
|
75
178
|
const start = join(pkgDir, entry);
|
|
@@ -95,7 +198,7 @@ function hasPlatformPackage(manifest) {
|
|
|
95
198
|
return names.some((name) => /(^|\/)platform-/.test(name));
|
|
96
199
|
}
|
|
97
200
|
|
|
98
|
-
export const title = '
|
|
201
|
+
export const title = '模块源码边界与零注释';
|
|
99
202
|
|
|
100
203
|
export async function run() {
|
|
101
204
|
const modulePackages = discoverModulePackages({ root: process.cwd() });
|
|
@@ -110,6 +213,16 @@ export async function run() {
|
|
|
110
213
|
const srcDir = join(pkg.dir, 'src');
|
|
111
214
|
const viaPlatform = hasPlatformPackage(pkg.manifest);
|
|
112
215
|
|
|
216
|
+
for (const file of codeFiles(srcDir)) {
|
|
217
|
+
const rel = relative(pkg.dir, file);
|
|
218
|
+
const text = readFileSync(file, 'utf8');
|
|
219
|
+
for (const line of commentViolationsOf(text, { css: file.endsWith('.css') })) {
|
|
220
|
+
problems.push(
|
|
221
|
+
`${name}: ${rel}:${line} 有注释 —— 模块源码零注释(说明写 README.md 或 spec;eslint / ts / vitest 指令注释除外)`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
113
226
|
if (viaPlatform && existsSync(join(srcDir, 'platforms'))) {
|
|
114
227
|
problems.push(
|
|
115
228
|
`${name}: 已经改用平台包了,src/platforms/ 就别再留一份 —— 平台知识两处都有,改平台要改两处`,
|
|
@@ -180,7 +293,7 @@ export async function run() {
|
|
|
180
293
|
if (platform !== null && viaPlatform) {
|
|
181
294
|
problems.push(
|
|
182
295
|
`${name}: ${rel} 里 import 了 ${specifier} —— 这个模块已经改用平台包了,平台知识走包名` +
|
|
183
|
-
`(比如 @
|
|
296
|
+
`(比如 @acme-dsh-workbench/platform),不许再用 @/platforms/*`,
|
|
184
297
|
);
|
|
185
298
|
} else if (platform !== null && !existsSync(join(srcDir, 'platforms', `${platform[2]}.ts`))) {
|
|
186
299
|
problems.push(
|
|
@@ -189,6 +302,26 @@ export async function run() {
|
|
|
189
302
|
);
|
|
190
303
|
}
|
|
191
304
|
}
|
|
305
|
+
|
|
306
|
+
for (const match of text.matchAll(SHARED_FROM)) {
|
|
307
|
+
if (typeOnlyOf(match[2], match[3])) continue;
|
|
308
|
+
const problem = sharedProblem(name, rel, match[4], sideOf(file), false);
|
|
309
|
+
if (problem !== undefined) problems.push(problem);
|
|
310
|
+
}
|
|
311
|
+
for (const match of text.matchAll(SHARED_DYNAMIC)) {
|
|
312
|
+
const problem = sharedProblem(name, rel, match[1], sideOf(file), true);
|
|
313
|
+
if (problem !== undefined) problems.push(problem);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const entry of LIB_CAPABILITIES) {
|
|
317
|
+
if (importsPreferred(text, entry.preferred)) continue;
|
|
318
|
+
if (!entry.pattern.test(text)) continue;
|
|
319
|
+
const line = text.split('\n').findIndex((one) => entry.pattern.test(one)) + 1;
|
|
320
|
+
problems.push(
|
|
321
|
+
`${name}: ${rel}:${String(line < 1 ? 1 : line)} 手写了 ${entry.capability} —— ${entry.why}` +
|
|
322
|
+
`(首选 ${entry.preferred};import 它,或把这件事交给外壳)`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
192
325
|
}
|
|
193
326
|
}
|
|
194
327
|
|
|
@@ -207,7 +340,8 @@ export async function run() {
|
|
|
207
340
|
notes: [
|
|
208
341
|
`${scanned} 个源文件 · 不伸进外壳 app/ · 不跨模块 · ` +
|
|
209
342
|
`平台知识要么在自己包里、要么走平台包(不许两样都有)· ` +
|
|
210
|
-
`dsh 的浏览器包只在客户端侧可用(${seedUsers}
|
|
343
|
+
`dsh 的浏览器包只在客户端侧可用(${seedUsers} 处)· ` +
|
|
344
|
+
`共享包根入口只取类型、/host 只在宿主侧取运行值 · 不手写进程/锁/原子写`,
|
|
211
345
|
],
|
|
212
346
|
problems: [],
|
|
213
347
|
};
|
|
@@ -1,16 +1,6 @@
|
|
|
1
|
-
// 模块**离开外壳源码**也得能 build + typecheck(发布出去的是模块,用户手上没有本仓的 src)。
|
|
2
|
-
//
|
|
3
|
-
// 手法:把外壳的 `src/` 暂时改名藏起来,再对每个模块包跑它自己的 build 与 typecheck,
|
|
4
|
-
// 最后无论成败都还原。藏起来还过不了 = 这条模块在用"只有本仓才有"的相对路径 / 别名 ——
|
|
5
|
-
// 发布出去会散架。
|
|
6
|
-
//
|
|
7
|
-
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
8
|
-
// 打印与退出码归 `../check.mjs` 那一层。所以这里**不许**自己退出:
|
|
9
|
-
// 仓外工作区(没有外壳源码)以前就是"提前成功退出"(退出码 0)—— 那会把排在后面的判据静默吞掉。
|
|
10
|
-
|
|
11
|
-
import { execFileSync } from 'node:child_process';
|
|
12
1
|
import { existsSync, renameSync } from 'node:fs';
|
|
13
2
|
import { join } from 'node:path';
|
|
3
|
+
import { execaSync } from 'execa';
|
|
14
4
|
import { discoverModulePackages, discoverPackages } from '../discover.mjs';
|
|
15
5
|
|
|
16
6
|
const SHELL = '@dsh-workbench/core';
|
|
@@ -24,7 +14,7 @@ export async function run() {
|
|
|
24
14
|
if (visible === undefined || !existsSync(visible)) {
|
|
25
15
|
return {
|
|
26
16
|
status: 'skip',
|
|
27
|
-
notes: [
|
|
17
|
+
notes: [`当前目录没有 ${SHELL} 的源码("不许 @/ 指外壳"那条判据照旧在守)`],
|
|
28
18
|
problems: [],
|
|
29
19
|
};
|
|
30
20
|
}
|
|
@@ -50,7 +40,7 @@ export async function run() {
|
|
|
50
40
|
const restore = () => {
|
|
51
41
|
if (existsSync(hidden) && !existsSync(visible)) renameSync(hidden, visible);
|
|
52
42
|
};
|
|
53
|
-
|
|
43
|
+
|
|
54
44
|
const onInterrupt = () => {
|
|
55
45
|
restore();
|
|
56
46
|
process.removeListener('SIGINT', onInterrupt);
|
|
@@ -66,7 +56,7 @@ export async function run() {
|
|
|
66
56
|
for (const script of ['build', 'typecheck']) {
|
|
67
57
|
if (pkg.manifest.scripts?.[script] === undefined) continue;
|
|
68
58
|
try {
|
|
69
|
-
|
|
59
|
+
execaSync('pnpm', ['run', script], { cwd: pkg.dir, stdio: 'pipe' });
|
|
70
60
|
} catch (error) {
|
|
71
61
|
const output = `${error.stdout ?? ''}${error.stderr ?? ''}`;
|
|
72
62
|
const first = output
|