@dsh-workbench/module-tools 0.0.0-stage → 0.1.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.
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from 'node:path';
3
+ import { build } from '../src/build.mjs';
4
+ import { check } from '../src/check.mjs';
5
+ import { init } from '../src/init.mjs';
6
+
7
+ const [command, ...rest] = process.argv.slice(2);
8
+ const cwd = process.env.DSH_WB_MODULE_CWD ?? process.cwd();
9
+
10
+ const commands = { build, check, init };
11
+
12
+ if (!command || !commands[command]) {
13
+ console.error('用法:dsh-wb-module <build|check|init> [--cwd <目录>] [--only contracts|bundles|source|cards]\n dsh-wb-module init <unit> [--dir <目录>] [--description <文字>]');
14
+ process.exit(2);
15
+ }
16
+
17
+ const value = (flag) => {
18
+ const at = rest.indexOf(flag);
19
+ return at === -1 ? undefined : rest[at + 1];
20
+ };
21
+ const args = [];
22
+ for (let i = 0; i < rest.length; i += 1) {
23
+ if (rest[i] === '--cwd' || rest[i] === '--only') {
24
+ i += 1;
25
+ continue;
26
+ }
27
+ args.push(rest[i]);
28
+ }
29
+
30
+ const root = resolve(value('--cwd') ?? cwd);
31
+ if (root !== process.cwd()) process.chdir(root);
32
+
33
+ await commands[command]({ root, only: value('--only'), args });
package/package.json CHANGED
@@ -1,6 +1,27 @@
1
1
  {
2
2
  "name": "@dsh-workbench/module-tools",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "模块包的构建与门禁工具:build(单文件 ESM 宿主侧 + 套 loader 壳的浏览器侧)、check(契约/产物边界/源码边界/卡片渲染)。模块包不再各带一份构建脚本。",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "dsh-wb-module": "./bin/dsh-wb-module.mjs"
9
+ },
10
+ "exports": {
11
+ ".": "./src/index.mjs"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "src",
16
+ "template"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "dependencies": {
22
+ "esbuild": "^0.28.2"
23
+ },
24
+ "scripts": {
25
+ "selfcheck": "node scripts/selfcheck.mjs"
26
+ }
6
27
  }
