@dimina-kit/devtools 0.3.2-dev.20260522131311 → 0.3.2-dev.20260522142649

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 (54) hide show
  1. package/README.md +89 -155
  2. package/dist/main/api.d.ts +3 -11
  3. package/dist/main/api.js +2 -13
  4. package/dist/main/app/app.d.ts +14 -1
  5. package/dist/main/app/app.js +93 -2
  6. package/dist/main/index.bundle.js +140 -52
  7. package/dist/main/ipc/app.d.ts +1 -1
  8. package/dist/main/ipc/app.js +3 -0
  9. package/dist/main/ipc/simulator.d.ts +1 -1
  10. package/dist/main/ipc/simulator.js +2 -3
  11. package/dist/main/ipc/toolbar.d.ts +1 -1
  12. package/dist/main/ipc/toolbar.js +14 -4
  13. package/dist/main/services/layout/index.d.ts +4 -10
  14. package/dist/main/services/layout/index.js +11 -19
  15. package/dist/main/services/module.d.ts +1 -2
  16. package/dist/main/services/simulator/custom-apis.d.ts +0 -1
  17. package/dist/main/services/simulator/custom-apis.js +0 -1
  18. package/dist/main/services/toolbar/toolbar-store.d.ts +23 -0
  19. package/dist/main/services/toolbar/toolbar-store.js +23 -0
  20. package/dist/main/services/views/view-manager.d.ts +6 -0
  21. package/dist/main/services/views/view-manager.js +6 -3
  22. package/dist/main/services/workbench-context.d.ts +40 -14
  23. package/dist/main/services/workbench-context.js +6 -1
  24. package/dist/main/utils/disposable.d.ts +5 -0
  25. package/dist/main/utils/disposable.js +7 -0
  26. package/dist/main/utils/ipc-registry.js +5 -1
  27. package/dist/main/utils/sender-policy.d.ts +4 -1
  28. package/dist/main/utils/sender-policy.js +6 -0
  29. package/dist/preload/runtime/custom-apis.js +72 -1
  30. package/dist/preload/windows/simulator.js +41 -1
  31. package/dist/renderer/assets/index-4lkyF644.js +46 -0
  32. package/dist/renderer/assets/{input-CoWcfa8u.js → input-D4byFOLS.js} +2 -2
  33. package/dist/renderer/assets/{ipc-transport-Cd2xPrsa.js → ipc-transport-BzPzxPwf.js} +2 -2
  34. package/dist/renderer/assets/{popover-Nq5gT9DO.js → popover-cxKrg_os.js} +2 -2
  35. package/dist/renderer/assets/{select-By5KH-UG.js → select-BgSkkdHW.js} +2 -2
  36. package/dist/renderer/assets/{settings-api-Do0_Smq1.js → settings-api-BXCw9vOZ.js} +2 -2
  37. package/dist/renderer/assets/{settings-Dj_N70qR.js → settings-oD2K1jdT.js} +2 -2
  38. package/dist/renderer/assets/{workbenchSettings-CazSLGYt.js → workbenchSettings-CTNBpTft.js} +2 -2
  39. package/dist/renderer/entries/main/index.html +4 -4
  40. package/dist/renderer/entries/popover/index.html +4 -4
  41. package/dist/renderer/entries/settings/index.html +4 -4
  42. package/dist/renderer/entries/workbench-settings/index.html +3 -3
  43. package/dist/shared/ipc-channels.d.ts +3 -2
  44. package/dist/shared/ipc-channels.js +4 -3
  45. package/dist/shared/ipc-schemas.d.ts +2 -0
  46. package/dist/shared/ipc-schemas.js +2 -0
  47. package/dist/shared/types.d.ts +55 -3
  48. package/dist/simulator/assets/simulator-CILbo0Ji.js +9 -0
  49. package/dist/simulator/simulator.html +1 -1
  50. package/package.json +3 -35
  51. package/dist/main/simulator-apis.d.ts +0 -23
  52. package/dist/main/simulator-apis.js +0 -24
  53. package/dist/renderer/assets/index-D8VFYBh-.js +0 -46
  54. package/dist/simulator/assets/simulator-DyXpdHtT.js +0 -9
package/README.md CHANGED
@@ -1,18 +1,17 @@
1
1
  # Dimina DevTools
2
2
 
3
- 基于 Electron 的小程序模块化开发者工具。提供模拟器、Chrome DevTools 面板、WXML/AppData/Storage 面板、编译配置等功能。
3
+ 基于 Electron 的小程序开发者工具。提供模拟器、Chrome DevTools 面板、WXML/AppData/Storage 面板、编译配置等功能。
4
4
 
5
- 所有功能拆分为独立可导入的模块,第三方可按需组装,最大程度支持自定义。
5
+ 下游 host 通过 `createWorkbenchApp()` 集成并定制 devtools。扩展模型设计见 [`docs/extension-model.md`](docs/extension-model.md)。
6
6
 
7
7
  ---
8
8
 
9
- ## 三种接入方式
9
+ ## 两种接入方式
10
10
 
