@dimina-kit/devtools 0.4.0-dev.20260828161241 → 0.4.0-dev.20260907055341

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 (277) hide show
  1. package/README.md +52 -729
  2. package/dist/main/app/app.d.ts +27 -7
  3. package/dist/main/app/app.js +285 -681
  4. package/dist/main/app/editor-view.d.ts +21 -0
  5. package/dist/main/app/editor-view.js +71 -0
  6. package/dist/main/app/global-mirrors.d.ts +23 -0
  7. package/dist/main/app/global-mirrors.js +37 -0
  8. package/dist/main/app/host-sidebar-default.d.ts +19 -0
  9. package/dist/main/app/host-sidebar-default.js +44 -0
  10. package/dist/main/app/menu-setup.d.ts +13 -0
  11. package/dist/main/app/menu-setup.js +49 -0
  12. package/dist/main/app/open-project-ipc.d.ts +13 -0
  13. package/dist/main/app/open-project-ipc.js +21 -0
  14. package/dist/main/app/project-list-surface.d.ts +31 -0
  15. package/dist/main/app/project-list-surface.js +20 -0
  16. package/dist/main/app/project-window.d.ts +36 -0
  17. package/dist/main/app/project-window.js +93 -0
  18. package/dist/main/app/servers.d.ts +17 -0
  19. package/dist/main/app/servers.js +60 -0
  20. package/dist/main/app/ui-extension-targets.d.ts +42 -0
  21. package/dist/main/app/ui-extension-targets.js +101 -0
  22. package/dist/main/app/window-close-reveal.harness.d.ts +34 -0
  23. package/dist/main/app/window-close-reveal.harness.js +65 -0
  24. package/dist/main/app/window-events.d.ts +15 -0
  25. package/dist/main/app/window-events.js +45 -0
  26. package/dist/main/app/window-runtime-services.d.ts +41 -0
  27. package/dist/main/app/window-runtime-services.js +178 -0
  28. package/dist/main/app/workbench-modules.d.ts +24 -0
  29. package/dist/main/app/workbench-modules.js +51 -0
  30. package/dist/main/app/workbench-window.d.ts +85 -0
  31. package/dist/main/app/workbench-window.js +310 -0
  32. package/dist/main/index.bundle.js +10669 -9096
  33. package/dist/main/ipc/app.d.ts +4 -1
  34. package/dist/main/ipc/app.js +4 -3
  35. package/dist/main/ipc/device-picker.d.ts +16 -0
  36. package/dist/main/ipc/device-picker.js +27 -0
  37. package/dist/main/ipc/index.d.ts +1 -0
  38. package/dist/main/ipc/index.js +1 -0
  39. package/dist/main/ipc/internal-devtools.d.ts +2 -1
  40. package/dist/main/ipc/internal-devtools.js +4 -3
  41. package/dist/main/ipc/popover.d.ts +4 -1
  42. package/dist/main/ipc/popover.js +25 -9
  43. package/dist/main/ipc/project-create.d.ts +2 -1
  44. package/dist/main/ipc/project-create.js +6 -5
  45. package/dist/main/ipc/project-fs.d.ts +3 -1
  46. package/dist/main/ipc/project-fs.js +17 -17
  47. package/dist/main/ipc/projects.d.ts +3 -3
  48. package/dist/main/ipc/projects.js +13 -12
  49. package/dist/main/ipc/session.d.ts +4 -1
  50. package/dist/main/ipc/session.js +16 -11
  51. package/dist/main/ipc/settings.d.ts +4 -1
  52. package/dist/main/ipc/settings.js +12 -19
  53. package/dist/main/ipc/simulator-module.d.ts +5 -0
  54. package/dist/main/ipc/simulator-module.js +9 -5
  55. package/dist/main/ipc/simulator.d.ts +4 -1
  56. package/dist/main/ipc/simulator.js +12 -18
  57. package/dist/main/ipc/tooltip.d.ts +2 -1
  58. package/dist/main/ipc/tooltip.js +8 -7
  59. package/dist/main/ipc/views.d.ts +4 -1
  60. package/dist/main/ipc/views.js +20 -9
  61. package/dist/main/runtime/devtools-backend-before-quit.harness.d.ts +2 -20
  62. package/dist/main/runtime/devtools-backend-before-quit.harness.js +14 -283
  63. package/dist/main/runtime/devtools-backend.js +14 -3
  64. package/dist/main/runtime/devtools-runtime-mock.harness.d.ts +21 -0
  65. package/dist/main/runtime/devtools-runtime-mock.harness.js +313 -0
  66. package/dist/main/services/app-services.d.ts +62 -0
  67. package/dist/main/services/app-services.js +64 -0
  68. package/dist/main/services/automation/connection-target.d.ts +36 -0
  69. package/dist/main/services/automation/connection-target.js +29 -0
  70. package/dist/main/services/automation/console-bridge.d.ts +28 -0
  71. package/dist/main/services/automation/console-bridge.js +91 -0
  72. package/dist/main/services/automation/handlers/app.js +4 -4
  73. package/dist/main/services/automation/handlers/element.js +55 -21
  74. package/dist/main/services/automation/index.d.ts +12 -1
  75. package/dist/main/services/automation/index.js +54 -133
  76. package/dist/main/services/default-adapter.js +1 -1
  77. package/dist/main/services/fs-bridge-protocol.d.ts +61 -0
  78. package/dist/main/services/fs-bridge-protocol.js +34 -0
  79. package/dist/main/services/fs-watch-sse.d.ts +8 -1
  80. package/dist/main/services/fs-watch-sse.js +19 -5
  81. package/dist/main/services/mcp/connection-owner.d.ts +38 -0
  82. package/dist/main/services/mcp/connection-owner.js +79 -0
  83. package/dist/main/services/mcp/index.d.ts +3 -1
  84. package/dist/main/services/mcp/index.js +4 -3
  85. package/dist/main/services/mcp/opened-project.d.ts +88 -0
  86. package/dist/main/services/mcp/opened-project.js +60 -0
  87. package/dist/main/services/mcp/target-manager.d.ts +71 -23
  88. package/dist/main/services/mcp/target-manager.js +269 -142
  89. package/dist/main/services/mcp/target-selection.d.ts +47 -0
  90. package/dist/main/services/mcp/target-selection.js +71 -0
  91. package/dist/main/services/mcp/tools/context-tools.js +20 -5
  92. package/dist/main/services/mcp/tools/project-tools.d.ts +29 -6
  93. package/dist/main/services/mcp/tools/project-tools.js +37 -25
  94. package/dist/main/services/module.d.ts +13 -5
  95. package/dist/main/services/notifications/renderer-notifier.d.ts +28 -4
  96. package/dist/main/services/notifications/renderer-notifier.js +15 -3
  97. package/dist/main/services/projects/local-provider.js +2 -0
  98. package/dist/main/services/projects/project-repository.d.ts +31 -1
  99. package/dist/main/services/projects/project-repository.js +144 -20
  100. package/dist/main/services/projects/types.d.ts +31 -8
  101. package/dist/main/services/safe-area/index.d.ts +19 -5
  102. package/dist/main/services/safe-area/index.js +38 -13
  103. package/dist/main/services/simulator/ui-extensions.d.ts +8 -0
  104. package/dist/main/services/simulator/ui-extensions.js +16 -6
  105. package/dist/main/services/simulator-storage/index.d.ts +12 -1
  106. package/dist/main/services/simulator-storage/index.js +22 -7
  107. package/dist/main/services/simulator-storage/window-scope.d.ts +20 -0
  108. package/dist/main/services/simulator-storage/window-scope.js +20 -0
  109. package/dist/main/services/views/host-slot-port-channel.d.ts +20 -3
  110. package/dist/main/services/views/host-slot-port-channel.js +33 -5
  111. package/dist/main/services/views/managed-web-contents-view.js +21 -1
  112. package/dist/main/services/views/native-simulator-view.js +13 -16
  113. package/dist/main/services/views/overlay-panels-view.d.ts +13 -0
  114. package/dist/main/services/views/overlay-panels-view.js +31 -0
  115. package/dist/main/services/views/view-manager.d.ts +1 -1
  116. package/dist/main/services/views/view-manager.js +3 -0
  117. package/dist/main/services/window-contexts/context-router.d.ts +36 -0
  118. package/dist/main/services/window-contexts/context-router.js +55 -0
  119. package/dist/main/services/workbench-coi-host.d.ts +52 -0
  120. package/dist/main/services/workbench-coi-host.js +227 -0
  121. package/dist/main/services/workbench-coi-server.js +75 -133
  122. package/dist/main/services/workbench-coi-shutdown.d.ts +40 -0
  123. package/dist/main/services/workbench-coi-shutdown.js +69 -0
  124. package/dist/main/services/workbench-coi-static.d.ts +23 -0
  125. package/dist/main/services/workbench-coi-static.js +87 -0
  126. package/dist/main/services/workbench-context.d.ts +27 -3
  127. package/dist/main/services/workbench-context.js +10 -5
  128. package/dist/main/services/workspace/compile-mode-adapter.d.ts +18 -0
  129. package/dist/main/services/workspace/compile-mode-adapter.js +57 -0
  130. package/dist/main/services/workspace/compile-mode-store.d.ts +25 -0
  131. package/dist/main/services/workspace/compile-mode-store.js +71 -0
  132. package/dist/main/services/workspace/workspace-compile-modes.d.ts +59 -0
  133. package/dist/main/services/workspace/workspace-compile-modes.js +93 -0
  134. package/dist/main/services/workspace/workspace-referer.d.ts +17 -0
  135. package/dist/main/services/workspace/workspace-referer.js +26 -0
  136. package/dist/main/services/workspace/workspace-service.d.ts +4 -0
  137. package/dist/main/services/workspace/workspace-service.js +51 -84
  138. package/dist/main/services/workspace/workspace-thumbnails.d.ts +22 -0
  139. package/dist/main/services/workspace/workspace-thumbnails.js +67 -0
  140. package/dist/main/utils/ipc-context-source.d.ts +40 -0
  141. package/dist/main/utils/ipc-context-source.js +23 -0
  142. package/dist/main/utils/ipc-registry.d.ts +42 -36
  143. package/dist/main/utils/ipc-registry.js +117 -50
  144. package/dist/main/utils/sender-policy.d.ts +9 -0
  145. package/dist/main/utils/sender-policy.js +26 -3
  146. package/dist/main/utils/theme.d.ts +3 -2
  147. package/dist/main/utils/theme.js +3 -2
  148. package/dist/main/windows/main-window/create.d.ts +16 -0
  149. package/dist/main/windows/main-window/create.js +19 -11
  150. package/dist/main/windows/main-window/events.d.ts +1 -1
  151. package/dist/main/windows/main-window/events.js +26 -13
  152. package/dist/main/windows/main-window/index.d.ts +1 -1
  153. package/dist/main/windows/main-window/index.js +1 -1
  154. package/dist/native-host/common/common.js +94 -73
  155. package/dist/native-host/render/render.js +4079 -3171
  156. package/dist/native-host/service/service.js +2 -2
  157. package/dist/preload/runtime/native-host.d.ts +4 -0
  158. package/dist/preload/runtime/temp-files.js +9 -2
  159. package/dist/preload/runtime/wait-for-slot-root.d.ts +7 -4
  160. package/dist/preload/runtime/wait-for-slot-root.js +36 -8
  161. package/dist/preload/windows/host-dialog-runtime.cjs +10 -3
  162. package/dist/preload/windows/host-dialog-runtime.cjs.map +2 -2
  163. package/dist/preload/windows/host-sidebar-runtime.cjs +10 -3
  164. package/dist/preload/windows/host-sidebar-runtime.cjs.map +2 -2
  165. package/dist/preload/windows/host-toolbar-runtime.cjs +10 -3
  166. package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
  167. package/dist/preload/windows/main.cjs +181 -4
  168. package/dist/preload/windows/main.cjs.map +4 -4
  169. package/dist/preload/windows/simulator.cjs +10 -0
  170. package/dist/preload/windows/simulator.cjs.map +2 -2
  171. package/dist/preload/windows/simulator.js +10 -0
  172. package/dist/render-host/pageFrame.html +1 -1
  173. package/dist/render-host/preload.cjs +16 -4
  174. package/dist/renderer/assets/button-CB2MWjHY.js +2 -0
  175. package/dist/renderer/assets/constants-CkoGPQTS.js +2 -0
  176. package/dist/renderer/assets/constants-zQZ2-y21.js +2 -0
  177. package/dist/renderer/assets/{createLucideIcon-Bqi8hpdi.js → createLucideIcon-cZ9FlZZw.js} +2 -2
  178. package/dist/renderer/assets/devicePicker-H2eIO4e1.js +2 -0
  179. package/dist/renderer/assets/dialog-pHzEA8Vx.js +46 -0
  180. package/dist/renderer/assets/dist-B5HADirS.js +2 -0
  181. package/dist/renderer/assets/{dist-DKWdE1TK.js → dist-DIFlRMKe.js} +2 -2
  182. package/dist/renderer/assets/hostSidebarDefault-DRWh6OCt.js +2 -0
  183. package/dist/renderer/assets/index-CEL_XFge.js +2 -0
  184. package/dist/renderer/assets/{input-Ct3alvkq.js → input-CAObw95L.js} +2 -2
  185. package/dist/renderer/assets/ipc-channels-overlays-gIPUFnqc.js +2 -0
  186. package/dist/renderer/assets/jsx-runtime-DICEQH_0.css +1 -0
  187. package/dist/renderer/assets/{jsx-runtime-DGLHmmOq.js → jsx-runtime-DIw5wnYl.js} +1 -1
  188. package/dist/renderer/assets/plus-BQ9u5EbL.js +2 -0
  189. package/dist/renderer/assets/popover-Cw7G2Ctp.js +2 -0
  190. package/dist/renderer/assets/presets-DMZOb9ay.js +2 -0
  191. package/dist/renderer/assets/project-api-YTOr_Yvo.js +2 -0
  192. package/dist/renderer/assets/{projectCreateDialog-Bbjz75bP.js → projectCreateDialog-BmLqiYxn.js} +2 -2
  193. package/dist/renderer/assets/search-BMvuYF48.js +2 -0
  194. package/dist/renderer/assets/select-nFP95TxM.js +2 -0
  195. package/dist/renderer/assets/settings-CnpVfSWt.js +2 -0
  196. package/dist/renderer/assets/settings-api-Ccsf-Nsj.js +2 -0
  197. package/dist/renderer/assets/settings-tab-bar-CAzrv32m.js +2 -0
  198. package/dist/renderer/assets/smartphone-Bb2GdIKy.js +2 -0
  199. package/dist/renderer/assets/tabs-CPKN3Mmq.js +2 -0
  200. package/dist/renderer/assets/{tooltip-B_-_YVbw.js → tooltip-D6AxXRk5.js} +2 -2
  201. package/dist/renderer/assets/types-Bh0hA7El.js +2 -0
  202. package/dist/renderer/assets/updateDialog-CwndJ0Bl.js +2 -0
  203. package/dist/renderer/assets/{utils-kfQov6Pa.js → utils-sEB5OZEQ.js} +2 -2
  204. package/dist/renderer/assets/view-api-B-2zPBR5.js +2 -0
  205. package/dist/renderer/assets/view-ids-BfaDVV5P.js +2 -0
  206. package/dist/renderer/assets/workbench-BLJ0wKsg.js +5 -0
  207. package/dist/renderer/assets/{workbenchSettings-CV4LVA9b.js → workbenchSettings-YD0Ctbm_.js} +3 -3
  208. package/dist/renderer/entries/device-picker/index.html +28 -0
  209. package/dist/renderer/entries/host-sidebar-default/index.html +6 -5
  210. package/dist/renderer/entries/main/index.html +16 -17
  211. package/dist/renderer/entries/popover/index.html +16 -10
  212. package/dist/renderer/entries/project-create-dialog/index.html +12 -12
  213. package/dist/renderer/entries/settings/index.html +10 -10
  214. package/dist/renderer/entries/tooltip/index.html +5 -5
  215. package/dist/renderer/entries/update-dialog/index.html +10 -10
  216. package/dist/renderer/entries/workbench/index.html +49 -0
  217. package/dist/renderer/entries/workbench-settings/index.html +12 -11
  218. package/dist/service-host/host-env-update.cjs +23 -0
  219. package/dist/service-host/preload.cjs +24 -49
  220. package/dist/service-host/service-message-queue.cjs +71 -0
  221. package/dist/service-host/sync-impls/system-info.js +31 -9
  222. package/dist/shared/compile-mode-state.d.ts +73 -0
  223. package/dist/shared/compile-mode-state.js +100 -0
  224. package/dist/shared/compile-modes.d.ts +152 -0
  225. package/dist/shared/compile-modes.js +237 -0
  226. package/dist/shared/ipc-channels-overlays.d.ts +8 -2
  227. package/dist/shared/ipc-channels-overlays.js +19 -2
  228. package/dist/shared/ipc-channels.d.ts +5 -10
  229. package/dist/shared/ipc-channels.js +17 -14
  230. package/dist/shared/ipc-schemas.d.ts +76 -24
  231. package/dist/shared/ipc-schemas.js +55 -10
  232. package/dist/shared/project-window-types.d.ts +42 -0
  233. package/dist/shared/project-window-types.js +2 -0
  234. package/dist/shared/types.d.ts +43 -17
  235. package/dist/shared/view-ids.d.ts +1 -0
  236. package/dist/shared/view-ids.js +3 -1
  237. package/dist/simulator/assets/bridge-channels-B3JAmRfl.js +2 -0
  238. package/dist/simulator/assets/device-shell-ATEhHV2_.css +1 -0
  239. package/dist/simulator/assets/device-shell-pxnZZp10.js +431 -0
  240. package/dist/simulator/assets/simulator-Dwp7lnvu.js +10 -0
  241. package/dist/simulator/assets/simulator-mini-app-DFo7fhi5.js +2 -0
  242. package/dist/simulator/assets/simulator-ui-B2ddJB1R.js +2 -0
  243. package/dist/simulator/simulator.html +3 -3
  244. package/dist/simulator/temp-files.d.ts +6 -0
  245. package/dist/simulator/temp-files.js +46 -4
  246. package/dist/simulator/types.d.ts +7 -0
  247. package/dist/vscode-workbench/assets/__vite-browser-external-JIkWewFf.js +1 -0
  248. package/dist/vscode-workbench/assets/{dist-CMfmHflW.js → dist-CXG-u1VB.js} +3 -3
  249. package/dist/vscode-workbench/assets/{iconv-lite-umd-Bx8CgaZq.js → iconv-lite-umd-Dxx9Qyb4.js} +1 -1
  250. package/dist/vscode-workbench/assets/{index-CdG6YWhW.js → index-6VJMsaWj.js} +460 -460
  251. package/dist/vscode-workbench/assets/{jschardet-M0qgd2lN.js → jschardet-Ba3D16vt.js} +1 -1
  252. package/dist/vscode-workbench/index.html +1 -1
  253. package/package.json +11 -8
  254. package/dist/renderer/assets/button-D5Q2wkXq.js +0 -2
  255. package/dist/renderer/assets/constants-D7DfVqMp.js +0 -2
  256. package/dist/renderer/assets/constants-D8zsejFq.js +0 -2
  257. package/dist/renderer/assets/dialog-e1TecTOY.js +0 -46
  258. package/dist/renderer/assets/hostSidebarDefault-B3yLfy8j.js +0 -2
  259. package/dist/renderer/assets/index-CsPD68N_.js +0 -5
  260. package/dist/renderer/assets/ipc-channels-overlays-LsDzGAtV.js +0 -2
  261. package/dist/renderer/assets/jsx-runtime-2vXHouwr.css +0 -1
  262. package/dist/renderer/assets/popover-Dix5cm_b.js +0 -2
  263. package/dist/renderer/assets/project-api-k7Oq9iaq.js +0 -2
  264. package/dist/renderer/assets/select-acGb0AmJ.js +0 -2
  265. package/dist/renderer/assets/settings-CQrTMr43.js +0 -2
  266. package/dist/renderer/assets/settings-api-CrUQ9rYV.js +0 -2
  267. package/dist/renderer/assets/settings-tab-bar-Cw1kVIf2.js +0 -2
  268. package/dist/renderer/assets/types-CsEKtFyr.js +0 -2
  269. package/dist/renderer/assets/updateDialog-NsF8223A.js +0 -2
  270. package/dist/renderer/assets/view-api-CLn3rcQE.js +0 -2
  271. package/dist/simulator/assets/bridge-channels-zhsumfky.js +0 -2
  272. package/dist/simulator/assets/device-shell-CmtM1QxP.js +0 -2
  273. package/dist/simulator/assets/device-shell-iFZo1igL.css +0 -1
  274. package/dist/simulator/assets/simulator-CIobn3Mh.js +0 -10
  275. package/dist/simulator/assets/simulator-mini-app-CRe6n9br.js +0 -2
  276. package/dist/simulator/assets/simulator-ui-C_cP602X.js +0 -2
  277. package/dist/vscode-workbench/assets/__vite-browser-external-Bc1vizTf.js +0 -1
