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.
@@ -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)