11
- | 级别 | 入口 | 控制力 | 适合场景 |
12
- | ------------ | ---------------------- | ------------------ | -------------------- |
13
- | **零配置** | `launch()` | 无 | 直接运行 |
14
- | **配置驱动** | `createWorkbenchApp()` | 开关模块、选择面板 | 简单定制 |
15
- | **模块组装** | 单独 import 各模块 | 完全控制 | 深度定制、自定义 IPC |
11
+ | 级别 | 入口 | 控制力 | 适合场景 |
12
+ | ------------ | ---------------------- | --------------------- | -------------- |
13
+ | **零配置** | `launch()` | 无 | 直接运行 |
14
+ | **配置驱动** | `createWorkbenchApp()` | 配置 Provider + 扩展点 | 品牌化、深度定制 |
16
15
 
17
16
  ### 零配置
18
17
 
@@ -23,124 +22,57 @@ launch()
23
22
 
24
23
  ### 配置驱动
25
24
 
26
- ```typescript
27
- import { createWorkbenchApp } from '@dimina-kit/devtools/app'
28
-
29
- createWorkbenchApp({
30
- appName: 'My DevTools',
31
- adapter: myAdapter,
32
- panels: ['console', 'storage'], // 只显示这两个内置面板
33
- modules: { settings: false }, // 禁用设置面板模块
34
- window: { width: 1400, height: 900 },
35
- }).start()
36
- ```
37
-
38
- ### 宿主扩展(推荐用于品牌化 / 自定义功能)
39
-
40
- 基于 `createWorkbenchApp()` 的配置驱动模式,通过 hooks 注入宿主逻辑,无需手动组装模块:
25
+ 扩展点分两类:**配置 Provider**(构造期、一对一,替换某个内置能力)走 `createWorkbenchApp` 配置字段;**Contribution**(`onSetup` 期、一对多,添加多个同类条目)走 `onSetup(instance)` 上的 typed 方法。所有 Contribution 都 per-context,随 context 自动销毁。
41
26
 
42
27
  ```typescript
43
28
  import { suppressEpipe } from '@dimina-kit/devtools/bootstrap'
44
29
  import { createWorkbenchApp } from '@dimina-kit/devtools/app'
45
30
  import { rendererDir } from '@dimina-kit/devtools/paths'
46
- import { ipcMain, Menu } from 'electron'
31
+ import { Menu } from 'electron'
47
32
 
48
33
  suppressEpipe()
49
34
 
50
35
  createWorkbenchApp({
36
+ // ── 配置 Provider(构造期,一对一)──
51
37
  appName: '我的开发工具',
52
38
  adapter: myAdapter,
53
39
  preloadPath: '/path/to/my-preload.js',
54
40
  rendererDir,
55
41
  apiNamespaces: ['my'],
42
+ headerHeight: 72,
56
43
  brandingProvider: () => ({ appName: '我的开发工具' }),
57
- toolbarActions: () => [
58
- { id: 'deploy', label: '发布' },
59
- { id: 'preview', label: '预览' },
60
- ],
61
44
  icon: '/path/to/icon.png',
62
- menuBuilder: (mainWindow, ctx) => {
63
- // 自定义菜单,ctxWorkbenchContext
45
+ menuBuilder: (mainWindow, menuCtx) => {
46
+ // menuCtxMenuContext(只读 menu 相关状态)
64
47
  Menu.setApplicationMenu(/* ... */)
65
48
  },
66
- onSetup: ({ mainWindow, context }) => {
67
- // 注册自定义 IPC handler
68
- ipcMain.handle('toolbar:action:deploy', async () => {
69
- // 通过 service 访问状态:context.workspace.getProjectPath()、context.workspace.hasActiveSession()
70
- })
49
+
50
+ // ── Contribution(onSetup 期,一对多)──
51
+ onSetup: (instance) => {
52
+ // simulator 自定义 API:per-context
53
+ instance.registerSimulatorApi('login', (params) => myLogin(params))
54
+
55
+ // 工具栏:整表替换,随 host 状态(如登录态)重算时再 set 一次
56
+ const refreshToolbar = () => instance.toolbar.set([
57
+ { id: 'deploy', label: '发布', handler: () => deploy() },
58
+ { id: 'preview', label: '预览', handler: () => preview() },
59
+ ])
60
+ refreshToolbar()
61
+
62
+ // 自定义 IPC:经 gated 的 IpcRegistry,不再裸 ipcMain.handle
63
+ instance.ipc.handle('my:action', () => collectStats())
64
+
65
+ // host 自己的弹窗须注册为受信 sender 后才能调 instance.ipc
66
+ // const win = createDialogWindow(/* ... */)
67
+ // instance.registerTrustedWindow(win)
71
68
  },
72
69
  onBeforeClose: ({ context }) => {
73
- // 窗口关闭前的自定义清理逻辑
74
- // session 关闭和 view 销毁由框架自动处理
70
+ // 窗口关闭前的自定义清理;session 关闭和 view 销毁由框架自动处理
75
71
  },
76
72
  }).start()
77
73
  ```
78
74
 