@@ -1,116 +1,15 @@
1
- /**
2
- * Cross-Origin-Isolation static server for the embedded VS Code workbench.
3
- *
4
- * The workbench needs `SharedArrayBuffer` for the TS web ext-host's
5
- * project-wide IntelliSense. Chromium only serves that to a document fetched
6
- * over http(s) carrying COOP `same-origin` + COEP `require-corp` (a custom
7
- * `protocol.handle` response does NOT unlock it), so the workbench bundle is
8
- * served from this in-process http server bound to 127.0.0.1 on a random port.
9
- * Every response carries the three isolation headers so same-origin
10
- * sub-resources and workers satisfy COEP `require-corp`.
11
- *
12
- * Kept separate from `dimina-resource-server` on purpose: that server sends
13
- * CORP `cross-origin` for the simulator bundles, which is incompatible with the
14
- * workbench's `same-origin` isolation requirement.
15
- *
16
- * `/__fs/*` bridges a `diminafs:`-style FileSystemProvider in the workbench to
17
- * the live active project on disk, reusing the renderer `project:fs:*`
18
- * sandbox's realpath-based guards (`resolveWithinProjectRoot`,
19
- * `assertWritableAncestor`, and the `O_NOFOLLOW` + post-open re-check inside
20
- * `readFileBuffer`/`writeFile`) so there is a SINGLE sandbox implementation: a
21
- * symlink inside the root pointing outside it cannot be followed to read or
22
- * write past the sandbox. The active project root is read per-request (via the
23
- * injected getter) so project switches take effect immediately.
24
- *
25
- * Mutating actions (write/mkdir/delete/rename) additionally require a non-GET
26
- * method and a same-origin (or origin-less) request, so an arbitrary localhost
27
- * page cannot drive a destructive `/__fs` call. GET serves only the read-only
28
- * stat/readdir/read actions.
29
- *
30
- * `/__fs/watch` is a read-only SSE stream of the active project's disk changes
31
- * (devtools-fs-core-feasibility.md §7), decoupled from the compile session so
32
- * the editor's memfs↔disk sync engine has a source that stays live regardless
33
- * of compile state; see {@link handleFsWatchRequest}.
34
- */
35
- import http from 'node:http';
36
- import nodeFs from 'node:fs';
37
1
  import fs from 'node:fs/promises';
