@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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -177
  3. package/bin/dsh-wb-module.mjs +89 -34
  4. package/package.json +22 -3
  5. package/src/arch.mjs +158 -129
  6. package/src/build.mjs +6 -30
  7. package/src/changelog.mjs +166 -0
  8. package/src/check-flags.mjs +0 -7
  9. package/src/check.mjs +115 -53
  10. package/src/checks/alias.mjs +0 -9
  11. package/src/checks/arch.mjs +3 -25
  12. package/src/checks/bundles.mjs +2 -34
  13. package/src/checks/cards.mjs +26 -30
  14. package/src/checks/contracts.mjs +72 -219
  15. package/src/checks/kit.mjs +2 -42
  16. package/src/checks/lint.mjs +9 -82
  17. package/src/checks/markers.mjs +48 -17
  18. package/src/checks/primitives.mjs +0 -39
  19. package/src/checks/protocol.mjs +0 -18
  20. package/src/checks/source.mjs +103 -51
  21. package/src/checks/standalone.mjs +4 -14
  22. package/src/checks/style.mjs +84 -230
  23. package/src/css-injection.mjs +0 -11
  24. package/src/css-shell.mjs +3 -31
  25. package/src/datasource-file.mjs +104 -0
  26. package/src/declared-tools.mjs +0 -24
  27. package/src/design.mjs +116 -38
  28. package/src/discover.mjs +2 -10
  29. package/src/font-scale.mjs +1 -61
  30. package/src/init.mjs +2 -17
  31. package/src/lint.mjs +24 -144
  32. package/src/market-index.mjs +569 -0
  33. package/src/market-init.mjs +80 -0
  34. package/src/primitives-surface.mjs +2 -28
  35. package/src/publish.mjs +11 -25
  36. package/src/release-changelog.mjs +29 -0
  37. package/src/release-journal.mjs +131 -0
  38. package/src/release-report.mjs +99 -0
  39. package/src/release-util.mjs +87 -0
  40. package/src/release.mjs +1250 -0
  41. package/src/seed.mjs +0 -25
  42. package/src/shell-type-files.mjs +3 -30
  43. package/src/sync.mjs +41 -24
  44. package/template/AGENTS.md.tpl +33 -0
  45. package/template/CHANGELOG.md.tpl +7 -0
  46. package/template/README.md.tpl +6 -33
  47. package/template/package.json.tpl +2 -2
  48. package/template/src/client.tsx.tpl +1 -18
  49. package/template/src/host.ts.tpl +2 -15
  50. package/template/types/globals.d.ts +1 -14
  51. package/template-diy/AGENTS.md.tpl +33 -0
  52. package/template-diy/CHANGELOG.md.tpl +7 -0
  53. package/template-diy/README.md.tpl +4 -133
  54. package/template-diy/package.json.tpl +2 -2
  55. package/template-diy/src/client.tsx.tpl +1 -29
  56. package/template-diy/src/host.ts.tpl +1 -15
  57. package/template-diy/src/plugin.test.ts.tpl +1 -1
  58. package/template-diy/types/globals.d.ts +1 -14
  59. package/template-diy/types/styles.d.ts +0 -10
@@ -1,54 +1,17 @@
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
- // 共享包(`@dsh-workbench/shared`)的**两个入口也是两个运行时**,这里守它的「运行值」边界:
17
- // · 根入口 `.` 是浏览器 barrel(组件 + 它们自己的样式)。**两侧都不许从它取运行值**:
18
- // 浏览器侧该走 kit(`@dsh-workbench/core/client`,单例只有那一份),宿主侧该走 `/host` ——
19
- // 根 barrel 会在 `dist/host.js` 里把 dsh 的浏览器件一路拉进来,Node 里 import 不进来(实测)。
20
- // · 宿主入口 `/host` 里有 node:crypto / node:fs,**只有宿主侧能引**;浏览器产物装不下。
21
- // · 类型是例外:`import type` / `export type`(含逐个 `type X`)编译期擦除,两侧随便引 ——
22
- // 契约类型(`Translate` / `ApiError` / `UnitClientFace` …)正是从根入口拿的。
23
- //
24
- // 为什么这一条住在**源码**判据里:它要知道"这个文件属于哪一侧",而侧只能从两个源码入口做可达性分析
25
- // 得到(产物里看不出是哪份源码把东西拖进来的)。判据的负例在 `scripts/selfcheck/checks/negative-probe.mjs`。
26
- //
27
- // 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
28
- // 打印与退出码归 `../check.mjs` 那一层。
29
-
30
1
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import ts from 'typescript';
31
3
  import { dirname, join, relative, resolve } from 'node:path';
32
4
  import { discoverModulePackages } from '../discover.mjs';
33
5
  import { isSeedSpecifier } from '../seed.mjs';
6
+ import { LIB_CAPABILITIES, importsPreferred } from '@dsh-workbench/shared/host';
34
7
 