package/src/build.mjs ADDED
@@ -0,0 +1,106 @@
1
+
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { builtinModules } from 'node:module';
4
+ import { join } from 'node:path';
5
+ import { build as esbuild } from 'esbuild';
6
+
7
+ const SHELL_CLIENT = '@dsh-workbench/core/client';
8
+ const RESOLVE_EXTENSIONS = ['.ts', '.tsx', '.js', '.json'];
9
+ const SINGLETON = /^@\/shared\/(hooks|components)\//;
10
+
11
+ function pickEntry(root, names) {
12
+ for (const name of names) {
13
+ const full = join(root, name);
14
+ if (existsSync(full)) return name;
15
+ }
16
+ return undefined;
17
+ }
18
+
19
+ function aliasResolver({ root, unit }) {
20
+ const localSrc = join(root, 'src');
21
+ return {
22
+ name: 'dsh-wb-module:alias',
23
+ setup(context) {
24
+ context.onResolve({ filter: /^@\// }, (args) => {
25
+ if (SINGLETON.test(args.path)) {
26
+ throw new Error(
27
+ `${args.path} 必须从 kit 拿(运行时要单例),不能内联进模块包 —— 见 ADR-0026 §4 代价 #9`,
28
+ );
29
+ }
30
+ let base;
31
+ if (unit !== undefined && args.path.startsWith(`@/modules/${unit}/`)) {
32
+ base = join(localSrc, args.path.slice(`@/modules/${unit}/`.length));
33
+ } else {
34
+ const platform = /^@\/platforms\/[^/]+\/(.+)$/.exec(args.path);
35
+ if (platform) base = join(localSrc, 'platforms', platform[1]);
36
+ }
37
+ if (base === undefined) {
38
+ throw new Error(
39
+ `${args.path} 指向外壳源码,模块不许这样引 —— 改从包名引(@dsh-workbench/shared 或 /host);` +
40
+ `模块自己的文件用 @/modules/${unit ?? '<unit>'}/… 或相对路径`,
41
+ );
42
+ }
43
+ for (const candidate of [`${base}.ts`, `${base}.tsx`, `${base}/index.ts`, `${base}/index.tsx`]) {
44
+ if (existsSync(candidate)) return { path: candidate };
45
+ }
46
+ throw new Error(`解析不到:${args.path}(试过 ${base}.{ts,tsx} 与 ${base}/index.{ts,tsx})`);
47
+ });
48
+ },
49
+ };
50
+ }
51
+
52
+ export async function build({ root }) {
53
+ const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
54
+ const id = manifest.name;
55
+ if (typeof id !== 'string' || id === '') throw new Error('package.json 里没有 name,认不出模块 id');
56
+ const unit = manifest.dsh?.workbench?.unit;
57
+ const declared = manifest.dsh?.client?.external ?? [];
58
+ const plugins = [aliasResolver({ root, unit })];
59
+ const nodeBuiltins = [...builtinModules, ...builtinModules.map((name) => `node:${name}`)];
60
+
61
+ const hostEntry = pickEntry(root, ['src/plugin.host.ts', 'src/host.ts']);
62
+ if (hostEntry !== undefined) {
63
+ await esbuild({
64
+ absWorkingDir: root,
65
+ entryPoints: [hostEntry],
66
+ outfile: 'dist/host.js',
67
+ bundle: true,
68
+ platform: 'node',
69
+ format: 'esm',
70
+ target: 'node20',
71
+ external: [...nodeBuiltins, '@dsh-workbench/shared', '@dsh-workbench/shared/host', '@dsh-workbench/core'],
72
+ resolveExtensions: RESOLVE_EXTENSIONS,
73
+ plugins,
74
+ logLevel: 'warning',
75
+ });
76
+ }
77
+
78
+ const clientEntry = pickEntry(root, ['src/plugin.client.ts', 'src/client.tsx']);
79
+ if (clientEntry === undefined) throw new Error('找不到浏览器侧入口(src/plugin.client.ts 或 src/client.tsx)');
80
+ const client = await esbuild({
81
+ absWorkingDir: root,
82
+ entryPoints: [clientEntry],
83
+ bundle: true,
84
+ platform: 'browser',
85
+ format: 'cjs',
86
+ target: 'es2022',
87
+ external: ['react', 'react-dom', SHELL_CLIENT, ...declared],
88
+ jsx: 'transform',
89
+ jsxFactory: 'h',
90
+ jsxFragment: 'Fragment',
91
+ resolveExtensions: RESOLVE_EXTENSIONS,
92
+ plugins,
93
+ write: false,
94
+ logLevel: 'warning',
95
+ });
96
+
97
+ const indented = client.outputFiles[0].text
98
+ .split('\n')
99
+ .map((line) => (line === '' ? line : ` ${line}`))
100
+ .join('\n');
101
+
102
+ 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`;
103
+
104
+ writeFileSync(join(root, 'client.js'), wrapped);
105
+ console.log(`${id}:client.js 已更新(${wrapped.length} 字节)`);
106
+ }
package/src/check.mjs ADDED
@@ -0,0 +1,23 @@
1
+
2
+ const CHECKS = {
3
+ contracts: { title: '模块契约', load: () => import('./checks/contracts.mjs') },
4
+ bundles: { title: '模块产物边界', load: () => import('./checks/bundles.mjs') },
5
+ source: { title: '模块源码边界', load: () => import('./checks/source.mjs') },
6
+ cards: { title: '卡片渲染', load: () => import('./checks/cards.mjs') },
7
+ standalone: { title: '独立构建', load: () => import('./checks/standalone.mjs') },
8
+ markers: { title: '公开包无内部标识', load: () => import('./checks/markers.mjs') },
9
+ style: { title: '样式归属', load: () => import('./checks/style.mjs') },
10
+ alias: { title: '模块不指外壳', load: () => import('./checks/alias.mjs') },
11
+ };
12
+
13
+ export async function check({ only } = {}) {
14
+ const names = only === undefined ? Object.keys(CHECKS) : [only];
15
+ for (const name of names) {
16
+ const entry = CHECKS[name];
17
+ if (entry === undefined) {
18
+ console.error(`不认识的判据:${name}(有:${Object.keys(CHECKS).join(' / ')})`);
19
+ process.exit(2);
20
+ }
21
+ await entry.load();
22
+ }
23
+ }
@@ -0,0 +1,49 @@
1
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { discoverModulePackages } from '../discover.mjs';
4
+
5
+ const SPECIFIER = /(?:from|import)\s*\(?\s*['"](@\/[^'"]+)['"]/g;
6
+
7
+ function walk(dir, acc = []) {
8
+ for (const entry of readdirSync(dir)) {
9
+ if (entry === 'node_modules' || entry === 'dist') continue;
10
+ const full = join(dir, entry);
11
+ if (statSync(full).isDirectory()) walk(full, acc);
12
+ else if (/\.tsx?$/.test(entry)) acc.push(full);
13
+ }
14
+ return acc;
15
+ }
16
+
17
+ const modules = discoverModulePackages({ root: process.cwd() });
18
+ const problems = [];
19
+ for (const pkg of modules) {
20
+ const unit = pkg.manifest.dsh?.workbench?.unit;
21
+ const src = join(pkg.dir, 'src');
22
+ const viaPlatform = /(^|\/)platform-/.test(
23
+ Object.keys({ ...(pkg.manifest.dependencies ?? {}), ...(pkg.manifest.devDependencies ?? {}) }).join(' '),
24
+ );
25
+ if (unit === undefined) continue;
26
+ for (const file of walk(src)) {
27
+ const text = readFileSync(file, 'utf8');
28
+ text.split('\n').forEach((line, index) => {
29
+ for (const match of line.matchAll(SPECIFIER)) {
30
+ const specifier = match[1];
31
+ const own = specifier.startsWith(`@/modules/${unit}/`);
32
+ const selfPlatform = specifier.startsWith('@/platforms/') && !viaPlatform;
33
+ if (!own && !selfPlatform) {
34
+ problems.push(
35
+ `${pkg.manifest.name}: ${relative(pkg.dir, file)}:${index + 1} 用了 ${specifier} —— ` +
36
+ `模块只许引自己的 @/modules/${unit}/*;共享代码走包名,平台知识走包名或本包的 @/platforms/*(用了平台包就不许再用)`,
37
+ );
38
+ }
39
+ }
40
+ });
41
+ }
42
+ }
43
+
44
+ if (problems.length > 0) {
45
+ console.error(`✖ 模块源码不许出现指向外壳的 @/:${problems.length} 处`);
46
+ for (const problem of problems) console.error(` · ${problem}`);
47
+ process.exit(1);
48
+ }
49
+ console.log(`✔ 模块不指外壳:${modules.length} 个模块的 src 里没有指向外壳的 @/`);
@@ -0,0 +1,71 @@
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
+ import { existsSync, readFileSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ import { discoverModulePackages } from '../discover.mjs';
20
+
21
+
22
+ const CLIENT_FORBIDDEN = [
23
+ ['node 内置', /\bnode:[a-z]/],
24
+ ['process.env(宿主环境)', /\bprocess\.env\b/],
25
+ ['Buffer(Node 全局)', /\bBuffer\b/],
26
+ ];
27
+
28
+ const HOST_FORBIDDEN = [
29
+ ['DOM(document)', /\bdocument\./],
30
+ ['DOM(window)', /\bwindow\./],
31
+ ['react', /require\("react"\)/],
32
+ ];
33
+
34
+ const modulePackages = () => discoverModulePackages({ root: process.cwd() });
35
+
36
+ const problems = [];
37
+ let checked = 0;
38
+
39
+ for (const pkg of modulePackages()) {
40
+ const name = pkg.manifest.name;
41
+ const artifacts = [
42
+ { what: '浏览器产物', file: 'client.js', rules: CLIENT_FORBIDDEN },
43
+ { what: '宿主产物', file: 'dist/host.js', rules: HOST_FORBIDDEN },
44
+ ];
45
+
46
+ for (const artifact of artifacts) {
47
+ const full = join(pkg.dir, artifact.file);
48
+ if (!existsSync(full)) {
49
+ problems.push(`${name}: 找不到${artifact.what} ${artifact.file} —— 先 build 再 check`);
50
+ continue;
51
+ }
52
+ checked += 1;
53
+ const source = readFileSync(full, 'utf8');
54
+ for (const [label, pattern] of artifact.rules) {
55
+ if (pattern.test(source)) {
56
+ problems.push(
57
+ `${name}: ${artifact.what} ${artifact.file} 里出现了${label} —— ` +
58
+ `两个运行时混了(判据是能力:浏览器侧不许碰宿主传输层,宿主侧不许碰 DOM/react)`,
59
+ );
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ if (problems.length > 0) {
66
+ console.error(`✖ 模块产物边界 ${problems.length} 条不过:`);
67
+ for (const problem of problems) console.error(` · ${problem}`);
68
+ process.exit(1);
69
+ }
70
+
71
+ console.log(`✔ 模块产物边界通过:${checked} 份产物 · 浏览器侧无 node 内置 · 宿主侧无 DOM/react`);
@@ -0,0 +1,229 @@
1
+ // 模块卡片能不能**画出来**(S1 视觉验收里唯一能机器化的那一半)。
2
+ //
3
+ // 「卡片出现」这句话里有两件事:
4
+ // ① 结构画得出来 —— 卡片的代码跑得通、返回一棵非空元素树。**这条能机器验**;
5
+ // ② 画成什么样(间距、颜色、两栏还是三栏)—— 只有人眼能判,这里不碰。
6
+ //
7
+ // 为什么用「空数据」当输入:外壳渲染卡片时,数据往往还没回来(第一次读、卡在审批、
8
+ // 平台没登录)。所以**卡片在拿到空数据时不许抛** —— 这正是"卡片一直不出来 / 点一下白屏"
9
+ // 那类事故的机器形态。给了数据才跑得通的卡片,等于把崩溃留到用户面前。
10
+ //
11
+ // 手法与 packages/workbench/scripts/selfcheck.mjs 里那套一致:产物是
12
+ // `window.__ModuleLoader__.load({id, factory})`,用假的 loader / require 把它拉起来。
13
+ // 覆盖范围跟着 `dsh.workbench` 自述清单走 —— 新模块不用改这里。
14
+
15
+ import { existsSync, readFileSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { discoverModulePackages } from '../discover.mjs';
18
+
19
+
20
+ const modulePackages = () => discoverModulePackages({ root: process.cwd() });
21
+
22
+ // 任何属性都拿得到、任何调用都返回自己的替身 —— 这样「外壳还没给数据」的各种形状
23
+ // 都不会让卡片因为我们**没桩到某个字段**而假红。真红只留给卡片自己的代码。
24
+ function anyStub(path = 'stub') {
25
+ const target = function () {
26
+ return anyStub(`${path}()`);
27
+ };
28
+ return new Proxy(target, {
29
+ get(_target, key) {
30
+ if (key === Symbol.toPrimitive) return () => 0;
31
+ if (key === Symbol.iterator) return function* () {};
32
+ if (key === 'then') return undefined;
33
+ if (key === 'length') return 0;
34
+ if (key === 'toString') return () => path;
35
+ return anyStub(`${path}.${String(key)}`);
36
+ },
37
+ apply() {
38
+ return anyStub(`${path}()`);
39
+ },
40
+ });
41
+ }
42
+
43
+ function fakeReact() {
44
+ const element = (type, props, ...children) => ({ type, props: props ?? {}, children });
45
+ return {
46
+ createElement: element,
47
+ createElementNS: (_namespace, type, props, ...children) => element(type, props, ...children),
48
+ Fragment: 'Fragment',
49
+ useState: (initial) => [typeof initial === 'function' ? initial() : initial, () => {}],
50
+ useReducer: (_reducer, initial) => [initial, () => {}],
51
+ useMemo: (factory) => factory(),
52
+ useCallback: (factory) => factory,
53
+ useRef: (initial) => ({ current: initial ?? null }),
54
+ useEffect: () => {},
55
+ useLayoutEffect: () => {},
56
+ useSyncExternalStore: (_subscribe, snapshot) => snapshot(),
57
+ useId: () => 'wb-id',
58
+ isValidElement: (value) => typeof value === 'object' && value !== null && 'type' in value,
59
+ Children: { toArray: (value) => (Array.isArray(value) ? value : [value]) },
60
+ };
61
+ }
62
+
63
+ function fakeKit() {
64
+ const callable = () => anyStub('result');
65
+ return {
66
+ hooks: new Proxy({}, { get: () => callable }),
67
+ components: new Proxy({}, { get: () => (props) => ({ type: 'stub', props: props ?? {}, children: [] }) }),
68
+ api: new Proxy({}, { get: () => callable }),
69
+ lib: anyStub('lib'),
70
+ };
71
+ }
72
+
73
+ function loadFace(clientFile) {
74
+ let captured;
75
+ const fakeWindow = {
76
+ // 浏览器里 `window.location` 永远在;缺了它,夹具模式的开关会先抛(第一次跑就踩到)。
77
+ location: { search: '', href: 'http://127.0.0.1:3081/' },
78
+ __ModuleLoader__: {
79
+ load: (entry) => {
80
+ captured = entry;
81
+ },
82
+ },
83
+ };
84
+ const expose = { window: fakeWindow, module: undefined, exports: undefined };
85
+ const source = readFileSync(clientFile, 'utf8');
86
+ const factoryOf = new Function('window', `${source}\nreturn undefined;`);
87
+ factoryOf(fakeWindow);
88
+ if (captured === undefined) throw new Error('client.js 没有调用 __ModuleLoader__.load()');
89
+ const module = { exports: {} };
90
+ expose.module = module;
91
+ expose.exports = module.exports;
92
+ const fakeRequire = (id) => {
93
+ if (id === 'react') return fakeReact();
94
+ if (id === '@dsh-workbench/core/client') return { kit: fakeKit() };
95
+ throw new Error(`产物 require 了没桩的外部:${id}`);
96
+ };
97
+ const produced = captured.factory(fakeRequire);
98
+ return { id: captured.id, exports: produced ?? module.exports };
99
+ }
100
+
101
+ // 递归**调用**函数组件 —— 不调用的话,树里只有最外层那个 `type`,
102
+ // 卡片体内的代码一行都没跑(第一次写漏了这步,4 张卡片都只数出 3 个节点)。
103
+ function render(node, depth, budget) {
104
+ if (node === null || node === undefined || typeof node === 'boolean') return 0;
105
+ if (typeof node === 'string' || typeof node === 'number') return 1;
106
+ if (Array.isArray(node)) {
107
+ let count = 0;
108
+ for (const one of node) count += render(one, depth, budget);
109
+ return count;
110
+ }
111
+ if (typeof node !== 'object') return 1;
112
+ if (budget.left <= 0) return 0;
113
+ budget.left -= 1;
114
+ budget.maxDepth = Math.max(budget.maxDepth, depth);
115
+
116
+ const children = [...(node.children ?? [])];
117
+ if (typeof node.type === 'function') {
118
+ if (depth > 40) throw new Error('组件递归超过 40 层(自引用?)');
119
+ const out = node.type({ ...(node.props ?? {}), children });
120
+ return 1 + render(out, depth + 1, budget);
121
+ }
122
+ let count = 1;
123
+ for (const child of children) count += render(child, depth, budget);
124
+ return count;
125
+ }
126
+
127
+ const problems = [];
128
+ const rows = [];
129
+
130
+ for (const pkg of modulePackages()) {
131
+ const name = pkg.manifest.name;
132
+ const clientFile = join(pkg.dir, 'client.js');
133
+ if (!existsSync(clientFile)) {
134
+ problems.push(`${name}: 没有 client.js —— 先 build`);
135
+ continue;
136
+ }
137
+
138
+ let face;
139
+ try {
140
+ const loaded = loadFace(clientFile);
141
+ face = loaded.exports;
142
+ } catch (error) {
143
+ problems.push(`${name}: 产物加载不起来 —— ${error.message}`);
144
+ continue;
145
+ }
146
+
147
+ let registered;
148
+ const ctx = {
149
+ get: (key) =>
150
+ key === 'workbenchUnits'
151
+ ? {
152
+ registerUnit: (value) => {
153
+ registered = value;
154
+ return () => {};
155
+ },
156
+ }
157
+ : undefined,
158
+ effect: (fn) => {
159
+ fn();
160
+ return () => {};
161
+ },
162
+ provide: () => {},
163
+ logger: { info: () => {}, warn: () => {}, error: () => {} },
164
+ };
165
+ const apply = face?.apply ?? face?.default?.apply;
166
+ if (typeof apply !== 'function') {
167
+ problems.push(`${name}: 产物没有导出 apply(外壳挂不上这个模块)`);
168
+ continue;
169
+ }
170
+ try {
171
+ apply(ctx);
172
+ } catch (error) {
173
+ problems.push(`${name}: apply 抛了 —— ${error.message}`);
174
+ continue;
175
+ }
176
+
177
+ const card = registered?.card;
178
+ if (typeof card !== 'function') {
179
+ problems.push(`${name}: apply 之后没登记卡片(registerUnit 没拿到 card)`);
180
+ continue;
181
+ }
182
+
183
+ const props = {
184
+ world: { current: {} },
185
+ t: (key) => key,
186
+ refreshToken: 0,
187
+ onRefresh: () => {},
188
+ onNavigate: () => {},
189
+ onOpenMarket: () => {},
190
+ onSummary: () => {},
191
+ };
192
+
193
+ let tree;
194
+ try {
195
+ tree = card(props);
196
+ } catch (error) {
197
+ problems.push(`${name}: 空数据下卡片抛了 —— ${error.message}`);
198
+ continue;
199
+ }
200
+
201
+ if (tree === null || tree === undefined) {
202
+ problems.push(`${name}: 空数据下卡片返回空(界面上就是一片空白)`);
203
+ continue;
204
+ }
205
+
206
+ const budget = { left: 20000, maxDepth: 0 };
207
+ let count;
208
+ try {
209
+ count = render(tree, 0, budget);
210
+ } catch (error) {
211
+ const where = (error.stack ?? '').split('\n').slice(1, 4).join(' | ');
212
+ problems.push(`${name}: 卡片体内的组件抛了 —— ${error.message}\n ${where}`);
213
+ continue;
214
+ }
215
+ if (count <= 1) {
216
+ problems.push(`${name}: 卡片只画出一个节点 —— 卡片体没跑(或什么都没画)`);
217
+ continue;
218
+ }
219
+ rows.push(`${name}: 空数据下画得出 ${count} 个节点(最深 ${budget.maxDepth} 层)`);
220
+ }
221
+
222
+ if (problems.length > 0) {
223
+ console.error(`✖ 卡片渲染 ${problems.length} 条不过:`);
224
+ for (const problem of problems) console.error(` · ${problem}`);
225
+ process.exit(1);
226
+ }
227
+
228
+ console.log(`✔ 卡片渲染通过:${rows.length} 张卡片在**空数据**下都画得出来(不许抛)`);
229
+ for (const row of rows) console.log(` ${row}`);
@@ -0,0 +1,183 @@
1
+ // 跨包的**模块契约**检查(S2 的第一块:门禁读包的自述清单,不扫源码)。
2
+ //
3
+ // 为什么需要它:dsh 的组合阶段**不校验** `dsh.client.external` —— 实测把一个不存在的
4
+ // 外部声明(`@dsh-workbench/core-nope/client`)写进模块包,它被原样收进启动图、
5
+ // 宿主侧一声不吭、程序照起;错的声明要等浏览器 `require` 那一刻才炸。
6
+ //
7
+ // 判据全部来自各包 package.json 里的 `dsh` 字段(**自述清单**):
8
+ // · `dsh.bundle.patch` 指的补丁文件真的在;
9
+ // · `dsh.client.platform` 是认识的平台;
10
+ // · `dsh.client` 的 inject / external 每一条都能落到图里的一行(seed 词 / 平台包 / workspace 包);
11
+ // · `dsh.workbench`(**这是个模块**):
12
+ // - `unit` 形状合法、全局唯一(撞了就是装上去当场抛);
13
+ // - `faces.host` / `faces.client` 指的入口文件真的在;
14
+ // - 对壳有 peer 依赖(它跑在外壳交出来的服务上);
15
+ // - 有自己的接线自检脚本(模块侧也必须机器判据,不能只靠人眼)。
16
+
17
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
18
+ import { createRequire } from 'node:module';
19
+ import { join, relative } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { discoverPackages } from '../discover.mjs';
22
+
23
+ const ROOT = process.env.DSH_WB_ROOT ?? fileURLToPath(new URL('../../../../', import.meta.url));
24
+
25
+ /** dsh 浏览器侧的固定 seed 词表(模块表里天生就有的那几个)。 */
26
+ const SEED = new Set(['react', 'react-dom', 'cordis', 'dsh-client-store', 'ui-slots', 'ui-primitives', 'ui-dockkit']);
27
+
28
+ const PLATFORMS = new Set(['web']);
29
+
30
+ const UNIT_ID = /^[a-z][a-z0-9-]*$/;
31
+
32
+ const SHELL = '@dsh-workbench/core';
33
+
34
+ function installed(specifier, from) {
35
+ try {
36
+ createRequire(join(from, 'package.json')).resolve(`${packageOf(specifier)}/package.json`);
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ function packageOf(specifier) {
44
+ const parts = specifier.split('/');
45
+ return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
46
+ }
47
+
48
+ const readPackages = () => discoverPackages({ root: process.cwd() });
49
+
50
+ const packages = readPackages();
51
+ const known = new Set(packages.map((pkg) => pkg.manifest.name));
52
+ const problems = [];
53
+ const units = new Map();
54
+ let declared = 0;
55
+ let modules = 0;
56
+
57
+ for (const pkg of packages) {
58
+ const name = pkg.manifest.name;
59
+ const dsh = pkg.manifest.dsh;
60
+ if (dsh === undefined) continue;
61
+ declared += 1;
62
+
63
+ if (dsh.bundle?.patch !== undefined && !existsSync(join(pkg.dir, dsh.bundle.patch))) {
64
+ problems.push(`${name}: dsh.bundle.patch 指的文件不存在 —— ${dsh.bundle.patch}`);
65
+ }
66
+
67
+ if (dsh.client !== undefined) {
68
+ if (!PLATFORMS.has(dsh.client.platform)) {
69
+ problems.push(`${name}: dsh.client.platform 不认识 —— ${String(dsh.client.platform)}`);
70
+ }
71
+ for (const field of ['inject', 'external']) {
72
+ for (const specifier of dsh.client[field] ?? []) {
73
+ const resolvable =
74
+ SEED.has(specifier) ||
75
+ packageOf(specifier).startsWith('@deepseek-ai/') ||
76
+ known.has(packageOf(specifier)) ||
77
+ installed(specifier, pkg.dir);
78
+ if (!resolvable) {
79
+ problems.push(
80
+ `${name}: dsh.client.${field} 里的 ${JSON.stringify(specifier)} 落不到图里任何一行 —— ` +
81
+ `既不是 seed 词、也不是平台包、也不在 workspace 里、也不是装进来的依赖(dsh 自己不校验这条,错的声明要等浏览器 require 才炸)`,
82
+ );
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ if (dsh.workbench === undefined) continue;
89
+ modules += 1;
90
+
91
+ const unit = dsh.workbench.unit;
92
+ if (typeof unit !== 'string' || !UNIT_ID.test(unit)) {
93
+ problems.push(`${name}: dsh.workbench.unit 形状不对 —— ${JSON.stringify(unit)}(要小写连字符)`);
94
+ } else if (units.has(unit)) {
95
+ problems.push(
96
+ `${name}: 单元 id ${JSON.stringify(unit)} 与 ${units.get(unit)} 撞了 —— ` +
97
+ `两个包都声明同一个单元,装上去注册表当场抛(id 会进持久化配置)`,
98
+ );
99
+ } else {
100
+ units.set(unit, name);
101
+ const shellPkg = packages.find((item) => item.manifest.name === SHELL);
102
+ if (shellPkg !== undefined && existsSync(join(shellPkg.dir, 'src/modules', unit, 'unit.ts'))) {
103
+ problems.push(
104
+ `${name}: 单元 ${JSON.stringify(unit)} 在外壳里还有一份(${SHELL}/src/modules/${unit}/)—— ` +
105
+ `同一个单元不许两边都住:两份真相,装上模块注册表当场抛`,
106
+ );
107
+ }
108
+ }
109
+
110
+ for (const face of ['host', 'client']) {
111
+ const rel = dsh.workbench.faces?.[face];
112
+ if (typeof rel !== 'string') {
113
+ problems.push(`${name}: dsh.workbench.faces.${face} 没声明`);
114
+ } else if (!existsSync(join(pkg.dir, rel))) {
115
+ problems.push(`${name}: dsh.workbench.faces.${face} 指的文件不存在 —— ${rel}`);
116
+ }
117
+ }
118
+
119
+ const publishable = pkg.manifest.private !== true;
120
+ const registry = pkg.manifest.publishConfig?.registry;
121
+ const isPublic = pkg.manifest.publishConfig?.access === 'public';
122
+ if (publishable && !isPublic && (typeof registry !== 'string' || registry.includes('registry.npmjs.org'))) {
123
+ problems.push(
124
+ `${name}: 它不是 private,却既没声明 publishConfig.access=public、也没有指向内网源的 registry —— ` +
125
+ `发布目标必须显式写出来(公网 / 内网),否则等于把私有模块发到公共源`,
126
+ );
127
+ }
128
+
129
+ if (pkg.manifest.peerDependencies?.[SHELL] === undefined) {
130
+ problems.push(
131
+ `${name}: 没有对 ${SHELL} 的 peer 依赖 —— 模块跑在外壳交出来的服务上,装的时候必须能说清版本`,
132
+ );
133
+ }
134
+
135
+ for (const script of ['build', 'typecheck', 'test', 'selfcheck']) {
136
+ if (pkg.manifest.scripts?.[script] === undefined) {
137
+ problems.push(
138
+ `${name}: 没有 ${script} 脚本 —— 模块包必须自带整套门禁:` +
139
+ `外壳的 arch / 覆盖率 / 运行时边界都只扫自己的 src/,模块出包之后没人替它跑`,
140
+ );
141
+ }
142
+ }
143
+ }
144
+
145
+ const unitList = [...units.entries()].map(([unit, name]) => `${unit} ← ${name}`).join(' · ');
146
+
147
+ const shellPkg = packages.find((pkg) => pkg.manifest.name === SHELL);
148
+ if (shellPkg !== undefined) {
149
+ const suspects = [join(shellPkg.dir, 'package.json')];
150
+ const srcDir = join(shellPkg.dir, 'src');
151
+ if (existsSync(srcDir)) {
152
+ const walk = (dir) => {
153
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
154
+ const full = join(dir, entry.name);
155
+ if (entry.isDirectory()) walk(full);
156
+ else if (/\.tsx?$/.test(entry.name)) suspects.push(full);
157
+ }
158
+ };
159
+ walk(srcDir);
160
+ }
161
+ for (const file of suspects) {
162
+ const text = readFileSync(file, 'utf8');
163
+ const hit = /@pagoda\/dsh-workbench-module-[a-z0-9-]+/.exec(text);
164
+ if (hit !== null) {
165
+ problems.push(
166
+ `${SHELL}: ${relative(ROOT, file)} 里点名了模块包 ${hit[0]} —— ` +
167
+ `外壳不许认识任何模块(那等于把"中央清单"搬进外壳);模块靠服务名挂进来`,
168
+ );
169
+ }
170
+ }
171
+ }
172
+
173
+ if (problems.length > 0) {
174
+ console.error(`✖ 模块契约 ${problems.length} 条不过(${declared} 个带 dsh 声明的包,其中 ${modules} 个模块):`);
175
+ for (const problem of problems) console.error(` · ${problem}`);
176
+ process.exit(1);
177
+ }
178
+
179
+ console.log(
180
+ `✔ 模块契约通过:${declared} 个带 dsh 声明的包(${modules} 个模块)· bundle 落点存在 · ` +
181
+ `inject/external 都能落到图里 · 单元 id 唯一 · 面文件与 peer 都在 · 各有接线自检`,
182
+ );
183
+ console.log(` 单元:${unitList}`);
@@ -0,0 +1,53 @@
1
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { discoverPackages } from '../discover.mjs';
4
+
5
+ const MARKERS = [
6
+ ['内网 cmdb 域名', /pagoda\.com\.cn/],
7
+ ['内网 coding.net 企业名', /\bbaiguokeji\b/i],
8
+ ['内网 coding.net 团队名', new RegExp('UED' + 'YHSJ')],
9
+ ['公司内部叫法', new RegExp('\u767e' + '\u679c')],
10
+ ['内网 npm 源', new RegExp('[a-z0-9-]+\\.pkg\\.coding\\.net', 'i')],
11
+ ];
12
+
13
+ const SKIP = new Set(['node_modules', 'dist', 'coverage', '.git', '.turbo']);
14
+
15
+ function walk(dir, acc = []) {
16
+ for (const entry of readdirSync(dir)) {
17
+ if (SKIP.has(entry)) continue;
18
+ const full = join(dir, entry);
19
+ if (statSync(full).isDirectory()) walk(full, acc);
20
+ else acc.push(full);
21
+ }
22
+ return acc;
23
+ }
24
+
25
+ const publicPackages = discoverPackages({ root: process.cwd() }).filter(
26
+ (pkg) => pkg.manifest.publishConfig?.access === 'public',
27
+ );
28
+ const problems = [];
29
+ for (const pkg of publicPackages) {
30
+ for (const file of walk(pkg.dir)) {
31
+ let text;
32
+ try {
33
+ text = readFileSync(file, 'utf8');
34
+ } catch {
35
+ continue;
36
+ }
37
+ text.split('\n').forEach((line, index) => {
38
+ for (const [label, pattern] of MARKERS) {
39
+ if (pattern.test(line)) {
40
+ problems.push(`${pkg.manifest.name}: ${relative(pkg.dir, file)}:${index + 1} 命中${label} —— ${line.trim().slice(0, 120)}`);
41
+ }
42
+ }
43
+ });
44
+ }
45
+ }
46
+
47
+ if (problems.length > 0) {
48
+ console.error(`✖ 公开包里不许有公司内部标识:${problems.length} 处`);
49
+ for (const problem of problems) console.error(` · ${problem}`);
50
+ console.error(' (发布前扫一遍:内网源地址、内网域名、企业内部名都不该出现在公网包里)');
51
+ process.exit(1);
52
+ }
53
+ console.log(`✔ 公开包无内部标识:${publicPackages.length} 个 access=public 的包扫过`);
@@ -0,0 +1,102 @@
1
+ // 模块包的**源码边界**(S2 第三块:门禁跟着自述清单走到模块包)。
2
+ //
3
+ // 外壳那份 `arch/rules/units.mjs` 是按**路径**发现套件的(`src/modules/<id>/unit.ts`),
4
+ // 模块搬出 `src/` 之后它照跑但不报。那几条规则守的多是「外壳不许认识套件」——对模块包没意义;
5
+ // 模块包自己要守的是另外四条,写在下面。判据全部来自源码里的 import 说明符,覆盖范围跟着
6
+ // `dsh.workbench` 自述清单走(新加模块不用改这里)。
7
+
8
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
9
+ import { join, relative } from 'node:path';
10
+ import { discoverModulePackages } from '../discover.mjs';
11
+
12
+
13
+ const IMPORT = /from\s+'([^']+)'|import\s+'([^']+)'/g;
14
+
15
+ function sourceFiles(dir, acc = []) {
16
+ for (const entry of readdirSync(dir)) {
17
+ const full = join(dir, entry);
18
+ if (statSync(full).isDirectory()) {
19
+ if (entry === '__tests__' || entry === 'node_modules') continue;
20
+ sourceFiles(full, acc);
21
+ } else if (/\.tsx?$/.test(entry) && !/\.test\.tsx?$/.test(entry)) {
22
+ acc.push(full);
23
+ }
24
+ }
25
+ return acc;
26
+ }
27
+
28
+ function hasPlatformPackage(manifest) {
29
+ const names = Object.keys({ ...(manifest.dependencies ?? {}), ...(manifest.devDependencies ?? {}) });
30
+ return names.some((name) => /(^|\/)platform-/.test(name));
31
+ }
32
+
33
+ const modulePackages = () => discoverModulePackages({ root: process.cwd() });
34
+
35
+ const problems = [];
36
+ let scanned = 0;
37
+
38
+ for (const pkg of modulePackages()) {
39
+ const name = pkg.manifest.name;
40
+ const unit = pkg.manifest.dsh.workbench.unit;
41
+ const srcDir = join(pkg.dir, 'src');
42
+ const viaPlatform = hasPlatformPackage(pkg.manifest);
43
+
44
+ if (viaPlatform && existsSync(join(srcDir, 'platforms'))) {
45
+ problems.push(
46
+ `${name}: 已经改用平台包了,src/platforms/ 就别再留一份 —— 平台知识两处都有,改平台要改两处`,
47
+ );
48
+ }
49
+
50
+ for (const file of sourceFiles(srcDir)) {
51
+ scanned += 1;
52
+ const rel = relative(pkg.dir, file);
53
+ const text = readFileSync(file, 'utf8');
54
+
55
+ for (const match of text.matchAll(IMPORT)) {
56
+ const specifier = match[1] ?? match[2];
57
+ if (specifier === undefined) continue;
58
+
59
+ if (specifier.startsWith('@deepseek-ai/')) {
60
+ problems.push(
61
+ `${name}: ${rel} 里 import 了 ${specifier} —— 平台契约要按需自己声明,不许直接吃 dsh 的包`,
62
+ );
63
+ continue;
64
+ }
65
+ if (specifier === '@/app' || specifier.startsWith('@/app/')) {
66
+ problems.push(
67
+ `${name}: ${rel} 里 import 了 ${specifier} —— 模块不许伸进外壳的入口/外壳层,只能走 kit 与 mount(deps)`,
68
+ );
69
+ continue;
70
+ }
71
+ const cross = /^@\/modules\/([^/]+)\//.exec(specifier);
72
+ if (cross !== null && cross[1] !== unit) {
73
+ problems.push(
74
+ `${name}: ${rel} 里 import 了 ${specifier} —— 模块之间不许互相 import(另一个套件没装就没有这份源码)`,
75
+ );
76
+ continue;
77
+ }
78
+ const platform = /^@\/platforms\/([^/]+)\/(.+)$/.exec(specifier);
79
+ if (platform !== null && viaPlatform) {
80
+ problems.push(
81
+ `${name}: ${rel} 里 import 了 ${specifier} —— 这个模块已经改用平台包了,平台知识走包名` +
82
+ `(比如 @pagoda-dsh-workbench/platform-coding),不许再用 @/platforms/*`,
83
+ );
84
+ } else if (platform !== null && !existsSync(join(srcDir, 'platforms', `${platform[2]}.ts`))) {
85
+ problems.push(
86
+ `${name}: ${rel} 里 import 了 ${specifier},但本包 src/platforms/${platform[2]}.ts 不存在 —— ` +
87
+ `平台知识要么放在自己包里的 src/platforms/,要么整个走平台包,两条只能选一条`,
88
+ );
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ if (problems.length > 0) {
95
+ console.error(`✖ 模块源码边界 ${problems.length} 条不过:`);
96
+ for (const problem of problems) console.error(` · ${problem}`);
97
+ process.exit(1);
98
+ }
99
+
100
+ console.log(
101
+ `✔ 模块源码边界通过:${scanned} 个源文件 · 不吃平台包 · 不伸进外壳 app/ · 不跨模块 · 平台知识要么在自己包里、要么走平台包(不许两样都有)`,
102
+ );
@@ -0,0 +1,65 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, renameSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { discoverModulePackages, discoverPackages } from '../discover.mjs';
5
+
6
+ const SHELL = '@dsh-workbench/core';
7
+
8
+ const shell = discoverPackages({ root: process.cwd() }).find((pkg) => pkg.manifest.name === SHELL);
9
+ if (shell === undefined || !existsSync(join(shell.dir, 'src'))) {
10
+ console.log(`✔ 独立构建:本仓没有 ${SHELL} 的源码(模块侧靠"不许 @/ 指外壳"那条判据守)`);
11
+ process.exit(0);
12
+ }
13
+
14
+ const modules = discoverModulePackages({ root: process.cwd() });
15
+ const visible = join(shell.dir, 'src');
16
+ const hidden = join(shell.dir, 'src.hidden');
17
+
18
+ const restore = () => {
19
+ if (existsSync(hidden) && !existsSync(visible)) renameSync(hidden, visible);
20
+ };
21
+ process.on('SIGINT', () => {
22
+ restore();
23
+ process.exit(130);
24
+ });
25
+
26
+ if (existsSync(hidden)) {
27
+ console.error(`上次没还原干净:${hidden} 还在,手动改名回去再跑。`);
28
+ process.exit(1);
29
+ }
30
+
31
+ const rows = [];
32
+ const problems = [];
33
+ renameSync(visible, hidden);
34
+ try {
35
+ for (const pkg of modules) {
36
+ const name = pkg.manifest.name;
37
+ for (const script of ['build', 'typecheck']) {
38
+ if (pkg.manifest.scripts?.[script] === undefined) continue;
39
+ try {
40
+ execFileSync('pnpm', ['run', script], { cwd: pkg.dir, stdio: 'pipe' });
41
+ rows.push(`${name} ${script}: ✅`);
42
+ } catch (error) {
43
+ const output = `${error.stdout ?? ''}${error.stderr ?? ''}`;
44
+ const first = output
45
+ .split('\n')
46
+ .filter((line) => /error|Cannot find|Could not resolve|ERR_/.test(line))
47
+ .slice(0, 2)
48
+ .map((line) => line.trim())
49
+ .join(' | ');
50
+ rows.push(`${name} ${script}: ❌`);
51
+ problems.push(`${name} ${script} 在外壳源码不在场时失败 —— ${first.slice(0, 220)}`);
52
+ }
53
+ }
54
+ }
55
+ } finally {
56
+ restore();
57
+ }
58
+
59
+ if (problems.length > 0) {
60
+ console.error(`✖ 独立构建 ${problems.length} 条不过:`);
61
+ for (const problem of problems) console.error(` · ${problem}`);
62
+ console.error(' (上面的清单就是"发布出去会散架"的地方:模块只许依赖已发布的接口)');
63
+ process.exit(1);
64
+ }
65
+ console.log(`✔ 独立构建通过:${modules.length} 个模块包在外壳源码不在场时都能 build + typecheck`);
@@ -0,0 +1,46 @@
1
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { discoverModulePackages } from '../discover.mjs';
4
+
5
+ const SHELL_SELECTOR = /(^|[\s,>+~])\.wb-[a-z0-9-]+/;
6
+ const RULE = /\.wb-[a-z0-9-]+[^{}]*\{/;
7
+
8
+ function walk(dir, acc = []) {
9
+ for (const entry of readdirSync(dir)) {
10
+ if (entry === 'node_modules' || entry === 'dist') continue;
11
+ const full = join(dir, entry);
12
+ if (statSync(full).isDirectory()) walk(full, acc);
13
+ else acc.push(full);
14
+ }
15
+ return acc;
16
+ }
17
+
18
+ const problems = [];
19
+ const modules = discoverModulePackages({ root: process.cwd() });
20
+ for (const pkg of modules) {
21
+ for (const file of walk(pkg.dir)) {
22
+ if (!/\.(css|ts|tsx)$/.test(file)) continue;
23
+ let text;
24
+ try {
25
+ text = readFileSync(file, 'utf8');
26
+ } catch {
27
+ continue;
28
+ }
29
+ text.split('\n').forEach((line, index) => {
30
+ if (!RULE.test(line)) return;
31
+ const selector = line.slice(0, line.indexOf('{'));
32
+ if (!SHELL_SELECTOR.test(selector)) return;
33
+ problems.push(
34
+ `${pkg.manifest.name}: ${relative(pkg.dir, file)}:${index + 1} 定义了外壳前缀的 CSS 规则 —— ${line.trim().slice(0, 100)}`,
35
+ );
36
+ });
37
+ }
38
+ }
39
+
40
+ if (problems.length > 0) {
41
+ console.error(`✖ 模块不许定义外壳前缀的 CSS 规则:${problems.length} 处`);
42
+ for (const problem of problems) console.error(` · ${problem}`);
43
+ console.error(' (wb-* 是全局类名:两份同名规则按注入顺序互相覆盖,用户看到的是随机的那个)');
44
+ process.exit(1);
45
+ }
46
+ console.log(`✔ 样式归属清楚:${modules.length} 个模块都没有定义外壳前缀(wb-*)的 CSS 规则`);
@@ -0,0 +1,50 @@
1
+
2
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+
5
+ function manifestOf(dir) {
6
+ const file = join(dir, 'package.json');
7
+ if (!existsSync(file)) return undefined;
8
+ try {
9
+ return JSON.parse(readFileSync(file, 'utf8'));
10
+ } catch {
11
+ return undefined;
12
+ }
13
+ }
14
+
15
+ function workspaceRootOf(start) {
16
+ if (process.env.DSH_WB_ROOT) return process.env.DSH_WB_ROOT;
17
+ let dir = start;
18
+ for (;;) {
19
+ if (existsSync(join(dir, 'packages')) || existsSync(join(dir, 'pnpm-workspace.yaml'))) return dir;
20
+ const parent = dirname(dir);
21
+ if (parent === dir) return start;
22
+ dir = parent;
23
+ }
24
+ }
25
+
26
+ export function discoverPackages({ root = process.cwd() } = {}) {
27
+ const workspace = workspaceRootOf(root);
28
+ const dirs = [];
29
+ const packagesDir = join(workspace, 'packages');
30
+ if (existsSync(packagesDir)) {
31
+ for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
32
+ if (entry.isDirectory()) dirs.push(join(packagesDir, entry.name));
33
+ }
34
+ }
35
+ dirs.push(root, workspace);
36
+ const seen = new Set();
37
+ const found = [];
38
+ for (const dir of dirs) {
39
+ if (seen.has(dir)) continue;
40
+ seen.add(dir);
41
+ const manifest = manifestOf(dir);
42
+ if (manifest === undefined || typeof manifest.name !== 'string') continue;
43
+ found.push({ dir, manifest });
44
+ }
45
+ return found;
46
+ }
47
+
48
+ export function discoverModulePackages(options = {}) {
49
+ return discoverPackages(options).filter((pkg) => pkg.manifest.dsh?.workbench !== undefined);
50
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ export { build } from './build.mjs';
2
+ export { check } from './check.mjs';
package/src/init.mjs ADDED
@@ -0,0 +1,48 @@
1
+ import { cpSync, existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const TEMPLATE = fileURLToPath(new URL('../template', import.meta.url));
6
+ const UNIT_ID = /^[a-z][a-z0-9-]*$/;
7
+
8
+ export async function init({ root, args = [] }) {
9
+ const unit = args[0];
10
+ if (unit === undefined || !UNIT_ID.test(unit)) {
11
+ console.error('用法:dsh-wb-module init <unit> [--dir <目录>] [--description <文字>]');
12
+ console.error(' unit 是小写短横线标识(它同时是单元 id、包名后缀、卡片 class 前缀)');
13
+ process.exit(2);
14
+ }
15
+ const at = args.indexOf('--dir');
16
+ const target = at === -1 ? join(root, `module-${unit}`) : args[at + 1];
17
+ const description =
18
+ args[args.indexOf('--description') + 1] ?? `${unit} 模块:独立 dsh 插件包`;
19
+ if (existsSync(target) && readdirSync(target).length > 0) {
20
+ console.error(`${target} 已经存在且不是空目录,不覆盖`);
21
+ process.exit(2);
22
+ }
23
+ const tokens = {
24
+ __UNIT__: unit,
25
+ __NAME__: `@dsh-workbench/module-${unit}`,
26
+ __DESC__: description,
27
+ };
28
+ const fill = (text) => text.replace(/__[A-Z]+__/g, (token) => tokens[token] ?? token);
29
+ cpSync(TEMPLATE, target, { recursive: true });
30
+ const walk = (dir) => {
31
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
32
+ const full = join(dir, entry.name);
33
+ if (entry.isDirectory()) {
34
+ walk(full);
35
+ continue;
36
+ }
37
+ if (!/\.(tpl|ts|tsx|json|md|yml)$/.test(entry.name)) continue;
38
+ writeFileSync(full, fill(readFileSync(full, 'utf8')));
39
+ if (/\.tpl$/.test(entry.name)) {
40
+ const stripped = join(dir, entry.name.replace(/\.tpl$/, ''));
41
+ cpSync(full, stripped);
42
+ rmSync(full);
43
+ }
44
+ }
45
+ };
46
+ walk(target);
47
+ console.log(`${target} 建好了:${unit} 模块骨架(build / check / selfcheck 已接工具)`);
48
+ }
@@ -0,0 +1,18 @@
1
+ # __NAME__
2
+
3
+ `__UNIT__` 模块:一个独立的 dsh 插件包,只认外壳的服务名,不认识外壳源码。
4
+
5
+ ```bash
6
+ dsh-wb-module build # 出 dist/host.js 与套了 loader 壳的 client.js
7
+ dsh-wb-module check # 契约 / 产物边界 / 源码边界 / 卡片渲染
8
+ ```
9
+
10
+ 装进本地工作台:
11
+
12
+ ```bash
13
+ dsh plugin --profile workbench add <本目录>
14
+ ```
15
+
16
+ 面板只读、写入只走 agent 工具;共享代码从 `@dsh-workbench/shared` 拿,
17
+ 单例机制(hooks / 容器组件 / 读缝)只能从 `@dsh-workbench/core/client` 的 kit 拿。
18
+ 要发布:把 `package.json` 里的 `private` 去掉、按需改 `publishConfig`。
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: workbench-module-__UNIT__
3
+ name: '__NAME__'
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "__NAME__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "__DESC__",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./dist/host.js",
9
+ "./client": "./client.js"
10
+ },
11
+ "files": ["dist", "client.js", "cordis.patch.yml"],
12
+ "scripts": {
13
+ "build": "dsh-wb-module build",
14
+ "typecheck": "tsc --noEmit",
15
+ "test": "vitest run",
16
+ "selfcheck": "dsh-wb-module build && dsh-wb-module check",
17
+ "prepack": "dsh-wb-module build"
18
+ },
19
+ "dsh": {
20
+ "bundle": { "patch": "./cordis.patch.yml" },
21
+ "client": {
22
+ "platform": "web",
23
+ "inject": ["@dsh-workbench/core"],
24
+ "external": ["@dsh-workbench/core/client"]
25
+ },
26
+ "workbench": {
27
+ "unit": "__UNIT__",
28
+ "faces": { "host": "src/host.ts", "client": "src/client.tsx" }
29
+ }
30
+ },
31
+ "peerDependencies": { "@dsh-workbench/core": "^0.1.0", "react": "*" },
32
+ "peerDependenciesMeta": { "react": { "optional": true } },
33
+ "devDependencies": {
34
+ "@dsh-workbench/core": "^0.1.0",
35
+ "@dsh-workbench/module-tools": "^0.1.0",
36
+ "@types/node": "^24.19.0",
37
+ "@types/react": "^19.3.0",
38
+ "react": "^19.3.0",
39
+ "typescript": "5.9",
40
+ "vitest": "^5.0.2"
41
+ },
42
+ "publishConfig": { "access": "public" }
43
+ }
@@ -0,0 +1,27 @@
1
+ import { createElement as h } from 'react';
2
+
3
+ const UNIT_ID = '__UNIT__';
4
+
5
+ export const inject = ['workbenchUnits'];
6
+
7
+ export function apply(ctx) {
8
+ const units = ctx.get('workbenchUnits');
9
+ if (units === undefined) {
10
+ console.warn(`[${UNIT_ID}] 外壳没有提供 workbenchUnits 服务:这张卡片装不进来`);
11
+ return;
12
+ }
13
+ ctx.effect(
14
+ () =>
15
+ units.registerUnit({
16
+ manifest: {
17
+ id: UNIT_ID,
18
+ kind: 'board',
19
+ card: { titleKey: `${UNIT_ID}.cardTitle`, order: 90, enabledByDefault: true },
20
+ tools: [],
21
+ host: { inject: [] },
22
+ },
23
+ card: () => h('section', { className: `wb-${UNIT_ID}` }, h('h2', null, `${UNIT_ID}`)),
24
+ }),
25
+ `${UNIT_ID}: 注册`,
26
+ );
27
+ }
@@ -0,0 +1,27 @@
1
+ const UNIT_ID = '__UNIT__';
2
+
3
+ export const name = '__NAME__';
4
+
5
+ export const inject = ['workbenchUnits'];
6
+
7
+ export function apply(ctx) {
8
+ const units = ctx.get('workbenchUnits');
9
+ if (units === undefined) {
10
+ ctx.logger?.warn?.(`[${UNIT_ID}] 外壳没有提供 workbenchUnits 服务:这个模块装不进来`);
11
+ return;
12
+ }
13
+ ctx.effect(
14
+ () =>
15
+ units.register({
16
+ manifest: {
17
+ id: UNIT_ID,
18
+ kind: 'board',
19
+ card: { titleKey: `${UNIT_ID}.cardTitle`, order: 90, enabledByDefault: true },
20
+ tools: [],
21
+ host: { inject: [] },
22
+ },
23
+ mount: () => ({ routes: {}, registerTools: () => [] }),
24
+ }),
25
+ `${UNIT_ID}: 注册`,
26
+ );
27
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022", "DOM"],
7
+ "jsx": "react",
8
+ "jsxFactory": "h",
9
+ "jsxFragmentFactory": "Fragment",
10
+ "strict": true,
11
+ "noUnusedLocals": true,
12
+ "noImplicitOverride": true,
13
+ "noEmit": true,
14
+ "skipLibCheck": true,
15
+ "isolatedModules": true,
16
+ "verbatimModuleSyntax": true,
17
+ "paths": { "@/modules/__UNIT__/*": ["src/*"] }
18
+ },
19
+ "include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*.d.ts"]
20
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 浏览器半侧的运行时垫片。
3
+ *
4
+ * dsh 的客户端模块格式是 `window.__ModuleLoader__.load({ id, factory(require) })`,
5
+ * 所以 `require` 不来自模块系统,而是宿主给的作用域。
6
+ */
7
+
8
+ /**
9
+ * React 19 的 `@types/react` 移除了全局 `JSX` 命名空间,而本工程用的是
10
+ * **经典 JSX 运行时**(`jsxFactory: "h"`):`react/jsx-runtime` 不在浏览器模块表里,
11
+ * 所以不能切到自动运行时。这里把全局命名空间接到 `React.JSX` 上 —— 这是官方迁移说明
12
+ * 给经典运行时的做法,写完 JSX 就有完整类型,产出仍是 `h(...)`。
13
+ */
14
+ import type * as ReactTypes from 'react';
15
+
16
+ declare global {
17
+ namespace JSX {
18
+ interface Element extends ReactTypes.JSX.Element {}
19
+ interface ElementClass extends ReactTypes.JSX.ElementClass {}
20
+ interface IntrinsicElements extends ReactTypes.JSX.IntrinsicElements {}
21
+ /** 有了它,返回 ReactNode 的组件(含 Fragment)才合法 —— React 19 的类型靠这条判断。 */
22
+ type ElementType = ReactTypes.JSX.ElementType;
23
+ interface IntrinsicAttributes extends ReactTypes.JSX.IntrinsicAttributes {}
24
+ interface ElementAttributesProperty extends ReactTypes.JSX.ElementAttributesProperty {}
25
+ interface ElementChildrenAttribute extends ReactTypes.JSX.ElementChildrenAttribute {}
26
+ }
27
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.