79
- ### 模块组装(仅在需要完全控制时使用)
80
-
81
- ```typescript
82
- import { app, globalShortcut } from 'electron'
83
- import path from 'path'
84
- import { createMainWindow } from '@dimina-kit/devtools/create-window'
85
- import { createWorkbenchContext } from '@dimina-kit/devtools/context'
86
- import { registerAppIpc } from '@dimina-kit/devtools'
87
- import { registerSimulatorIpc } from '@dimina-kit/devtools/ipc-simulator'
88
- import { registerPanelsIpc } from '@dimina-kit/devtools/ipc-panels'
89
- import { registerToolbarIpc } from '@dimina-kit/devtools/ipc-toolbar'
90
- import { registerPopoverIpc } from '@dimina-kit/devtools/ipc-popover'
91
- import { registerProjectsIpc } from '@dimina-kit/devtools/ipc-projects'
92
- import { registerSessionIpc } from '@dimina-kit/devtools/ipc-session'
93
- // registerSettingsIpc 不需要,不导入
94
- import { rendererDir, defaultPreloadPath } from '@dimina-kit/devtools/paths'
95
-
96
- app.whenReady().then(() => {
97
- const mainWindow = createMainWindow({
98
- title: 'My DevTools',
99
- indexHtml: path.join(rendererDir, 'entries/main/index.html'),
100
- })
101
-
102
- const ctx = createWorkbenchContext({
103
- mainWindow,
104
- adapter: myAdapter,
105
- preloadPath: defaultPreloadPath,
106
- rendererDir,
107
- })
108
-
109
- // registerAppIpc 必装:renderer 启动时调用 app:getPreloadPath / app:getBranding,
110
- // 缺它会卡在加载阶段。
111
- registerAppIpc(ctx)
112
- registerProjectsIpc(ctx)
113
- registerSessionIpc(ctx)
114
- registerSimulatorIpc(ctx)
115
- registerPanelsIpc(ctx)
116
- registerToolbarIpc(ctx)
117
- registerPopoverIpc(ctx)
118
-
119
- // Storage / 元素选区面板的后端 handler 由 setupSimulatorStorage 单独装。
120
- // 该函数当前**未列入稳定公共 API**(在 `@dimina-kit/devtools` 之外)。
121
- // 若需要这两个面板,建议改用上方的 `createWorkbenchApp({ onSetup })` 路线,
122
- // 框架会自动注册;模块组装路线下你只能用不依赖它们的精简面板集。
123
-
124
- // 添加自定义 IPC — 通过 ctx.workspace 访问会话状态
125
- ipcMain.handle('my:action', () => {
126
- console.log('current project:', ctx.workspace.getProjectPath())
127
- })
128
-
129
- mainWindow.on('resize', () => ctx.views.repositionAll())
130
- mainWindow.on('close', async (e) => {
131
- if (ctx.workspace.hasActiveSession()) {
132
- e.preventDefault()
133
- await ctx.workspace.closeProject()
134
- ctx.notify.windowNavigateBack()
135
- }
136
- })
137
- })
138
-
139
- app.on('window-all-closed', () => {
140
- globalShortcut.unregisterAll()
141
- app.quit()
142
- })
143
- ```
75
+ > Contribution 注册物全部自动进 `context.registry`,host 无需手写 cleanup。devtools 内部仍用 `WorkbenchModule` 组织内置 IPC 模块,但这是实现细节,不再作为对外接入方式导出。
144
76
 
145
77
  ---
146
78
 
@@ -251,15 +183,15 @@ src/
251
183
 
252
184
  `launch()` 和 `createWorkbenchApp()` 共用:
253
185
 
254
- | 字段 | 类型 | 默认值 | 说明 |
255
- | ------------------ | ----------------------- | ------------------- | ---------------------------------- |
256
- | `appName` | `string` | `'Dimina DevTools'` | 窗口标题 |
257
- | `adapter` | `CompilationAdapter` | 内置 | 项目编译适配器 |
258
- | `panels` | `BuiltinPanelId[]` | 全部四个 | 显示哪些内置面板 |
259
- | `preloadPath` | `string` | 内置 | 自定义 preload 脚本路径 |
260
- | `apiNamespaces` | `string[]` | `[]` | 自定义 API 命名空间(如 `['qd']`) |
261
- | `brandingProvider` | `() => { appName }` | — | 品牌信息 provider |
262
- | `toolbarActions` | `() => ToolbarAction[]` | | 工具栏动作按钮 provider |
186
+ | 字段 | 类型 | 默认值 | 说明 |
187
+ | ------------------ | -------------------- | ------------------- | ---------------------------------- |
188
+ | `appName` | `string` | `'Dimina DevTools'` | 窗口标题 |
189
+ | `adapter` | `CompilationAdapter` | 内置 | 项目编译适配器 |
190
+ | `panels` | `BuiltinPanelId[]` | 全部四个 | 显示哪些内置面板 |
191
+ | `preloadPath` | `string` | 内置 | 自定义 preload 脚本路径 |
192
+ | `apiNamespaces` | `string[]` | `[]` | 自定义 API 命名空间(如 `['qd']`) |
193
+ | `brandingProvider` | `() => { appName }` | — | 品牌信息 provider |
194
+ | `headerHeight` | `number` | `40` | 头部栏高度(px),同步下发 main + renderer |
263
195
 
264
196
  ### WorkbenchAppConfig(扩展 WorkbenchConfig)
265
197
 
@@ -267,16 +199,27 @@ src/
267
199
 
268
200
  | 字段 | 类型 | 默认值 | 说明 |
269
201
  | --------------- | ------------------------------------------- | ----------- | ------------------------------------------------- |