35
- // 静态 `from '…'`、副作用 `import '…'`、动态 `import('…')` —— 动态那条不能漏,
36
- // 模块入口常见写法是 `await import('./views/…')`,漏了就会把可达文件判成"死代码"。
37
8
  const IMPORT = /from\s*'([^']+)'|import\s*\(?\s*'([^']+)'/g;
38
9
 
39
- // 共享包那两条规则要分清「取运行值」与「只取类型」,所以按**整条语句**匹配(`import { a }` 与
40
- // `import type { A }` 的差别在语句开头与花括号里)。片段里不许出现引号或分号:否则一条 `import type`
41
- // 会把后面那条运行值 import 一起吞进来,判据就漏了。
42
10
  const SHARED_FROM =
43
11
  /(?:^|[;}\s])(import|export)\s+(type\s+)?([^'";]{0,600}?)\s*from\s*['"](@dsh-workbench\/shared(?:\/host)?)['"]/g;
44
- // 动态 `import('…')` 一定是运行值(`import type` 没有这个写法)。
12
+
45
13
  const SHARED_DYNAMIC = /import\s*\(\s*['"](@dsh-workbench\/shared(?:\/host)?)['"]/g;
46
14
 
47
- /**
48
- * 这条 import/export 是不是**只取类型**:`import type …`、`export type …`、
49
- * 以及每个具名都带 `type` 的 `import { type A, type B }`。
50
- * `import X, { type A }` / `import * as ns` / `export *` 都算运行值。
51
- */
52
15
  function typeOnlyOf(keyword, clause) {
53
16
  if (keyword !== undefined) return true;
54
17
  const braces = /\{([\s\S]*)\}/.exec(clause);
@@ -68,7 +31,83 @@ const SIDE_TEXT = {
68
31
  orphan: '两个入口都到不了(可能是死代码)',
69
32
  };
70
33
 
71
- /** 共享包两个入口的「运行值」违规 → 一句人话(说清为什么、改哪、类型怎么引)。 */
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
+
72
111
  function sharedProblem(name, rel, specifier, side, dynamic) {
73
112
  const what = dynamic ? `动态 import 了 ${specifier}` : `从 ${specifier} 取了运行值`;
74
113
  if (specifier === '@dsh-workbench/shared') {
@@ -108,11 +147,6 @@ function sourceFiles(dir, acc = []) {
108
147
  return acc;
109
148
  }
110
149
 
111
- /**
112
- * 把一条 import 说明符解析成本包内的文件;解析不到就是包说明符(不外追)。
113
- * 别名两条(与 `build.mjs` 的 aliasResolver 同一条约定):
114
- * `@/modules/<unit>/X` → `src/X` · `@/platforms/X` → `src/platforms/X`
115
- */
116
150
  function resolveLocal(pkgDir, unit, fromFile, specifier) {
117
151
  let base;
118
152
  if (specifier.startsWith('@/')) {
@@ -139,7 +173,6 @@ function resolveLocal(pkgDir, unit, fromFile, specifier) {
139
173
  return undefined;
140
174
  }
141
175
 
142
- /** 从某个入口出发,本包内可达的源文件。 */
143
176
  function reachableFrom(pkgDir, unit, entry) {
144
177
  const seen = new Set();
145
178
  const start = join(pkgDir, entry);
@@ -165,7 +198,7 @@ function hasPlatformPackage(manifest) {
165
198
  return names.some((name) => /(^|\/)platform-/.test(name));
166
199
  }
167
200
 
168
- export const title = '模块源码边界';
201
+ export const title = '模块源码边界与零注释';
169
202
 
170
203
  export async function run() {
171
204
  const modulePackages = discoverModulePackages({ root: process.cwd() });
@@ -180,6 +213,16 @@ export async function run() {
180
213
  const srcDir = join(pkg.dir, 'src');
181
214
  const viaPlatform = hasPlatformPackage(pkg.manifest);
182
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
+
183
226
  if (viaPlatform && existsSync(join(srcDir, 'platforms'))) {
184
227
  problems.push(
185
228
  `${name}: 已经改用平台包了,src/platforms/ 就别再留一份 —— 平台知识两处都有,改平台要改两处`,
@@ -250,7 +293,7 @@ export async function run() {
250
293
  if (platform !== null && viaPlatform) {
251
294
  problems.push(
252
295
  `${name}: ${rel} 里 import 了 ${specifier} —— 这个模块已经改用平台包了,平台知识走包名` +
253
- `(比如 @pagoda-dsh-workbench/platform-coding),不许再用 @/platforms/*`,
296
+ `(比如 @acme-dsh-workbench/platform),不许再用 @/platforms/*`,
254
297
  );
255
298
  } else if (platform !== null && !existsSync(join(srcDir, 'platforms', `${platform[2]}.ts`))) {
256
299
  problems.push(
@@ -260,7 +303,6 @@ export async function run() {
260
303
  }
261
304
  }
262
305
 
263
- // 共享包的两个入口:根入口只许取类型、宿主入口只许宿主侧取运行值(理由见文件头)。
264
306
  for (const match of text.matchAll(SHARED_FROM)) {
265
307
  if (typeOnlyOf(match[2], match[3])) continue;
266
308
  const problem = sharedProblem(name, rel, match[4], sideOf(file), false);
@@ -270,6 +312,16 @@ export async function run() {
270
312
  const problem = sharedProblem(name, rel, match[1], sideOf(file), true);
271
313
  if (problem !== undefined) problems.push(problem);
272
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
+ }
273
325
  }
274
326
  }
275
327
 
@@ -289,7 +341,7 @@ export async function run() {
289
341
  `${scanned} 个源文件 · 不伸进外壳 app/ · 不跨模块 · ` +
290
342
  `平台知识要么在自己包里、要么走平台包(不许两样都有)· ` +
291
343
  `dsh 的浏览器包只在客户端侧可用(${seedUsers} 处)· ` +
292
- `共享包根入口只取类型、/host 只在宿主侧取运行值`,
344
+ `共享包根入口只取类型、/host 只在宿主侧取运行值 · 不手写进程/锁/原子写`,
293
345
  ],
294
346
  problems: [],
295
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: [`本仓没有 ${SHELL} 的源码("不许 @/ 指外壳"那条判据照旧在守)`],
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
- // Ctrl-C 时也必须还原(进程被杀时 finally 不跑):还原完把信号还回去,让默认行为收场。
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
- execFileSync('pnpm', ['run', script], { cwd: pkg.dir, stdio: 'pipe' });
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