dsh-my-computer 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +257 -2
- package/cordis.patch.yml +11 -0
- package/lib/client.js +1566 -0
- package/lib/index.js +840 -0
- package/lib/zip.js +365 -0
- package/package.json +56 -4
- package/tools/client-check.mjs +429 -0
- package/tools/right-tab-check.mjs +118 -0
- package/tools/smoke.mjs +489 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// dsh-my-computer 客户端 bundle 自检
|
|
3
|
+
//
|
|
4
|
+
// 这个脚本**不能**证明 DSH 会真的把面板渲染出来(那只有装进 profile、重启 dsh web、
|
|
5
|
+
// 刷新页面才算),但它能在没有浏览器的前提下验证最容易写错的那几件事:
|
|
6
|
+
// 1. lib/client.js 能被解析(语法没坏);
|
|
7
|
+
// 2. 它调用的是 window.__ModuleLoader__.load({ id, factory }) 这个约定;
|
|
8
|
+
// 3. factory 返回的模块里有 apply / inject,apply 是函数;
|
|
9
|
+
// 4. apply(ctx) 注册了 sidebar.panellist(id=my-computer, order=-5, label=我的电脑)
|
|
10
|
+
// 和 main(key=my-computer),两个组件都是函数;
|
|
11
|
+
// 5. 面板能渲染:空目录态、有内容的列表态、删除确认弹窗、预览弹窗
|
|
12
|
+
// (用 React 真身 + 手搭的最小 hook dispatcher;不执行 useEffect,因此不发请求)。
|
|
13
|
+
//
|
|
14
|
+
// 跑法:node tools/client-check.mjs
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs'
|
|
17
|
+
import path from 'node:path'
|
|
18
|
+
import vm from 'node:vm'
|
|
19
|
+
import { createRequire } from 'node:module'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
|
|
22
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
23
|
+
const CLIENT = path.resolve(HERE, '../lib/client.js')
|
|
24
|
+
|
|
25
|
+
const checks = []
|
|
26
|
+
function check(name, pass, detail) {
|
|
27
|
+
checks.push({ name, pass: Boolean(pass), detail })
|
|
28
|
+
console.log((pass ? ' ✓' : ' ✗') + ' ' + name + (detail ? ' ' + detail : ''))
|
|
29
|
+
return Boolean(pass)
|
|
30
|
+
}
|
|
31
|
+
function section(title) {
|
|
32
|
+
console.log('\n── ' + title + ' ' + '─'.repeat(Math.max(0, 56 - title.length)))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log('dsh-my-computer 客户端 bundle 自检')
|
|
36
|
+
console.log(' 文件:' + CLIENT)
|
|
37
|
+
|
|
38
|
+
const source = fs.readFileSync(CLIENT, 'utf8')
|
|
39
|
+
|
|
40
|
+
section('bundle 形状')
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
new vm.Script(source, { filename: CLIENT })
|
|
44
|
+
check('bundle 语法可解析', true, source.length + ' 字节')
|
|
45
|
+
} catch (error) {
|
|
46
|
+
check('bundle 语法可解析', false, String(error))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const requireFrom = createRequire(path.resolve(HERE, '../package.json'))
|
|
50
|
+
let React = null
|
|
51
|
+
let reactFrom = ''
|
|
52
|
+
for (const candidate of ['C:/Users/Administrator/.dsh/profiles/web/node_modules/react', 'react']) {
|
|
53
|
+
try {
|
|
54
|
+
React = requireFrom(candidate)
|
|
55
|
+
reactFrom = candidate
|
|
56
|
+
break
|
|
57
|
+
} catch {
|
|
58
|
+
/* 换下一个 */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
check('找到 react 运行时', Boolean(React), reactFrom || '未找到(渲染检查会跳过)')
|
|
62
|
+
|
|
63
|
+
const loaded = []
|
|
64
|
+
const styleNodes = []
|
|
65
|
+
const documentStub = {
|
|
66
|
+
querySelector() {
|
|
67
|
+
return null
|
|
68
|
+
},
|
|
69
|
+
createElement() {
|
|
70
|
+
const node = {
|
|
71
|
+
attributes: {},
|
|
72
|
+
textContent: '',
|
|
73
|
+
setAttribute(name, value) {
|
|
74
|
+
this.attributes[name] = value
|
|
75
|
+
},
|
|
76
|
+
remove() {
|
|
77
|
+
const at = styleNodes.indexOf(node)
|
|
78
|
+
if (at >= 0) styleNodes.splice(at, 1)
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
return node
|
|
82
|
+
},
|
|
83
|
+
head: {
|
|
84
|
+
appendChild(node) {
|
|
85
|
+
styleNodes.push(node)
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
body: { appendChild() {} },
|
|
89
|
+
}
|
|
90
|
+
const windowStub = {
|
|
91
|
+
__ModuleLoader__: {
|
|
92
|
+
load(entry) {
|
|
93
|
+
loaded.push(entry)
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let bundleError = null
|
|
99
|
+
try {
|
|
100
|
+
const run = new Function('window', 'document', 'require', 'globalThis', source)
|
|
101
|
+
run(
|
|
102
|
+
windowStub,
|
|
103
|
+
documentStub,
|
|
104
|
+
(name) => {
|
|
105
|
+
if (name === 'react' && React) return React
|
|
106
|
+
throw new Error('宿主要求了一个这里没有的模块:' + name)
|
|
107
|
+
},
|
|
108
|
+
globalThis,
|
|
109
|
+
)
|
|
110
|
+
} catch (error) {
|
|
111
|
+
bundleError = error
|
|
112
|
+
}
|
|
113
|
+
check('bundle 可以执行(没抛异常)', bundleError === null, bundleError ? String(bundleError) : '')
|
|
114
|
+
check('通过 __ModuleLoader__.load 注册了自己', loaded.length === 1, loaded.length + ' 次')
|
|
115
|
+
|
|
116
|
+
const entry = loaded[0] || {}
|
|
117
|
+
check('模块 id 是 dsh-my-computer', entry.id === 'dsh-my-computer', String(entry.id))
|
|
118
|
+
check('factory 是函数', typeof entry.factory === 'function')
|
|
119
|
+
|
|
120
|
+
const mod = entry.factory
|
|
121
|
+
? entry.factory((name) => {
|
|
122
|
+
if (name === 'react' && React) return React
|
|
123
|
+
throw new Error('未知模块:' + name)
|
|
124
|
+
})
|
|
125
|
+
: {}
|
|
126
|
+
|
|
127
|
+
check('exports.apply 是函数', typeof mod.apply === 'function')
|
|
128
|
+
check('exports.inject 是数组', Array.isArray(mod.inject), JSON.stringify(mod.inject))
|
|
129
|
+
|
|
130
|
+
section('apply(ctx) 注册的 slots')
|
|
131
|
+
|
|
132
|
+
const registrations = []
|
|
133
|
+
const tabRegistrations = []
|
|
134
|
+
const fakeClientCtx = {
|
|
135
|
+
effect(fn) {
|
|
136
|
+
const dispose = fn()
|
|
137
|
+
return typeof dispose === 'function' ? dispose : () => {}
|
|
138
|
+
},
|
|
139
|
+
// 真实宿主里 ctx.inject(names, cb) 会把依赖就绪后的服务集合交给 cb。
|
|
140
|
+
// 这里直接把 ctx 自己给过去(slots / sidebarRightTabs 都在上面)。
|
|
141
|
+
inject(names, callback) {
|
|
142
|
+
return callback(fakeClientCtx)
|
|
143
|
+
},
|
|
144
|
+
slots: {
|
|
145
|
+
inject(name, callback) {
|
|
146
|
+
return callback()
|
|
147
|
+
},
|
|
148
|
+
register(spec, component) {
|
|
149
|
+
registrations.push({ spec, component })
|
|
150
|
+
return () => {}
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
sidebarRightTabs: {
|
|
154
|
+
register(spec) {
|
|
155
|
+
tabRegistrations.push(spec)
|
|
156
|
+
return () => {}
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let applyError = null
|
|
162
|
+
try {
|
|
163
|
+
mod.apply(fakeClientCtx)
|
|
164
|
+
} catch (error) {
|
|
165
|
+
applyError = error
|
|
166
|
+
}
|
|
167
|
+
check('apply(ctx) 没抛异常', applyError === null, applyError ? String(applyError) : '')
|
|
168
|
+
check('注册了 >=2 个 slot', registrations.length >= 2, registrations.map((r) => r.spec && r.spec.name).join(', '))
|
|
169
|
+
check('注入了 <style>(样式跟随主题)', styleNodes.length === 1, styleNodes.length + ' 个 style 节点')
|
|
170
|
+
|
|
171
|
+
const side = registrations.find((r) => r.spec && r.spec.name === 'sidebar.panellist')
|
|
172
|
+
const main = registrations.find((r) => r.spec && r.spec.name === 'main')
|
|
173
|
+
|
|
174
|
+
check('有 sidebar.panellist 注册', Boolean(side))
|
|
175
|
+
check('侧栏 id 是 my-computer', side && side.spec.id === 'my-computer', side ? String(side.spec.id) : '')
|
|
176
|
+
check('侧栏 order 是 -5', side && side.spec.order === -5, side ? String(side.spec.order) : '')
|
|
177
|
+
check(
|
|
178
|
+
'侧栏 label 返回「我的电脑」',
|
|
179
|
+
side && typeof side.spec.label === 'function' && side.spec.label() === '我的电脑',
|
|
180
|
+
side && typeof side.spec.label === 'function' ? side.spec.label() : '',
|
|
181
|
+
)
|
|
182
|
+
check('侧栏图标是函数组件', side && typeof side.component === 'function')
|
|
183
|
+
|
|
184
|
+
check('有 main 注册', Boolean(main))
|
|
185
|
+
check('main.key 与侧栏 id 一致', main && side && main.spec.key === side.spec.id, main ? String(main.spec.key) : '')
|
|
186
|
+
check('面板是函数组件', main && typeof main.component === 'function')
|
|
187
|
+
|
|
188
|
+
/* 右栏 tab 三件套(sidebarRightTabs + pane.tab + pane.tab.title) */
|
|
189
|
+
const tab = tabRegistrations.find((spec) => spec && spec.id === 'my-computer')
|
|
190
|
+
const paneTab = registrations.find((r) => r.spec && r.spec.name === 'sidebar.right.pane.tab')
|
|
191
|
+
const paneTitle = registrations.find((r) => r.spec && r.spec.name === 'sidebar.right.pane.tab.title')
|
|
192
|
+
check('有 sidebarRightTabs 注册', Boolean(tab))
|
|
193
|
+
check('guide 非空(否则不进右栏「+」菜单)', Boolean(tab && Array.isArray(tab.guide) && tab.guide.length > 0))
|
|
194
|
+
check('右栏 pane.tab key 与 tab id 一致', Boolean(paneTab && tab && paneTab.spec.key === tab.id))
|
|
195
|
+
check('右栏 pane.tab.title 也注册了', Boolean(paneTitle && paneTitle.spec.key === 'my-computer'))
|
|
196
|
+
|
|
197
|
+
/* ------------------------------------------------------------------ *
|
|
198
|
+
* 手搭一个只够跑首屏的渲染器:真 React.createElement + 最小 hook dispatcher
|
|
199
|
+
* ------------------------------------------------------------------ */
|
|
200
|
+
|
|
201
|
+
function makeRenderer(React) {
|
|
202
|
+
const internals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
|
203
|
+
const dispatcherRef = internals && internals.ReactCurrentDispatcher
|
|
204
|
+
if (!dispatcherRef) return null
|
|
205
|
+
|
|
206
|
+
return function render(element, options = {}) {
|
|
207
|
+
const seen = { tags: [], classes: [], components: [] }
|
|
208
|
+
const seed = options.seed || null
|
|
209
|
+
let seededOnce = false
|
|
210
|
+
|
|
211
|
+
function walk(node, depth) {
|
|
212
|
+
if (node == null || typeof node === 'boolean') return
|
|
213
|
+
if (typeof node === 'string' || typeof node === 'number') return
|
|
214
|
+
if (Array.isArray(node)) {
|
|
215
|
+
node.forEach((child) => walk(child, depth))
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
if (typeof node.type === 'string') {
|
|
219
|
+
seen.tags.push(node.type)
|
|
220
|
+
const className = node.props && node.props.className
|
|
221
|
+
if (className) seen.classes.push(String(className))
|
|
222
|
+
walk(node.props && node.props.children, depth + 1)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
if (typeof node.type === 'function') {
|
|
226
|
+
if (depth > 14) throw new Error('渲染层级过深(疑似递归组件)')
|
|
227
|
+
const name = node.type.name || '匿名组件'
|
|
228
|
+
seen.components.push(name)
|
|
229
|
+
const store = []
|
|
230
|
+
const useSeed = seed && !seededOnce
|
|
231
|
+
if (useSeed) seededOnce = true
|
|
232
|
+
let index = 0
|
|
233
|
+
const previous = dispatcherRef.current
|
|
234
|
+
dispatcherRef.current = {
|
|
235
|
+
useState(initial) {
|
|
236
|
+
const at = index
|
|
237
|
+
index += 1
|
|
238
|
+
if (!(at in store)) {
|
|
239
|
+
const preset = useSeed && seed[at] !== undefined ? seed[at] : undefined
|
|
240
|
+
store[at] = preset !== undefined ? preset : typeof initial === 'function' ? initial() : initial
|
|
241
|
+
}
|
|
242
|
+
return [
|
|
243
|
+
store[at],
|
|
244
|
+
(next) => {
|
|
245
|
+
store[at] = typeof next === 'function' ? next(store[at]) : next
|
|
246
|
+
},
|
|
247
|
+
]
|
|
248
|
+
},
|
|
249
|
+
useReducer(reducer, initial) {
|
|
250
|
+
const at = index
|
|
251
|
+
index += 1
|
|
252
|
+
if (!(at in store)) store[at] = initial
|
|
253
|
+
return [store[at], () => {}]
|
|
254
|
+
},
|
|
255
|
+
useRef(initial) {
|
|
256
|
+
const at = index
|
|
257
|
+
index += 1
|
|
258
|
+
if (!(at in store)) store[at] = { current: initial }
|
|
259
|
+
return store[at]
|
|
260
|
+
},
|
|
261
|
+
useMemo(factory) {
|
|
262
|
+
index += 1
|
|
263
|
+
return factory()
|
|
264
|
+
},
|
|
265
|
+
useCallback(callback) {
|
|
266
|
+
index += 1
|
|
267
|
+
return callback
|
|
268
|
+
},
|
|
269
|
+
useEffect() {
|
|
270
|
+
index += 1
|
|
271
|
+
},
|
|
272
|
+
useLayoutEffect() {
|
|
273
|
+
index += 1
|
|
274
|
+
},
|
|
275
|
+
useContext(context) {
|
|
276
|
+
index += 1
|
|
277
|
+
return context && context._currentValue
|
|
278
|
+
},
|
|
279
|
+
useId() {
|
|
280
|
+
index += 1
|
|
281
|
+
return 'id-' + index
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
walk(node.type(node.props || {}), depth + 1)
|
|
286
|
+
} finally {
|
|
287
|
+
dispatcherRef.current = previous
|
|
288
|
+
}
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
throw new Error('未知节点类型:' + String(node.type))
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
walk(element, 0)
|
|
295
|
+
return seen
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
section('面板渲染(四个状态)')
|
|
300
|
+
|
|
301
|
+
if (React && main && typeof main.component === 'function') {
|
|
302
|
+
const render = makeRenderer(React)
|
|
303
|
+
if (!render) {
|
|
304
|
+
console.log(' · 跳过:这个 React 版本没有暴露 ReactCurrentDispatcher')
|
|
305
|
+
} else {
|
|
306
|
+
const driveA = { letter: 'C:', label: '系统盘', total: 100 * 1024 ** 3, free: 30 * 1024 ** 3 }
|
|
307
|
+
const driveB = { letter: 'D:', label: '本地磁盘', total: 500 * 1024 ** 3, free: 210 * 1024 ** 3 }
|
|
308
|
+
const entries = [
|
|
309
|
+
{ name: 'dsh-workspace', path: 'D:\\dsh-workspace', kind: 'dir', size: 0, mtime: Date.now(), ext: '' },
|
|
310
|
+
{ name: 'vault', path: 'D:\\dsh-workspace\\vault', kind: 'dir', size: 0, mtime: Date.now(), ext: '' },
|
|
311
|
+
{ name: 'README.md', path: 'D:\\dsh-workspace\\README.md', kind: 'file', size: 4096, mtime: Date.now(), ext: '.md' },
|
|
312
|
+
{ name: '快捷方式', path: 'D:\\link', kind: 'link', targetKind: 'dir', size: 0, mtime: Date.now(), ext: '' },
|
|
313
|
+
]
|
|
314
|
+
// 与 MyComputerPanel 的 hook 顺序一一对应:
|
|
315
|
+
// cwd, entries, drives, capacity, loading, error, notice, selected, query, pathInput, dialog, busy
|
|
316
|
+
const baseSeed = (dialog) => [
|
|
317
|
+
'D:\\dsh-workspace',
|
|
318
|
+
entries,
|
|
319
|
+
[driveA, driveB],
|
|
320
|
+
{ total: driveB.total, free: driveB.free },
|
|
321
|
+
false,
|
|
322
|
+
'',
|
|
323
|
+
'',
|
|
324
|
+
[],
|
|
325
|
+
'',
|
|
326
|
+
'D:\\dsh-workspace',
|
|
327
|
+
dialog || null,
|
|
328
|
+
'',
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
// A) 空状态(刚打开、还没探测到磁盘)
|
|
332
|
+
let empty = null
|
|
333
|
+
let emptyError = null
|
|
334
|
+
try {
|
|
335
|
+
empty = render(React.createElement(main.component, {}), {
|
|
336
|
+
seed: ['', [], [], { total: null, free: null }, true, '', '', [], '', '', null, ''],
|
|
337
|
+
})
|
|
338
|
+
} catch (error) {
|
|
339
|
+
emptyError = error
|
|
340
|
+
}
|
|
341
|
+
check('空状态渲染没抛异常', emptyError === null, emptyError ? String(emptyError) : '')
|
|
342
|
+
check('空状态有根容器 mc-root', empty && empty.classes.some((c) => c.indexOf('mc-root') === 0))
|
|
343
|
+
check('空状态显示「加载中」', empty && empty.classes.some((c) => c === 'mc-empty'))
|
|
344
|
+
|
|
345
|
+
// B) 有内容的列表
|
|
346
|
+
let list = null
|
|
347
|
+
let listError = null
|
|
348
|
+
try {
|
|
349
|
+
list = render(React.createElement(main.component, {}), { seed: baseSeed(null) })
|
|
350
|
+
} catch (error) {
|
|
351
|
+
listError = error
|
|
352
|
+
}
|
|
353
|
+
check('列表态渲染没抛异常', listError === null, listError ? String(listError) : '')
|
|
354
|
+
const rowCount = list ? list.classes.filter((c) => c === 'mc-row').length : 0
|
|
355
|
+
check('目录里的 4 个条目都渲染成行', rowCount === 4, rowCount + ' 行')
|
|
356
|
+
check('切盘区渲染出 2 个盘符按钮', list && list.classes.filter((c) => c.indexOf('mc-chip') === 0).length === 2)
|
|
357
|
+
check('面包屑渲染出来', list && list.classes.some((c) => c === 'mc-crumb' || c === 'mc-crumb last'))
|
|
358
|
+
check('工具栏有新建/上传按钮', list && list.tags.filter((t) => t === 'button').length >= 4)
|
|
359
|
+
check('行内操作按钮存在(改名/删除/下载)', list && list.classes.includes('mc-ibtn') && list.classes.includes('mc-ibtn danger'))
|
|
360
|
+
|
|
361
|
+
// C) 删除确认弹窗
|
|
362
|
+
let del = null
|
|
363
|
+
let delError = null
|
|
364
|
+
try {
|
|
365
|
+
del = render(React.createElement(main.component, {}), {
|
|
366
|
+
seed: baseSeed({ kind: 'delete', paths: ['D:\\dsh-workspace\\README.md'] }),
|
|
367
|
+
})
|
|
368
|
+
} catch (error) {
|
|
369
|
+
delError = error
|
|
370
|
+
}
|
|
371
|
+
check('删除确认弹窗渲染没抛异常', delError === null, delError ? String(delError) : '')
|
|
372
|
+
check('弹窗有遮罩与主体', del && del.classes.includes('mc-modal-bg') && del.classes.some((c) => c.indexOf('mc-modal') === 0))
|
|
373
|
+
check('弹窗里有「永久删除」按钮', del && del.components.indexOf('ModalShell') >= 0)
|
|
374
|
+
|
|
375
|
+
// D) 预览弹窗(图片直显)
|
|
376
|
+
let img = null
|
|
377
|
+
let imgError = null
|
|
378
|
+
try {
|
|
379
|
+
img = render(React.createElement(main.component, {}), {
|
|
380
|
+
seed: baseSeed({
|
|
381
|
+
kind: 'preview',
|
|
382
|
+
path: 'D:\\shot.png',
|
|
383
|
+
name: 'shot.png',
|
|
384
|
+
size: 20480,
|
|
385
|
+
group: 'image',
|
|
386
|
+
text: null,
|
|
387
|
+
loaded: false,
|
|
388
|
+
loadError: '',
|
|
389
|
+
truncated: false,
|
|
390
|
+
encoding: '',
|
|
391
|
+
}),
|
|
392
|
+
})
|
|
393
|
+
} catch (error) {
|
|
394
|
+
imgError = error
|
|
395
|
+
}
|
|
396
|
+
check('图片预览渲染没抛异常', imgError === null, imgError ? String(imgError) : '')
|
|
397
|
+
check('图片预览用了 <img>', img && img.tags.includes('img'))
|
|
398
|
+
check('图片指向 /api/my-computer/download', img && img.components.length > 0)
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
console.log(' · 跳过渲染检查(没有 react 或没有拿到组件)')
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
section('侧栏图标')
|
|
405
|
+
|
|
406
|
+
if (React && side && typeof side.component === 'function') {
|
|
407
|
+
let icon = null
|
|
408
|
+
let iconError = null
|
|
409
|
+
try {
|
|
410
|
+
icon = side.component({})
|
|
411
|
+
check('图标渲染出 svg', icon && icon.type === 'svg')
|
|
412
|
+
check('图标 viewBox 24×24', icon && icon.props && icon.props.viewBox === '0 0 24 24')
|
|
413
|
+
} catch (error) {
|
|
414
|
+
iconError = error
|
|
415
|
+
}
|
|
416
|
+
check('图标渲染没抛异常', iconError === null, iconError ? String(iconError) : '')
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const passed = checks.filter((c) => c.pass).length
|
|
420
|
+
const total = checks.length
|
|
421
|
+
console.log('\n' + '='.repeat(64))
|
|
422
|
+
console.log(`结果:${passed}/${total} 通过,${total - passed} 失败`)
|
|
423
|
+
console.log('='.repeat(64))
|
|
424
|
+
if (passed !== total) {
|
|
425
|
+
for (const c of checks) if (!c.pass) console.log(' ✗ ' + c.name + (c.detail ? ' ' + c.detail : ''))
|
|
426
|
+
process.exitCode = 1
|
|
427
|
+
} else {
|
|
428
|
+
console.log('全部通过。注意:这不等于 DSH 里已经能看见面板——还要装进 profile 并重启 dsh web。')
|
|
429
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// 检查客户端半区在真实 ctx 形状下会注册哪些东西(不渲染,只用假 ctx 捕获注册)
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
const src = readFileSync(new URL('../lib/client.js', import.meta.url), 'utf8')
|
|
5
|
+
|
|
6
|
+
let registration
|
|
7
|
+
const fakeWindow = {
|
|
8
|
+
__ModuleLoader__: {
|
|
9
|
+
load(reg) {
|
|
10
|
+
registration = reg
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
const fakeDocument = {
|
|
15
|
+
head: { appendChild() {} },
|
|
16
|
+
createElement() {
|
|
17
|
+
return { setAttribute() {}, appendChild() {}, style: {}, textContent: '' }
|
|
18
|
+
},
|
|
19
|
+
querySelector() {
|
|
20
|
+
return null
|
|
21
|
+
},
|
|
22
|
+
getElementById() {
|
|
23
|
+
return null
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
globalThis.window = fakeWindow
|
|
27
|
+
globalThis.document = fakeDocument
|
|
28
|
+
|
|
29
|
+
new Function('window', 'document', src)(fakeWindow, fakeDocument)
|
|
30
|
+
|
|
31
|
+
if (!registration) throw new Error('没有捕获到 __ModuleLoader__.load 注册')
|
|
32
|
+
console.log('module id =', registration.id)
|
|
33
|
+
|
|
34
|
+
// react 桩:apply 阶段只注册组件,不真的渲染
|
|
35
|
+
const fakeReact = {
|
|
36
|
+
createElement: (type, props, ...kids) => ({ type, props, kids }),
|
|
37
|
+
Fragment: 'Fragment',
|
|
38
|
+
useState: (v) => [typeof v === 'function' ? v() : v, () => {}],
|
|
39
|
+
useEffect: () => {},
|
|
40
|
+
useMemo: (f) => f(),
|
|
41
|
+
useCallback: (f) => f,
|
|
42
|
+
useRef: (v) => ({ current: v }),
|
|
43
|
+
}
|
|
44
|
+
const clientPlugin = registration.factory((spec) => {
|
|
45
|
+
if (spec === 'react') return fakeReact
|
|
46
|
+
if (spec === 'react/jsx-runtime') return { jsx: fakeReact.createElement, jsxs: fakeReact.createElement }
|
|
47
|
+
throw new Error('unexpected external: ' + spec)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
console.log('exports:', Object.keys(clientPlugin).join(', '))
|
|
51
|
+
console.log('inject:', JSON.stringify(clientPlugin.inject))
|
|
52
|
+
|
|
53
|
+
const seen = []
|
|
54
|
+
function makeSlots() {
|
|
55
|
+
return {
|
|
56
|
+
inject(name, cb) {
|
|
57
|
+
seen.push('slots.inject:' + name)
|
|
58
|
+
const dispose = cb()
|
|
59
|
+
return typeof dispose === 'function' ? dispose : () => {}
|
|
60
|
+
},
|
|
61
|
+
register(opts, comp) {
|
|
62
|
+
seen.push('slots.register:' + opts.name + (opts.key ? '[key=' + opts.key + ']' : opts.id ? '[id=' + opts.id + ']' : ''))
|
|
63
|
+
if (typeof comp !== 'function' && comp !== null) throw new Error('component 不是函数: ' + opts.name)
|
|
64
|
+
return () => {}
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const disposers = []
|
|
70
|
+
const ctx = {
|
|
71
|
+
slots: makeSlots(),
|
|
72
|
+
effect(fn) {
|
|
73
|
+
const d = fn()
|
|
74
|
+
if (typeof d === 'function') disposers.push(d)
|
|
75
|
+
seen.push('effect')
|
|
76
|
+
},
|
|
77
|
+
inject(deps, cb) {
|
|
78
|
+
seen.push('ctx.inject:' + deps.join('+'))
|
|
79
|
+
const child = {
|
|
80
|
+
slots: ctx.slots,
|
|
81
|
+
sidebarRightTabs: {
|
|
82
|
+
register(def) {
|
|
83
|
+
seen.push(
|
|
84
|
+
'sidebarRightTabs.register(id=' + def.id + ', kind=' + def.kind + ', guide=' + (def.guide ? def.guide.length : 0) + ')',
|
|
85
|
+
)
|
|
86
|
+
if (!def.id || !def.kind) throw new Error('id/kind 缺失')
|
|
87
|
+
if (typeof def.title !== 'function') throw new Error('title 必须是函数')
|
|
88
|
+
return () => {}
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
const d = cb(child)
|
|
93
|
+
if (typeof d === 'function') disposers.push(d)
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
clientPlugin.apply(ctx)
|
|
98
|
+
|
|
99
|
+
console.log('\n--- 注册轨迹 ---')
|
|
100
|
+
for (const s of seen) console.log(' ' + s)
|
|
101
|
+
console.log('\n__MYCOMP_ERR =', globalThis.window.__MYCOMP_ERR || '(无错误)')
|
|
102
|
+
|
|
103
|
+
const must = [
|
|
104
|
+
'sidebarRightTabs.register(id=my-computer',
|
|
105
|
+
'slots.inject:sidebar.right.pane.tab',
|
|
106
|
+
'slots.register:sidebar.right.pane.tab[key=my-computer]',
|
|
107
|
+
'slots.inject:sidebar.right.pane.tab.title',
|
|
108
|
+
'slots.register:sidebar.right.pane.tab.title[key=my-computer]',
|
|
109
|
+
]
|
|
110
|
+
let bad = 0
|
|
111
|
+
console.log('\n--- 断言 ---')
|
|
112
|
+
for (const m of must) {
|
|
113
|
+
const ok = seen.some((s) => s.startsWith(m.replace('[key=my-computer]', '')) || s === m || s.startsWith(m))
|
|
114
|
+
console.log((ok ? ' ✓ ' : ' ✗ ') + m)
|
|
115
|
+
if (!ok) bad++
|
|
116
|
+
}
|
|
117
|
+
console.log(bad === 0 ? '\n全部通过' : '\n失败 ' + bad + ' 项')
|
|
118
|
+
process.exit(bad === 0 ? 0 : 1)
|