270
- | `modules` | `Partial<Record<BuiltinModuleId, boolean>>` | 全部 `true` | 开关 IPC 模块组 |
202
+ | `modules` | `Partial<Record<BuiltinModuleId, boolean>>` | 全部 `true` | 开关内置 IPC 模块组 |
271
203
  | `rendererDir` | `string` | 内置 | 自定义 renderer HTML 目录 |
272
204
  | `icon` | `string` | — | 窗口/任务栏图标路径(macOS 使用 app bundle 图标) |
273
- | `menuBuilder` | `(mainWindow, context) => void` | 内置菜单 | 自定义菜单构建器 |
274
- | `onSetup` | `(instance) => void` | — | 窗口和 context 创建后的回调,用于注册自定义 IPC |
205
+ | `menuBuilder` | `(mainWindow, menuContext: MenuContext) => void` | 内置菜单 | 自定义菜单构建器;`menuContext` 为收窄后的 `MenuContext`,不含 `registry`/`senderPolicy` 等内部管线 |
206
+ | `onSetup` | `(instance) => void` | — | 窗口和 context 创建后的回调,用于注册 Contribution(见下文)|
275
207
  | `onBeforeClose` | `(instance) => void` | — | 窗口关闭前的回调,session 关闭由框架自动处理 |
276
208
  | `window` | `WorkbenchWindowConfig` | — | 窗口尺寸覆盖 |
277
209
  | `updateChecker` | `UpdateChecker` | — | 自定义更新检查器;提供后启用"检查更新"功能 |
278
210
  | `updateOptions` | `{ checkInterval?, initialDelay?, getCurrentVersion? }` | — | 仅当 `updateChecker` 提供时生效,默认 1h / 5s |
279
211
 
212
+ ### onSetup Contribution
213
+
214
+ `onSetup(instance)` 收到的 `instance` 上有一组 typed 注册方法,全部 per-context、自动进 `context.registry`:
215
+
216
+ | 方法 | 说明 |
217
+ | --- | --- |
218
+ | `instance.registerSimulatorApi(name, handler)` | 注册 simulator 自定义 API,小程序里 `wx.<name>()` 调用(详见下方"Simulator 自定义 API")。返回 `Disposable` |
219
+ | `instance.toolbar.set(actions)` | 原子整表替换工具栏;`actions` 为 `ToolbarActionInput[]`(`{ id, label, handler }`),`id` 重复抛错。host 状态变化(如登录态)时构造新表再 `set` 一次 |
220
+ | `instance.ipc` | `IpcRegistry` 实例,`instance.ipc.handle(channel, fn)` 注册自定义 IPC;已绑定 `senderPolicy` 网关 |
221
+ | `instance.registerTrustedWindow(win)` | 把 host 自有弹窗 `BrowserWindow` 加入受信 sender 集,否则其发起的 `instance.ipc` 调用会被网关拒绝。窗口关闭即移除 |
222
+
280
223
  ### 内置面板 ID
281
224
 
282
225
  `'wxml'` \| `'console'` \| `'appdata'` \| `'storage'`
@@ -505,13 +448,13 @@ launch({ preloadPath: '/absolute/path/to/my-preload.js' })
505
448
 
506
449
  ## Simulator 自定义 API
507
450
 
508
- 下游可以通过 `registerSimulatorApi` 把业务 API 注入到模拟器内运行的小程序。Handler 在 Electron **主进程**执行,能用任何 Node 能力(fs、net、原生模块、持久状态);模拟器侧拿到 `wx.<name>(params)` 调用后通过 IPC 转发到主进程,await 返回结果,handler 抛错则原型回传给小程序。
451
+ 下游通过 `onSetup` 里的 `instance.registerSimulatorApi(name, handler)` 把业务 API 注入到模拟器内运行的小程序。注册 per-context,随 context 销毁。Handler 在 Electron **主进程**执行,能用任何 Node 能力(fs、net、原生模块、持久状态);模拟器侧拿到 `wx.<name>(params)` 调用后通过 IPC 转发到主进程,await 返回结果,handler 抛错则原型回传给小程序。
509
452
 
510
- > ⚠️ 使用本功能要求模拟器 preload 调用 `installCustomApisBridge()`(详见上方 "Preload Instrumentation")。内置 preload 已包含此调用;若你用了自定义 preload 又忘记调用,注册的 API 在小程序运行时**没有任何反应**,且不会报错。
453
+ > ⚠️ 使用本功能要求模拟器 preload 调用 `installCustomApisBridge()`(详见上方 "Preload Instrumentation")。内置 preload 已包含此调用;若用了自定义 preload 又漏装,注册的 API 在小程序运行时**没有任何反应**,simulator 端会有 `console.warn`。
511
454
 
512
455
  ### 命名约定
513
456
 
514
- `name` 是 dimina service 层 `invokeAPI(name, opts)` 用的**裸名**,不带 `wx.` 前缀。注册之后小程序里通过 `wx.<name>(...)`(以及你在 `apiNamespaces` 配的其它命名空间)调用都会命中:
457
+ `name` 是 dimina service 层 `invokeAPI(name, opts)` 用的**裸名**,不带 `wx.` 前缀。注册之后小程序里通过 `wx.<name>(...)`(以及在 `apiNamespaces` 配的其它命名空间)调用都会命中:
515
458
 
516
459
  | 小程序里写 | `registerSimulatorApi` 的 name |