38
2
  import path from 'node:path';
39
3
  import { readFileBufferWithin, writeFileWithin, statWithin, readdirWithin, mkdirWithin, deleteWithin, renameWithin, SKIP_DIRS, } from '../ipc/project-fs.js';
40
4
  import { handleFsWatchRequest } from './fs-watch-sse.js';
5
+ import { createCoiServerShutdown } from './workbench-coi-shutdown.js';
6
+ import { serveStaticFile } from './workbench-coi-static.js';
7
+ import { registerCoiHostSession } from './workbench-coi-host.js';
8
+ import { FS_TYPE_DIR, FS_TYPE_FILE, toFsBridgeEntry } from './fs-bridge-protocol.js';
41
9
  import { jsonRes } from './http-json.js';
42
10
  import { miniappPartitionKey } from './views/miniapp-partition.js';
43
11
  /** Max `/__fs/write` body; enough to save large source files without OOM. */
44
12
  const MAX_FS_WRITE_BYTES = 32 * 1024 * 1024;
45
- const MIME = {
46
- '.html': 'text/html; charset=utf-8',
47
- '.js': 'text/javascript; charset=utf-8',
48
- '.mjs': 'text/javascript; charset=utf-8',
49
- '.css': 'text/css; charset=utf-8',
50
- '.json': 'application/json; charset=utf-8',
51
- '.map': 'application/json; charset=utf-8',
52
- '.wasm': 'application/wasm',
53
- '.svg': 'image/svg+xml',
54
- '.png': 'image/png',
55
- '.ttf': 'font/ttf',
56
- '.woff': 'font/woff',
57
- '.woff2': 'font/woff2',
58
- '.ico': 'image/x-icon',
59
- };
60
- function setIsolationHeaders(res) {
61
- res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
62
- res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
63
- res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
64
- }
65
- /** Resolve a request pathname inside `root`, rejecting lexical traversal escapes. */
66
- function containedStaticPath(root, pathname) {
67
- const rel = pathname.replace(/^\/+/, '') || 'index.html';
68
- const resolved = path.resolve(root, rel);
69
- if (resolved !== root && !resolved.startsWith(root + path.sep))
70
- return null;
71
- return resolved;
72
- }
73
- /**
74
- * Serve a static file under `root`: lexical containment, then `fs.realpath`
75
- * BOTH the root and the resolved file before stat/stream, so a symlink inside
76
- * `root` pointing outside it cannot be served (the lexical check alone follows
77
- * symlinks because `stat`/`createReadStream` do). Sends the response itself.
78
- */
79
- function serveStaticFile(res, root, pathname) {
80
- const candidate = containedStaticPath(root, pathname);
81
- if (!candidate) {
82
- res.writeHead(403);
83
- res.end('Forbidden');
84
- return;
85
- }
86
- Promise.all([fs.realpath(root), fs.realpath(candidate)])
87
- .then(([realRoot, realFile]) => {
88
- if (realFile !== realRoot && !realFile.startsWith(realRoot + path.sep)) {
89
- res.writeHead(403);
90
- res.end('Forbidden');
91
- return;
92
- }
93
- nodeFs.stat(realFile, (err, stat) => {
94
- if (err || !stat.isFile()) {
95
- res.writeHead(404);
96
- res.end('Not Found');
97
- return;
98
- }
99
- res.writeHead(200, {
100
- 'Content-Type': MIME[path.extname(realFile)] ?? 'application/octet-stream',
101
- 'Content-Length': stat.size,
102
- 'Cache-Control': 'no-store',
103
- });
104
- // Stream the realpath'd file (already resolved to its real on-disk
105
- // location, so a symlink inside the root cannot redirect it outside).
106
- nodeFs.createReadStream(realFile).pipe(res);
107
- });
108
- })
109
- .catch(() => { if (!res.headersSent) {
110
- res.writeHead(404);
111
- res.end('Not Found');
112
- } });
113
- }
114
13
  /** Thrown by {@link readBody} when the accumulated body exceeds the cap. */