517
460
  | --- | --- |
@@ -520,17 +463,19 @@ launch({ preloadPath: '/absolute/path/to/my-preload.js' })
520
463
  | `wx.getStorage(...)` 覆盖内置 | `'getStorage'`(小心:这会**屏蔽** dimina 容器的实现) |
521
464
 
522
465
  ```typescript
523
- import { registerSimulatorApi } from '@dimina-kit/devtools/simulator-apis'
466
+ createWorkbenchApp({
467
+ onSetup: (instance) => {
468
+ // 提供 wx.login 的实现(dimina 容器默认未实现,注册后才能用)
469
+ const dispose = instance.registerSimulatorApi('login', async ({ success }) => {
470
+ // 主进程上下文:可调用内部 HTTP、读取凭证文件、访问原生模块
471
+ const code = await callInternalAuth()
472
+ return { code }
473
+ })
524
474
 
525
- // 提供 wx.login 的实现(dimina 容器默认未实现,注册后才能用)
526
- const dispose = registerSimulatorApi('login', async () => {
527
- // 主进程上下文:可调用内部 HTTP、读取凭证文件、访问原生模块
528
- const code = await callInternalAuth()
529
- return { code }
475
+ // 注册物已进 context.registry,随 context 销毁;
476
+ // 提前取消用返回的 dispose()
477
+ },
530
478
  })
531
-
532
- // 不再需要时取消注册(不传也行,进程退出会一起清理)
533
- dispose()
534
479
  ```
535
480
 
536
481
  ### 行为约定
@@ -547,27 +492,25 @@ dispose()
547
492
 
548
493
  ## 模块导出一览
549
494
 
550
- ### Stable public exports
551
-
552
- `@dimina-kit/devtools` 的稳定入口,签名遵循 semver;新版本不会做破坏性改动(除非 major bump)。
495
+ `@dimina-kit/devtools` 的公共入口。`api.ts`(根入口)聚合了所有公共 API,优先从根入口导入。
553
496
 
554
497
  ```
555
498
  @dimina-kit/devtools launch, createWorkbenchApp, buildDefaultMenu,
556
499
  openSettingsWindow, suppressEpipe, setupCdpPort,
557
500
  createWorkbenchContext, createMainWindow,
558
- createViewManager, register*Ipc,
559
- registerSimulatorApi,
560
- setHeaderHeight, UpdateManager,
561
- createGitHubReleaseChecker, ...
562
- (api.ts 聚合了所有公共 API;优先从根入口导入)
501
+ createViewManager, IpcRegistry,
502
+ UpdateManager, createGitHubReleaseChecker, ...
563
503
  @dimina-kit/devtools/launch launch(config?), buildDefaultMenu, openSettingsWindow
564
504
  @dimina-kit/devtools/app createWorkbenchApp(config?)
565
505
  @dimina-kit/devtools/types TypeScript 类型定义
566
506
  @dimina-kit/devtools/bootstrap suppressEpipe(), setupCdpPort()
507
+ @dimina-kit/devtools/context createWorkbenchContext(opts),
508
+ hasBuiltinPanel(ctx, panelId), getDefaultTab(ctx)
509
+ @dimina-kit/devtools/create-window createMainWindow(opts)
567
510
  @dimina-kit/devtools/paths rendererDir, defaultPreloadPath, simulatorDir,
568
511
  getRendererDir, getPreloadDir, getRendererHtml
569
- @dimina-kit/devtools/simulator-apis registerSimulatorApi(name, handler)
570
- 注入由主进程托管的小程序 API(详见上方"Simulator 自定义 API")
512
+ @dimina-kit/devtools/projects-provider ProjectsProvider / ProjectTemplate 等类型
513
+ @dimina-kit/devtools/workbench-settings loadWorkbenchSettings(), saveWorkbenchSettings(), applyTheme()
571
514
  @dimina-kit/devtools/preload installSimulatorBridge,
572
515
  installCustomApisBridge,
573
516
  installConsoleInstrumentation,
@@ -577,29 +520,15 @@ dispose()
577
520
  (storage 面板数据由主进程 CDP 抓取,无 preload 侧 hook)
578
521
  ```
579
522
 
580
- ### Experimental exports (v0.x signatures may change in minor versions)
581
-
582
- 下列子路径主要用于"模块组装"等深度定制场景。在 0.x 阶段,函数签名和模块边界可能在 minor 版本之间调整;如果你不需要逐模块组装,请优先使用上方的 stable 入口或根 barrel。
583
-
584
- ```
585
- @dimina-kit/devtools/context createWorkbenchContext(opts),
586
- hasBuiltinPanel(ctx, panelId), getDefaultTab(ctx)
587
- @dimina-kit/devtools/create-window createMainWindow(opts)
588
- @dimina-kit/devtools/ipc-simulator registerSimulatorIpc(ctx)
589
- @dimina-kit/devtools/ipc-panels registerPanelsIpc(ctx)
590
- @dimina-kit/devtools/ipc-toolbar registerToolbarIpc(ctx)
591
- @dimina-kit/devtools/ipc-popover registerPopoverIpc(ctx)
592
- @dimina-kit/devtools/ipc-settings registerSettingsIpc(ctx)
593
- @dimina-kit/devtools/ipc-projects registerProjectsIpc(ctx)
594
- @dimina-kit/devtools/ipc-session registerSessionIpc(ctx), sessionModule
595
- @dimina-kit/devtools/workbench-settings loadWorkbenchSettings(), saveWorkbenchSettings(), applyTheme()
596
- ```
523
+ > `IpcRegistry` host 写自定义 IPC 的网关基类——`onSetup` 里通过 `instance.ipc` 拿到一个已绑定 `senderPolicy` 的实例,无需自行构造。
524
+ > 自定义 simulator API 通过 `instance.registerSimulatorApi` 注册,没有独立的导出入口。
525
+ > 内置 IPC 模块(`register*Ipc` / `WorkbenchModule`)是 devtools 内部实现,不再对外导出。
597
526
 
598
527
  ---
599
528
 
600
529
  ## WorkbenchContext
601
530
 
602
- `WorkbenchContext` 是所有 IPC 模块共享的状态入口。配置字段直接暴露,运行时状态通过三个 service 访问:
531
+ `WorkbenchContext` 是所有扩展点的唯一容器。配置字段直接暴露,运行时状态通过 service 访问。host hook 拿到的 `instance.context` 即此类型;`menuBuilder` 拿到的是收窄后的 `MenuContext`(剔除 `registry` / `senderPolicy` / `trustedWindowSenderIds` / `simulatorApis` / `toolbar` 等内部管线)。
603
532
 
604
533
  ```typescript
605
534
  interface WorkbenchContext {
@@ -609,7 +538,7 @@ interface WorkbenchContext {
609
538
  panels: string[]
610
539
  apiNamespaces: string[]
611
540
  appName: string
612
- toolbarActions?: () => Promise<ToolbarAction[]> | ToolbarAction[]
541
+ headerHeight: number
613
542
  brandingProvider?: () => Promise<{ appName: string }> | { appName: string }
614
543
 
615
544
  // ── Services(运行时状态封装在内部,通过方法访问)──
@@ -618,6 +547,11 @@ interface WorkbenchContext {
618
547
  notify: RendererNotifier // ctx.notify.projectStatus(), ctx.notify.windowNavigateBack(), ...
619
548
  workspace: WorkspaceService // ctx.workspace.getProjectPath(), ctx.workspace.hasActiveSession(), ...
620
549
 
550
+ // ── 扩展容器(host 通过 instance 上的方法写入,不直接操作)──
551
+ simulatorApis: SimulatorApiRegistry // instance.registerSimulatorApi 写入
552
+ toolbar: ToolbarStore // instance.toolbar.set 写入
553
+ trustedWindowSenderIds: Map<number, number> // instance.registerTrustedWindow 写入(webContents.id → 引用计数)
554
+
621
555
  // ── Internal lifecycle / security (host 不直接消费) ──
622
556
  registry: DisposableRegistry // 框架在创建时统一聚合 dispose
623
557
  senderPolicy: SenderPolicy // IpcRegistry 自动校验 sender 白名单
@@ -9,18 +9,10 @@ export { createViewManager } from './services/views/view-manager.js';
9
9
  export type { ViewManager } from './services/views/view-manager.js';
10
10
  export type { WorkspaceService } from './services/workspace/workspace-service.js';
11
11
  export type { Project, ProjectPages, ProjectSettings } from './services/projects/project-repository.js';
12
- export { registerSimulatorApi } from './simulator-apis.js';
13
- export type { SimulatorApiHandler } from './simulator-apis.js';
14
- export { registerAppIpc } from './ipc/app.js';
15
- export { registerSimulatorIpc } from './ipc/simulator.js';
16
- export { registerPanelsIpc } from './ipc/panels.js';
17
- export { registerPopoverIpc } from './ipc/popover.js';
18
- export { registerSettingsIpc } from './ipc/settings.js';
19
- export { registerProjectsIpc } from './ipc/projects.js';
20
- export { registerSessionIpc } from './ipc/session.js';
21
- export { registerToolbarIpc } from './ipc/toolbar.js';
12
+ export type { SimulatorApiHandler } from './services/simulator/custom-apis.js';
22
13
  export { rendererDir, defaultPreloadPath, simulatorDir, getRendererDir, getPreloadDir, getRendererHtml, } from './utils/paths.js';
23
- export { setHeaderHeight } from './services/layout/index.js';
14
+ export { IpcRegistry } from './utils/ipc-registry.js';
15
+ export type { SenderPolicy } from './utils/ipc-registry.js';
24
16
  export { UpdateManager, createGitHubReleaseChecker } from './services/update/index.js';
25
17
  export type { UpdateManagerOptions, GitHubReleaseCheckerOptions, PickAssetContext, VersionScheme, } from './services/update/index.js';
26
18
  export type { WorkbenchConfig, WorkbenchAppConfig, CompilationAdapter, ProjectSession, ToolbarAction, BuiltinPanelId, BuiltinModuleId, UpdateChecker, UpdateInfo, } from '../shared/types.js';
package/dist/main/api.js CHANGED
@@ -7,21 +7,10 @@ export { suppressEpipe, setupCdpPort } from './app/bootstrap.js';
7
7
  export { createWorkbenchContext, hasBuiltinPanel, getDefaultTab } from './services/workbench-context.js';
8
8
  export { createMainWindow } from './windows/main-window/index.js';
9
9
  export { createViewManager } from './services/views/view-manager.js';
10
- // ── Simulator extension surface ──────────────────────────────────────────
11
- export { registerSimulatorApi } from './simulator-apis.js';
12
- // ── IPC module registration ──────────────────────────────────────────────
13
- export { registerAppIpc } from './ipc/app.js';
14
- export { registerSimulatorIpc } from './ipc/simulator.js';
15
- export { registerPanelsIpc } from './ipc/panels.js';
16
- export { registerPopoverIpc } from './ipc/popover.js';
17
- export { registerSettingsIpc } from './ipc/settings.js';
18
- export { registerProjectsIpc } from './ipc/projects.js';
19
- export { registerSessionIpc } from './ipc/session.js';
20
- export { registerToolbarIpc } from './ipc/toolbar.js';
21
10
  // ── Paths ────────────────────────────────────────────────────────────────
22
11
  export { rendererDir, defaultPreloadPath, simulatorDir, getRendererDir, getPreloadDir, getRendererHtml, } from './utils/paths.js';
23
- // ── Layout ───────────────────────────────────────────────────────────────
24
- export { setHeaderHeight } from './services/layout/index.js';
12
+ // ── IPC gateway (for host-registered custom IPC) ─────────────────────────
13
+ export { IpcRegistry } from './utils/ipc-registry.js';
25
14
  // ── Update checker ───────────────────────────────────────────────────────
26
15
  export { UpdateManager, createGitHubReleaseChecker } from './services/update/index.js';
27
16
  //# sourceMappingURL=api.js.map
@@ -1,11 +1,24 @@
1
1
  import { BrowserWindow } from 'electron';
2
- import type { WorkbenchAppConfig } from '../../shared/types.js';
2
+ import type { ToolbarActionInput, WorkbenchAppConfig } from '../../shared/types.js';
3
+ import type { SimulatorApiHandler } from '../services/simulator/custom-apis.js';
3
4
  import { type WorkbenchContext } from '../services/workbench-context.js';
4
5
  import { type AutomationServer } from '../services/automation/index.js';
5
6
  import { UpdateManager } from '../services/update/index.js';
7
+ import { type Disposable } from '../utils/disposable.js';
8
+ import { IpcRegistry } from '../utils/ipc-registry.js';
6
9
  export interface WorkbenchAppInstance {
7
10
  mainWindow: BrowserWindow;
8
11
  context: WorkbenchContext;
12
+ /** Gated custom-IPC registration surface bound to `context.senderPolicy`. */
13
+ readonly ipc: IpcRegistry;
14
+ /** Adds a host-owned BrowserWindow to the trusted-sender set. */
15
+ registerTrustedWindow: (win: BrowserWindow) => Disposable;
16
+ /** Registers a simulator custom API into this context's registry. */
17
+ registerSimulatorApi: (name: string, handler: SimulatorApiHandler) => Disposable;
18
+ /** Per-context toolbar surface — `set()` atomically replaces the table. */
19
+ readonly toolbar: {
20
+ set(actions: ToolbarActionInput[]): void;
21
+ };
9
22
  automationServer?: AutomationServer;
10
23
  updateManager?: UpdateManager;
11
24
  dispose: () => Promise<void>;
@@ -15,6 +15,7 @@ import { startMcpServer } from '../services/mcp/index.js';
15
15
  import { setupSimulatorStorage } from '../services/simulator-storage/index.js';
16
16
  import { UpdateManager } from '../services/update/index.js';
17
17
  import { toDisposable } from '../utils/disposable.js';
18
+ import { IpcRegistry } from '../utils/ipc-registry.js';
18
19
  const DEFAULT_MODULES = {
19
20
  projects: true,
20
21
  session: true,
@@ -29,6 +30,57 @@ const BUILTIN_MODULES = {
29
30
  popover: popoverModule,
30
31
  settings: settingsModule,
31
32
  };
33
+ /**
34
+ * Adds `win.webContents` to the context's trusted-sender set and returns a
35
+ * Disposable that removes it again.
36
+ *
37
+ * Trust is reference-counted: registering the SAME window N times keeps it
38
+ * trusted until every one of the N returned Disposables has been disposed.
39
+ * `trustedWindowSenderIds` is a `Map<webContents.id, refCount>`; each register
40
+ * bumps the count, each dispose decrements it, and the window is un-trusted
41
+ * only when the count reaches zero.
42
+ *
43
+ * The window's `closed` event short-circuits the ref-count: it deletes the
44
+ * map entry outright (the window is dead, so it must be un-trusted
45
+ * immediately regardless of how many Disposables are still outstanding).
46
+ * After `closed`, disposing any leftover Disposable is a safe no-op — the
47
+ * map entry is already gone, so the decrement finds `undefined` and bails
48
+ * without driving the count negative or resurrecting trust.
49
+ *
50
+ * Each returned Disposable is itself idempotent (`removed` flag), and its
51
+ * `closed` listener removes itself once fired so a long-lived context
52
+ * doesn't accumulate dead listeners on closed windows.
53
+ */
54
+ function registerTrustedWindow(context, win) {
55
+ const senderId = win.webContents.id;
56
+ const counts = context.trustedWindowSenderIds;
57
+ counts.set(senderId, (counts.get(senderId) ?? 0) + 1);
58
+ let removed = false;
59
+ const onClosed = () => {
60
+ // The window is gone: zero the ref-count for this sender id outright,
61
+ // regardless of any other outstanding registrations for the same window.
62
+ counts.delete(senderId);
63
+ win.removeListener('closed', onClosed);
64
+ };
65
+ function remove() {
66
+ if (removed)
67
+ return;
68
+ removed = true;
69
+ win.removeListener('closed', onClosed);
70
+ const count = counts.get(senderId);
71
+ // `undefined` → the entry was already cleared (e.g. by `closed` or by a
72
+ // prior sibling's decrement that hit zero). Nothing to do — never go
73
+ // negative, never resurrect trust.
74
+ if (count === undefined)
75
+ return;
76
+ if (count <= 1)
77
+ counts.delete(senderId);
78
+ else
79
+ counts.set(senderId, count - 1);
80
+ }
81
+ win.once('closed', onClosed);
82
+ return toDisposable(remove);
83
+ }
32
84
  /** Parse --auto, --auto-port, --project from process.argv. */
33
85
  function parseAutoArgs() {
34
86
  const argv = process.argv;
@@ -93,8 +145,8 @@ function createContext(config, mainWindow, rendererDir) {
93
145
  panels: config.panels,
94
146
  appName: config.appName,
95
147
  apiNamespaces: config.apiNamespaces,
148
+ headerHeight: config.headerHeight,
96
149
  brandingProvider: config.brandingProvider,
97
- toolbarActions: config.toolbarActions,
98
150
  // The host-supplied ProjectsProvider / template types in `shared/types`
99
151
  // are structurally compatible with the main-process equivalents —
100
152
  // these casts are safe; we re-narrow at the workspace-service /
@@ -112,10 +164,27 @@ function registerBuiltinModules(config, context) {
112
164
  context.registry.add(BUILTIN_MODULES[moduleId].setup(context));
113
165
  });
114
166
  }
167
+ /**
168
+ * Strip the internal-plumbing fields a host menu builder must not reach
169
+ * (registry / senderPolicy / trustedWindowSenderIds / simulatorApis / toolbar)
170
+ * so `menuBuilder` receives the narrowed `MenuContext` its contract promises —
171
+ * at runtime, not just at the type level.
172
+ */
173
+ function toMenuContext(context) {
174
+ // Shallow-copy, then drop the internal-plumbing fields. A rest-destructure
175
+ // would be terser but trips no-unused-vars on the dropped siblings.
176
+ const menuContext = { ...context };
177
+ delete menuContext.registry;
178
+ delete menuContext.senderPolicy;
179
+ delete menuContext.trustedWindowSenderIds;
180
+ delete menuContext.simulatorApis;
181
+ delete menuContext.toolbar;
182
+ return menuContext;
183
+ }
115
184
  function installMenu(config, mainWindow, context) {
116
185
  // Menu: use host-provided builder or fall back to default
117
186
  if (config.menuBuilder) {
118
- config.menuBuilder(mainWindow, context);
187
+ config.menuBuilder(mainWindow, toMenuContext(context));
119
188
  }
120
189
  else {
121
190
  installAppMenu(context);
@@ -205,9 +274,30 @@ export function createWorkbenchApp(config = {}) {
205
274
  context.registry.add(installThemeBackgroundSync());
206
275
  registerBuiltinModules(config, context);
207
276
  installMenu(config, mainWindow, context);
277
+ // Gated custom-IPC surface for the host. Bound to ctx.senderPolicy so
278
+ // host channels go through the same gateway as built-in IPC, and
279
+ // registered into ctx.registry so its handlers are torn down with the
280
+ // context.
281
+ const hostIpc = new IpcRegistry(context.senderPolicy);
282
+ context.registry.add(hostIpc);
208
283
  const instance = {
209
284
  mainWindow,
210
285
  context,
286
+ ipc: hostIpc,
287
+ // Return the registry wrapper, not the raw disposable: disposing the
288
+ // wrapper splices the registry entry out AND drives the underlying
289
+ // teardown, so a single dispose leaves no dead entry behind.
290
+ registerTrustedWindow: (win) => context.registry.add(registerTrustedWindow(context, win)),
291
+ registerSimulatorApi: (name, handler) => context.registry.add(toDisposable(context.simulatorApis.register(name, handler))),
292
+ toolbar: {
293
+ set: (actions) => {
294
+ // `context.toolbar.set` validates id-uniqueness and throws on a
295
+ // duplicate BEFORE mutating, so a rejected batch never reaches
296
+ // the notify below — no phantom ActionsChanged.
297
+ context.toolbar.set(actions);
298
+ context.notify.toolbarActionsChanged();
299
+ },
300
+ },
211
301
  dispose: () => disposeContext(context),
212
302
  };
213
303
  if (config.onSetup) {
@@ -217,6 +307,7 @@ export function createWorkbenchApp(config = {}) {
217
307
  instance.updateManager = new UpdateManager({
218
308
  checker: config.updateChecker,
219
309
  mainWindow,
310
+ senderPolicy: context.senderPolicy,
220
311
  checkInterval: config.updateOptions?.checkInterval,
221
312
  initialDelay: config.updateOptions?.initialDelay,
222
313
  getCurrentVersion: config.updateOptions?.getCurrentVersion,