115
14
  class BodyTooLargeError extends Error {
116
15
  constructor() {
@@ -181,6 +80,22 @@ function rejectUnsafeMutation(req) {
181
80
  return null;
182
81
  }
183
82
  /** Map a node fs error to the HTTP status the provider expects. */
83
+ /**
84
+ * Percent-decode a request path, answering the client on a malformed escape.
85
+ * A stray `%` is the client's error and belongs here, not at the shared
86
+ * listener's exception boundary, which cannot tell a bad request from a bug in
87
+ * this handler. Returns `null` once the 400 has been sent.
88
+ */
89
+ function decodePath(res, pathname) {
90
+ try {
91
+ return decodeURIComponent(pathname);
92
+ }
93
+ catch {
94
+ res.writeHead(400);
95
+ res.end('Bad Request');
96
+ return null;
97
+ }
98
+ }
184
99
  function fsErrorStatus(e) {
185
100
  const code = e.code;
186
101
  if (code === 'ENOACTIVE')
@@ -199,7 +114,13 @@ function fsErrorStatus(e) {
199
114
  }
200
115
  async function fsStat(ctx) {
201
116
  const st = await statWithin(ctx.projectRoot, ctx.rel);
202
- jsonRes(ctx.res, 200, { type: st.isDirectory() ? 2 : 1, size: st.size, ctime: st.ctimeMs, mtime: st.mtimeMs });
117
+ const payload = {
118
+ type: st.isDirectory() ? FS_TYPE_DIR : FS_TYPE_FILE,
119
+ size: st.size,
120
+ ctime: st.ctimeMs,
121
+ mtime: st.mtimeMs,
122
+ };
123
+ jsonRes(ctx.res, 200, payload);
203
124
  }
204
125
  async function fsReaddir(ctx) {
205
126
  const entries = await readdirWithin(ctx.projectRoot, ctx.rel);
@@ -208,14 +129,8 @@ async function fsReaddir(ctx) {
208
129
  // this returns, so listing them would pull entire dependency trees into the
209
130
  // editor memfs and the OPFS ledger. Same-named FILES stay visible.
210
131
  const visible = entries.filter((e) => !(e.isDirectory && SKIP_DIRS.has(e.name)));
211
- // Wire shape: `[name, type]` for a directory, `[name, type, size, mtimeMs]`
212
- // for a file — the sync engine's watch-batch stat-diffing
213
- // (@dimina-kit/fs-core/sync/watch-expander) needs size+mtimeMs to tell a stat-unchanged
214
- // survivor from an actually-modified file without re-reading its content.
215
- // `size`/`mtimeMs` are simply absent (not `null`) for a stat-less entry
216
- // (readdirWithin's readdir/stat race) — the client side then always
217
- // treats it as changed.
218
- jsonRes(ctx.res, 200, visible.map((e) => (e.isDirectory ? [e.name, 2] : [e.name, 1, e.size, e.mtimeMs])));
132
+ // Wire shape and why it carries stats: see fs-bridge-protocol.ts.
133
+ jsonRes(ctx.res, 200, visible.map(toFsBridgeEntry));
219
134
  }
220
135
  async function fsRead(ctx) {
221
136
  const buf = await readFileBufferWithin(ctx.projectRoot, ctx.rel);
@@ -356,15 +271,28 @@ function workspaceIdOf(identity) {
356
271
  export async function startWorkbenchCoiServer(options) {
357
272
  const root = path.resolve(options.rootDir);
358
273
  const contribRoot = options.extensionsDir ? path.resolve(options.extensionsDir) : null;
359
- const server = http.createServer((req, res) => {
360
- setIsolationHeaders(res);
361
- const url = new URL(req.url ?? '/', 'http://127.0.0.1');
274
+ const shutdown = createCoiServerShutdown();
275
+ const handle = (req, res, url) => {
276
+ // This window is tearing down. Anything accepted from here on would act on
277
+ // a project the user has already closed, and a new SSE stream would outlive
278
+ // the drain that was supposed to end them all.
279
+ if (shutdown.closing) {
280
+ res.writeHead(503);
281
+ res.end('Closing');
282
+ return;
283
+ }
362
284
  // Special-cased ahead of the generic `/__fs/*` dispatch: unlike every other
363
285
  // action, this is a long-lived SSE stream, not a one-shot JSON response.
364
286
  if (url.pathname === '/__fs/watch') {
365
- handleFsWatchRequest(req, res, options.getProjectRoot);
287
+ const endStream = handleFsWatchRequest(req, res, options.getProjectRoot);
288
+ if (endStream)
289
+ shutdown.trackWatchStream(res, endStream);
366
290
  return;
367
291
  }
292
+ // Everything below answers once. Shutdown lets these land rather than
293
+ // dropping the socket under them — a `/__fs/write` mid-body is the file the
294
+ // user just saved.
295
+ shutdown.trackRequest(res);
368
296
  if (url.pathname.startsWith('/__fs/')) {
369
297
  handleFsRequest(req, res, options.getProjectRoot(), url).catch((e) => {
370
298
  if (!res.headersSent)
@@ -409,25 +337,39 @@ export async function startWorkbenchCoiServer(options) {
409
337
  jsonRes(res, 500, { error: String(e) }); });
410
338
  return;
411
339
  }
412
- const rel = decodeURIComponent(url.pathname.slice('/__contrib/'.length));
340
+ const rel = decodePath(res, url.pathname.slice('/__contrib/'.length));
341
+ if (rel === null)
342
+ return;
413
343
  serveStaticFile(res, contribRoot, rel);
414
344
  return;
415
345
  }
416
- serveStaticFile(res, root, decodeURIComponent(url.pathname));
417
- });
418
- await new Promise((resolve, reject) => {
419
- server.once('error', reject);
420
- server.listen(0, '127.0.0.1', () => {
421
- server.off('error', reject);
422
- resolve();
423
- });
424
- });
425
- const addr = server.address();
426
- const port = typeof addr === 'object' && addr ? addr.port : 0;
346
+ const filePath = decodePath(res, url.pathname);
347
+ if (filePath === null)
348
+ return;
349
+ serveStaticFile(res, root, filePath);
350
+ };
351
+ // The listener is shared with every other project window so they land on one
352
+ // origin; this window is reached through its own path prefix. See
353
+ // workbench-coi-host.ts for why the port cannot be per-window.
354
+ const registration = await registerCoiHostSession({ rootDir: root, extensionsDir: contribRoot, session: { handle } });
427
355
  return {
428
- baseUrl: `http://127.0.0.1:${port}/`,
429
- port,
430
- close: () => new Promise((r) => server.close(() => r())),
356
+ baseUrl: `${registration.origin}/w/${registration.token}/`,
357
+ port: registration.port,
358
+ // Why this is not a bare `server.close()`: see workbench-coi-shutdown.ts.
359
+ // Drain first so this window's SSE streams end and its in-flight writes
360
+ // land, THEN drop the route — the listener itself only goes away with the
361
+ // last window.
362
+ close: async () => {
363
+ // `finally`: a drain that throws must still drop this window's route.
364
+ // Leaving it registered would keep a torn-down window's bridge reachable
365
+ // on the shared listener for the rest of the process.
366
+ try {
367
+ await shutdown.drain();
368
+ }
369
+ finally {
370
+ await registration.release();
371
+ }
372
+ },
431
373
  };
432
374
  }
433
375
  //# sourceMappingURL=workbench-coi-server.js.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Shutdown bookkeeping for the workbench COI bridge, at two levels.
3
+ *
4
+ * A project window's bridge is closed as part of that window's teardown, which
5
+ * awaits it. Two things pull against each other at that moment. `/__fs/watch`
6
+ * is an SSE stream that only ever ends when its client disconnects, so waiting
7
+ * on it waits on the editor being destroyed in the same breath — the window
8
+ * hangs. Dropping every socket outright instead loses a `/__fs/write` whose
9
+ * body is still arriving, which is the file the user just saved.
10
+ *
11
+ * So neither extreme: {@link CoiServerShutdown.drain} ends the streams this
12
+ * owner started, lets a request already being served finish, and returns once
13
+ * it has — or once the grace window expires, so teardown is bounded no matter
14
+ * what a client does. The shared listener behind every window (see
15
+ * workbench-coi-host.ts) then uses {@link CoiServerShutdown.close}, which adds
16
+ * the server close and takes the sockets that outlived it.
17
+ */
18
+ import type http from 'node:http';
19
+ export interface CoiServerShutdown {
20
+ /** A live `/__fs/watch` stream, with the call that ends it server-side. */
21
+ trackWatchStream: (res: http.ServerResponse, endStream: () => void) => void;
22
+ /** A one-shot request whose response must be allowed to land. */
23
+ trackRequest: (res: http.ServerResponse) => void;
24
+ /**
25
+ * True once {@link drain} has started. The owner refuses new requests from
26
+ * here on: this bridge is bound to a window that is going away, and a
27
+ * request accepted now would act on a project the user has closed.
28
+ */
29
+ readonly closing: boolean;
30
+ /**
31
+ * End the tracked streams and wait for the tracked one-shot responses to
32
+ * land, bounded by the grace window. Leaves the listener alone — one window
33
+ * going away must not take the shared server with it.
34
+ */
35
+ drain: () => Promise<void>;
36
+ /** {@link drain}, plus closing the server and taking any surviving sockets. */
37
+ close: (server: http.Server) => Promise<void>;
38
+ }
39
+ export declare function createCoiServerShutdown(graceMs?: number): CoiServerShutdown;
40
+ //# sourceMappingURL=workbench-coi-shutdown.d.ts.map
@@ -0,0 +1,69 @@
1
+ /** How long a request already in flight may hold up the window's teardown. */
2
+ const DEFAULT_GRACE_MS = 2_000;
3
+ export function createCoiServerShutdown(graceMs = DEFAULT_GRACE_MS) {
4
+ const watchStreams = new Set();
5
+ const inFlight = new Set();
6
+ let onDrained = null;
7
+ let closing = false;
8
+ const drain = () => new Promise((resolve) => {
9
+ closing = true;
10
+ for (const endStream of watchStreams)
11
+ endStream();
12
+ watchStreams.clear();
13
+ // Nothing is being served: the sockets that remain are a client's to
14
+ // keep alive, and waiting on them is waiting on nobody.
15
+ if (inFlight.size === 0) {
16
+ resolve();
17
+ return;
18
+ }
19
+ let deadline = null;
20
+ const finish = () => {
21
+ onDrained = null;
22
+ if (deadline)
23
+ clearTimeout(deadline);
24
+ resolve();
25
+ };
26
+ const expire = () => {
27
+ // The grace window is up and something is still mid-request — a client
28
+ // that stalled halfway through a body, most likely. Destroying it here
29
+ // is the point: the handler still holds the closing window's project,
30
+ // so a body that lands minutes from now would write into a project the
31
+ // user has closed, and the socket would outlive the window on the
32
+ // shared listener.
33
+ for (const res of [...inFlight])
34
+ res.destroy();
35
+ inFlight.clear();
36
+ finish();
37
+ };
38
+ deadline = setTimeout(expire, graceMs);
39
+ onDrained = finish;
40
+ });
41
+ return {
42
+ get closing() {
43
+ return closing;
44
+ },
45
+ trackWatchStream(res, endStream) {
46
+ watchStreams.add(endStream);
47
+ res.on('close', () => watchStreams.delete(endStream));
48
+ },
49
+ trackRequest(res) {
50
+ inFlight.add(res);
51
+ res.on('close', () => {
52
+ inFlight.delete(res);
53
+ if (inFlight.size === 0)
54
+ onDrained?.();
55
+ });
56
+ },
57
+ drain,
58
+ async close(server) {
59
+ // Stop accepting first, so nothing new arrives during the drain; the
60
+ // callback only fires once every connection is gone, which is why the
61
+ // sockets are taken below rather than waited on.
62
+ const closed = new Promise((resolve) => server.close(() => resolve()));
63
+ await drain();
64
+ server.closeAllConnections();
65
+ await closed;
66
+ },
67
+ };
68
+ }
69
+ //# sourceMappingURL=workbench-coi-shutdown.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Static-file serving and the cross-origin-isolation headers shared by the
3
+ * workbench COI host and every window's bridge.
4
+ *
5
+ * Split out of `workbench-coi-server.ts` so the host module can serve the
6
+ * bundle without importing the per-window bridge (which imports the host).
7
+ */
8
+ import http from 'node:http';
9
+ /**
10
+ * The workbench needs `SharedArrayBuffer` for the TS web ext-host's
11
+ * project-wide IntelliSense, and Chromium only serves that to a
12
+ * cross-origin-isolated document. Every response carries the three headers so
13
+ * same-origin sub-resources and workers satisfy COEP `require-corp`.
14
+ */
15
+ export declare function setIsolationHeaders(res: http.ServerResponse): void;
16
+ /**
17
+ * Serve a static file under `root`: lexical containment, then `fs.realpath`
18
+ * BOTH the root and the resolved file before stat/stream, so a symlink inside
19
+ * `root` pointing outside it cannot be served (the lexical check alone follows
20
+ * symlinks because `stat`/`createReadStream` do). Sends the response itself.
21
+ */
22
+ export declare function serveStaticFile(res: http.ServerResponse, root: string, pathname: string): void;
23
+ //# sourceMappingURL=workbench-coi-static.d.ts.map
@@ -0,0 +1,87 @@
1
+ import nodeFs from 'node:fs';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ const MIME = {
5
+ '.html': 'text/html; charset=utf-8',
6
+ '.js': 'text/javascript; charset=utf-8',
7
+ '.mjs': 'text/javascript; charset=utf-8',
8
+ '.css': 'text/css; charset=utf-8',
9
+ '.json': 'application/json; charset=utf-8',
10
+ '.map': 'application/json; charset=utf-8',
11
+ '.wasm': 'application/wasm',
12
+ '.svg': 'image/svg+xml',
13
+ '.png': 'image/png',
14
+ '.ttf': 'font/ttf',
15
+ '.woff': 'font/woff',
16
+ '.woff2': 'font/woff2',
17
+ '.ico': 'image/x-icon',
18
+ };
19
+ /**
20
+ * The workbench needs `SharedArrayBuffer` for the TS web ext-host's
21
+ * project-wide IntelliSense, and Chromium only serves that to a
22
+ * cross-origin-isolated document. Every response carries the three headers so
23
+ * same-origin sub-resources and workers satisfy COEP `require-corp`.
24
+ */
25
+ export function setIsolationHeaders(res) {
26
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
27
+ res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
28
+ res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
29
+ }
30
+ /** Resolve a request pathname inside `root`, rejecting lexical traversal escapes. */
31
+ function containedStaticPath(root, pathname) {
32
+ const rel = pathname.replace(/^\/+/, '') || 'index.html';
33
+ const resolved = path.resolve(root, rel);
34
+ if (resolved !== root && !resolved.startsWith(root + path.sep))
35
+ return null;
36
+ return resolved;
37
+ }
38
+ /**
39
+ * Serve a static file under `root`: lexical containment, then `fs.realpath`
40
+ * BOTH the root and the resolved file before stat/stream, so a symlink inside
41
+ * `root` pointing outside it cannot be served (the lexical check alone follows
42
+ * symlinks because `stat`/`createReadStream` do). Sends the response itself.
43
+ */
44
+ export function serveStaticFile(res, root, pathname) {
45
+ const candidate = containedStaticPath(root, pathname);
46
+ if (!candidate) {
47
+ res.writeHead(403);
48
+ res.end('Forbidden');
49
+ return;
50
+ }
51
+ Promise.all([fs.realpath(root), fs.realpath(candidate)])
52
+ .then(([realRoot, realFile]) => {
53
+ if (realFile !== realRoot && !realFile.startsWith(realRoot + path.sep)) {
54
+ res.writeHead(403);
55
+ res.end('Forbidden');
56
+ return;
57
+ }
58
+ nodeFs.stat(realFile, (err, stat) => {
59
+ if (err || !stat.isFile()) {
60
+ res.writeHead(404);
61
+ res.end('Not Found');
62
+ return;
63
+ }
64
+ res.writeHead(200, {
65
+ 'Content-Type': MIME[path.extname(realFile)] ?? 'application/octet-stream',
66
+ 'Content-Length': stat.size,
67
+ 'Cache-Control': 'no-store',
68
+ });
69
+ // Stream the realpath'd file (already resolved to its real on-disk
70
+ // location, so a symlink inside the root cannot redirect it outside).
71
+ const stream = nodeFs.createReadStream(realFile);
72
+ // The file can vanish or lose permissions between the stat and the
73
+ // open. The stream reports that asynchronously, once the 200 and the
74
+ // Content-Length are already on the wire, so no status can be changed
75
+ // — and an unhandled 'error' on a stream is an uncaught exception in
76
+ // the main process. Take the socket instead, which is what a client
77
+ // sees for a truncated body anyway.
78
+ stream.on('error', () => { res.destroy(); });
79
+ stream.pipe(res);
80
+ });
81
+ })
82
+ .catch(() => { if (!res.headersSent) {
83
+ res.writeHead(404);
84
+ res.end('Not Found');
85
+ } });
86
+ }
87
+ //# sourceMappingURL=workbench-coi-static.js.map
@@ -12,6 +12,7 @@ import type { StorageApi } from './simulator-storage/index.js';
12
12
  import type { SyncStorageChange } from '../../shared/ipc-channels.js';
13
13
  import { DisposableRegistry, type ConnectionRegistry } from '@dimina-kit/electron-deck/main';
14
14
  import type { SenderPolicy } from '../utils/ipc-registry.js';
15
+ import type { AppServices } from './app-services.js';
15
16
  import { type RendererNotifier } from './notifications/renderer-notifier.js';
16
17
  import { type CompileLogBuffer } from './workspace/compile-log-buffer.js';
17
18
  import { type SessionStatusStore } from './workspace/session-status-store.js';
@@ -120,6 +121,14 @@ export interface WorkbenchContext extends RuntimeContext {
120
121
  * lazily so it stays correct as windows/views come and go.
121
122
  */
122
123
  senderPolicy: SenderPolicy;
124
+ /**
125
+ * The subset of `senderPolicy` this context structurally OWNS — its own
126
+ * window's renderer, its settings window and the overlay views mounted
127
+ * inside it. Excludes the app-wide trusted-window map, which no single
128
+ * context can claim. `createWindowContextRouter` uses it to decide which
129
+ * window a message actually came from.
130
+ */
131
+ ownsSender: SenderPolicy;
123
132
  /**
124
133
  * Reference-counted map of `webContents.id` → live registration count for
125
134
  * host-owned BrowserWindows registered as trusted senders via
@@ -128,12 +137,20 @@ export interface WorkbenchContext extends RuntimeContext {
128
137
  * single `closed` event, which zeroes the count outright) to un-trust it.
129
138
  * Consulted by `createWorkbenchSenderPolicy` in addition to the static
130
139
  * main-window / overlay checks.
140
+ *
141
+ * Supplied by {@link AppServices} when the caller has one: the host
142
+ * registers its window against the application, so the same map backs every
143
+ * context.
131
144
  */
132
145
  trustedWindowSenderIds: Map<number, number>;
133
146
  /**
134
- * Per-context registry of host-registered simulator custom APIs. Populated
135
- * via `instance.registerSimulatorApi`; read by the simulator IPC handlers.
136
- * One registry per context — no process-global crosstalk.
147
+ * Registry of host-registered simulator custom APIs. Populated via
148
+ * `instance.registerSimulatorApi`; read by the simulator IPC handlers.
149
+ *
150
+ * Supplied by {@link AppServices} when the caller has one, and therefore
151
+ * shared across contexts: the host registers each handler once, so a window
152
+ * opened afterwards must still answer those `wx.*` calls. A context built
153
+ * without app services (focused unit tests) gets its own registry.
137
154
  */
138
155
  simulatorApis: SimulatorApiRegistry;
139
156
  /**
@@ -258,6 +275,13 @@ export interface CreateContextOptions extends Pick<WorkbenchConfig, 'adapter' |
258
275
  mainWindow: BrowserWindow;
259
276
  preloadPath: string;
260
277
  rendererDir: string;
278
+ /**
279
+ * Services owned by the application rather than by this window. When
280
+ * present their instances are adopted instead of constructing per-context
281
+ * ones, so every window shares the host's single registration. Optional so
282
+ * focused unit tests can still build a stand-alone context.
283
+ */
284
+ appServices?: AppServices;
261
285
  brandingProvider?: WorkbenchContext['brandingProvider'];
262
286
  /** Host-supplied project list backend. Defaults to LocalProjectsProvider. */
263
287
  projectsProvider?: ProjectsProvider;
@@ -1,7 +1,7 @@
1
1
  import { createRuntimeEvents, } from '@dimina-kit/electron-runtime/main/runtime-context';
2
2
  import { createCdpSessionBroker } from './cdp-session/index.js';
3
3
  import { createConnectionRegistry, DisposableRegistry, } from '@dimina-kit/electron-deck/main';
4
- import { createWorkbenchSenderPolicy } from '../utils/sender-policy.js';
4
+ import { createWorkbenchOwnedSenderCheck, createWorkbenchSenderPolicy, } from '../utils/sender-policy.js';
5
5
  import { defaultAdapter } from './default-adapter.js';
6
6
  import { createRendererNotifier, } from './notifications/renderer-notifier.js';
7
7
  import { createCompileLogBuffer, } from './workspace/compile-log-buffer.js';
@@ -50,8 +50,9 @@ export function createWorkbenchContext(opts) {
50
50
  ctx.connections = createConnectionRegistry();
51
51
  ctx.cdpSessionBroker = createCdpSessionBroker({ connections: ctx.connections });
52
52
  ctx.registry.add(() => ctx.cdpSessionBroker.dispose());
53
- ctx.trustedWindowSenderIds = new Map();
54
- ctx.simulatorApis = createSimulatorApiRegistry();
53
+ ctx.trustedWindowSenderIds =
54
+ opts.appServices?.trustedWindowSenderIds ?? new Map();
55
+ ctx.simulatorApis = opts.appServices?.simulatorApis ?? createSimulatorApiRegistry();
55
56
  ctx.simulatorUiExtensions = createSimulatorUiExtensionRegistry();
56
57
  ctx.registry.add(() => ctx.simulatorUiExtensions.clear());
57
58
  ctx.windows = createWindowService(opts.mainWindow);
@@ -71,12 +72,16 @@ export function createWorkbenchContext(opts) {
71
72
  // at call time through the live context, which structurally satisfies the
72
73
  // helper's narrow OpenSettingsWindowDeps.
73
74
  ctx.openSettings = () => openSettingsWindow(ctx);
74
- ctx.projectsProvider = opts.projectsProvider ?? createLocalProjectsProvider();
75
- ctx.projectTemplates = resolveTemplates(BUILTIN_TEMPLATES, opts.projectTemplates ?? [], opts.builtinTemplates ?? 'all');
75
+ ctx.projectsProvider =
76
+ opts.appServices?.projectsProvider ?? opts.projectsProvider ?? createLocalProjectsProvider();
77
+ ctx.projectTemplates =
78
+ opts.appServices?.projectTemplates ??
79
+ resolveTemplates(BUILTIN_TEMPLATES, opts.projectTemplates ?? [], opts.builtinTemplates ?? 'all');
76
80
  ctx.customCreateProjectDialog = opts.customCreateProjectDialog;
77
81
  ctx.customEditProjectDialog = opts.customEditProjectDialog;
78
82
  ctx.onBeforeOpenProject = opts.onBeforeOpenProject;
79
83
  ctx.workspace = createWorkspaceService(ctx);
84
+ ctx.ownsSender = createWorkbenchOwnedSenderCheck(ctx);
80
85
  ctx.senderPolicy = createWorkbenchSenderPolicy(ctx);
81
86
  return ctx;
82
87
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Adapts the compile-mode half of `ProjectsProvider` — where every method is
3
+ * optional — onto the four methods `WorkspaceService` always exposes.
4
+ *
5
+ * A host may implement the mode list, only the older single-config pair, or
6
+ * neither. Each direction degrades on its own so a partial host still gets the
7
+ * behavior it can support instead of an exception.
8
+ */
9
+ import type { CompileConfig, CompileModes } from '../../../shared/types.js';
10
+ import type { ProjectsProvider } from '../projects/types.js';
11
+ export interface CompileModeAdapter {
12
+ getCompileConfig(projectPath: string): Promise<CompileConfig>;
13
+ saveCompileConfig(projectPath: string, config: CompileConfig): Promise<void>;
14
+ getCompileModes(projectPath: string): Promise<CompileModes>;
15
+ saveCompileModes(projectPath: string, modes: CompileModes): Promise<void>;
16
+ }
17
+ export declare function createCompileModeAdapter(provider: ProjectsProvider): CompileModeAdapter;
18
+ //# sourceMappingURL=compile-mode-adapter.d.ts.map
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Adapts the compile-mode half of `ProjectsProvider` — where every method is
3
+ * optional — onto the four methods `WorkspaceService` always exposes.
4
+ *
5
+ * A host may implement the mode list, only the older single-config pair, or
6
+ * neither. Each direction degrades on its own so a partial host still gets the
7
+ * behavior it can support instead of an exception.
8
+ */
9
+ import { compileConfigToModes, emptyCompileModes, resolveCompileConfig, } from '../../../shared/compile-modes.js';
10
+ import { DEFAULT_COMPILE_CONFIG } from '../projects/types.js';
11
+ export function createCompileModeAdapter(provider) {
12
+ return {
13
+ // Prefer the host's own resolver: only it can substitute the project's
14
+ // entry page for 普通编译's empty start page. Deriving from the mode list
15
+ // is the fallback for hosts that implement modes but not this.
16
+ async getCompileConfig(projectPath) {
17
+ if (provider.getCompileConfig) {
18
+ return (await provider.getCompileConfig(projectPath));
19
+ }
20
+ if (provider.getCompileModes) {
21
+ return resolveCompileConfig(await provider.getCompileModes(projectPath));
22
+ }
23
+ return DEFAULT_COMPILE_CONFIG;
24
+ },
25
+ async saveCompileConfig(projectPath, config) {
26
+ if (provider.saveCompileConfig) {
27
+ await provider.saveCompileConfig(projectPath, config);
28
+ }
29
+ // No persistence when the host opts out; the renderer's edits then
30
+ // do not survive a reload, matching the documented contract.
31
+ },
32
+ // Mirror of saveCompileModes' degradation: a host still on the single
33
+ // config gets it projected into one selected mode, so its start page and
34
+ // params actually launch instead of being read as 普通编译 and dropped.
35
+ async getCompileModes(projectPath) {
36
+ if (provider.getCompileModes) {
37
+ return await provider.getCompileModes(projectPath);
38
+ }
39
+ if (provider.getCompileConfig) {
40
+ return compileConfigToModes((await provider.getCompileConfig(projectPath)));
41
+ }
42
+ return emptyCompileModes();
43
+ },
44
+ async saveCompileModes(projectPath, modes) {
45
+ if (provider.saveCompileModes) {
46
+ await provider.saveCompileModes(projectPath, modes);
47
+ return;
48
+ }
49
+ // Degrade for a host still on the single-config setter: the selection
50
+ // still takes effect, it just cannot store the named list.
51
+ if (provider.saveCompileConfig) {
52
+ await provider.saveCompileConfig(projectPath, resolveCompileConfig(modes));
53
+ }
54
+ },
55
+ };
56
+ }
57
+ //# sourceMappingURL=compile-mode-adapter.js.map