@dxos/app-framework 0.8.4-main.9be5663bfe → 0.8.4-main.abd8ff62ef

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 (278) hide show
  1. package/dist/lib/browser/{capability-BBBBAPDI.mjs → capability-Q5XRXRD2.mjs} +10 -10
  2. package/dist/lib/browser/{capability-OP63CD5N.mjs → capability-V7LR4LQN.mjs} +11 -11
  3. package/dist/lib/browser/capability-V7LR4LQN.mjs.map +7 -0
  4. package/dist/lib/browser/{chunk-T3Y4AEKX.mjs → chunk-23D4SJUE.mjs} +3 -3
  5. package/dist/lib/browser/{chunk-T3Y4AEKX.mjs.map → chunk-23D4SJUE.mjs.map} +1 -1
  6. package/dist/lib/browser/{chunk-2CKCJ6PN.mjs → chunk-3JWJXGLK.mjs} +1 -1
  7. package/dist/lib/browser/{chunk-2CKCJ6PN.mjs.map → chunk-3JWJXGLK.mjs.map} +1 -1
  8. package/dist/lib/browser/{chunk-GX4TUNM6.mjs → chunk-3ZS2A3DN.mjs} +170 -226
  9. package/dist/lib/browser/chunk-3ZS2A3DN.mjs.map +7 -0
  10. package/dist/lib/browser/{chunk-I34GF4NG.mjs → chunk-45CHLTBV.mjs} +2 -2
  11. package/dist/lib/browser/chunk-5LAIGWLU.mjs +467 -0
  12. package/dist/lib/browser/chunk-5LAIGWLU.mjs.map +7 -0
  13. package/dist/lib/browser/{chunk-QSXYHXCE.mjs → chunk-66IXTIVK.mjs} +1 -1
  14. package/dist/lib/browser/{chunk-QSXYHXCE.mjs.map → chunk-66IXTIVK.mjs.map} +2 -2
  15. package/dist/lib/browser/{chunk-TGX63LTL.mjs → chunk-FJ4765WW.mjs} +1 -1
  16. package/dist/lib/browser/{chunk-TGX63LTL.mjs.map → chunk-FJ4765WW.mjs.map} +2 -2
  17. package/dist/lib/browser/chunk-G7SDBRKH.mjs +1 -0
  18. package/dist/lib/browser/chunk-JXCBZSBJ.mjs +372 -0
  19. package/dist/lib/browser/chunk-JXCBZSBJ.mjs.map +7 -0
  20. package/dist/lib/browser/chunk-MX5DKEJH.mjs +584 -0
  21. package/dist/lib/browser/chunk-MX5DKEJH.mjs.map +7 -0
  22. package/dist/lib/browser/{chunk-JKWMHZP6.mjs → chunk-WBHCSOBW.mjs} +2 -2
  23. package/dist/lib/browser/chunk-WBHCSOBW.mjs.map +7 -0
  24. package/dist/lib/browser/{chunk-FU4GAFUQ.mjs → chunk-Z55LVAGN.mjs} +80 -15
  25. package/dist/lib/browser/chunk-Z55LVAGN.mjs.map +7 -0
  26. package/dist/lib/browser/{chunk-F7FW2RK2.mjs → chunk-ZGJAZSNE.mjs} +7 -32
  27. package/dist/lib/browser/chunk-ZGJAZSNE.mjs.map +7 -0
  28. package/dist/lib/browser/cli/index.mjs +11 -27
  29. package/dist/lib/browser/cli/index.mjs.map +2 -2
  30. package/dist/lib/browser/common/activation-events.mjs +7 -7
  31. package/dist/lib/browser/common/capabilities.mjs +7 -7
  32. package/dist/lib/browser/core/activation-event.mjs +1 -1
  33. package/dist/lib/browser/core/capability.mjs +1 -1
  34. package/dist/lib/browser/core/plugin-manager.mjs +6 -4
  35. package/dist/lib/browser/core/plugin.mjs +10 -2
  36. package/dist/lib/browser/core/url-loader.mjs +13 -5
  37. package/dist/lib/browser/index.mjs +22 -18
  38. package/dist/lib/browser/index.mjs.map +3 -3
  39. package/dist/lib/browser/{invoker-capability-H5PPENOC.mjs → invoker-capability-LNX4CGIV.mjs} +12 -11
  40. package/dist/lib/browser/invoker-capability-LNX4CGIV.mjs.map +7 -0
  41. package/dist/lib/browser/meta.json +1 -1
  42. package/dist/lib/browser/testing/index.mjs +144 -27
  43. package/dist/lib/browser/testing/index.mjs.map +4 -4
  44. package/dist/lib/browser/testing/react.mjs +78 -0
  45. package/dist/lib/browser/testing/react.mjs.map +7 -0
  46. package/dist/lib/browser/ui/index.mjs +18 -14
  47. package/dist/lib/node-esm/{capability-AWBEMRYR.mjs → capability-EW5GJCI6.mjs} +10 -10
  48. package/dist/lib/node-esm/{capability-WFEG6CIZ.mjs → capability-YKBMMD53.mjs} +11 -11
  49. package/dist/lib/node-esm/capability-YKBMMD53.mjs.map +7 -0
  50. package/dist/lib/node-esm/{chunk-FKE4Z3D6.mjs → chunk-37Z53PXZ.mjs} +1 -1
  51. package/dist/lib/node-esm/{chunk-FKE4Z3D6.mjs.map → chunk-37Z53PXZ.mjs.map} +2 -2
  52. package/dist/lib/node-esm/{chunk-WZCSOX5Q.mjs → chunk-6XW6LET6.mjs} +2 -2
  53. package/dist/lib/node-esm/{chunk-URWHJQT2.mjs → chunk-D347W3KO.mjs} +7 -32
  54. package/dist/lib/node-esm/chunk-D347W3KO.mjs.map +7 -0
  55. package/dist/lib/node-esm/chunk-D5PO2WXX.mjs +373 -0
  56. package/dist/lib/node-esm/chunk-D5PO2WXX.mjs.map +7 -0
  57. package/dist/lib/node-esm/{chunk-ULUEXB7Q.mjs → chunk-HTBJU5FX.mjs} +80 -15
  58. package/dist/lib/node-esm/chunk-HTBJU5FX.mjs.map +7 -0
  59. package/dist/lib/node-esm/chunk-KM2F6GH6.mjs +468 -0
  60. package/dist/lib/node-esm/chunk-KM2F6GH6.mjs.map +7 -0
  61. package/dist/lib/node-esm/{chunk-EL3R25OQ.mjs → chunk-OZ7DZA5Z.mjs} +1 -1
  62. package/dist/lib/node-esm/{chunk-BCEOLX47.mjs → chunk-Q7XBFII4.mjs} +170 -226
  63. package/dist/lib/node-esm/chunk-Q7XBFII4.mjs.map +7 -0
  64. package/dist/lib/node-esm/{chunk-VKHGNEDB.mjs → chunk-SBS2YMPT.mjs} +3 -3
  65. package/dist/lib/node-esm/{chunk-VKHGNEDB.mjs.map → chunk-SBS2YMPT.mjs.map} +1 -1
  66. package/dist/lib/node-esm/{chunk-42KBWDE4.mjs → chunk-SDJ4B2LU.mjs} +1 -1
  67. package/dist/lib/node-esm/{chunk-42KBWDE4.mjs.map → chunk-SDJ4B2LU.mjs.map} +1 -1
  68. package/dist/lib/node-esm/{chunk-G3RTFSNG.mjs → chunk-WFSRZKBP.mjs} +2 -2
  69. package/dist/lib/node-esm/chunk-WFSRZKBP.mjs.map +7 -0
  70. package/dist/lib/node-esm/chunk-WKTLE7MG.mjs +585 -0
  71. package/dist/lib/node-esm/chunk-WKTLE7MG.mjs.map +7 -0
  72. package/dist/lib/node-esm/{chunk-ZZ7CKK6W.mjs → chunk-XOCUANHO.mjs} +1 -1
  73. package/dist/lib/node-esm/{chunk-ZZ7CKK6W.mjs.map → chunk-XOCUANHO.mjs.map} +2 -2
  74. package/dist/lib/node-esm/cli/index.mjs +11 -27
  75. package/dist/lib/node-esm/cli/index.mjs.map +2 -2
  76. package/dist/lib/node-esm/common/activation-events.mjs +7 -7
  77. package/dist/lib/node-esm/common/capabilities.mjs +7 -7
  78. package/dist/lib/node-esm/core/activation-event.mjs +1 -1
  79. package/dist/lib/node-esm/core/capability.mjs +1 -1
  80. package/dist/lib/node-esm/core/plugin-manager.mjs +6 -4
  81. package/dist/lib/node-esm/core/plugin.mjs +10 -2
  82. package/dist/lib/node-esm/core/url-loader.mjs +13 -5
  83. package/dist/lib/node-esm/index.mjs +22 -18
  84. package/dist/lib/node-esm/index.mjs.map +3 -3
  85. package/dist/lib/node-esm/{invoker-capability-S3ZA527J.mjs → invoker-capability-O4T5PHLA.mjs} +12 -11
  86. package/dist/lib/node-esm/invoker-capability-O4T5PHLA.mjs.map +7 -0
  87. package/dist/lib/node-esm/meta.json +1 -1
  88. package/dist/lib/node-esm/testing/index.mjs +144 -27
  89. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  90. package/dist/lib/node-esm/testing/react.mjs +79 -0
  91. package/dist/lib/node-esm/testing/react.mjs.map +7 -0
  92. package/dist/lib/node-esm/ui/index.mjs +18 -14
  93. package/dist/plugin/node-esm/index.mjs +480 -32
  94. package/dist/plugin/node-esm/index.mjs.map +4 -4
  95. package/dist/plugin/node-esm/meta.json +1 -1
  96. package/dist/types/src/common/capabilities.d.ts +2 -1
  97. package/dist/types/src/common/capabilities.d.ts.map +1 -1
  98. package/dist/types/src/common/operations.d.ts +1 -1
  99. package/dist/types/src/common/operations.d.ts.map +1 -1
  100. package/dist/types/src/core/activation-event.d.ts +4 -4
  101. package/dist/types/src/core/activation-event.d.ts.map +1 -1
  102. package/dist/types/src/core/capability-manager.d.ts.map +1 -1
  103. package/dist/types/src/core/capability.d.ts +2 -2
  104. package/dist/types/src/core/capability.d.ts.map +1 -1
  105. package/dist/types/src/core/index.d.ts +2 -0
  106. package/dist/types/src/core/index.d.ts.map +1 -1
  107. package/dist/types/src/core/plugin-asset-cache.d.ts +71 -0
  108. package/dist/types/src/core/plugin-asset-cache.d.ts.map +1 -0
  109. package/dist/types/src/core/plugin-manager.d.ts +51 -2
  110. package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
  111. package/dist/types/src/core/plugin-manifest.d.ts +76 -0
  112. package/dist/types/src/core/plugin-manifest.d.ts.map +1 -0
  113. package/dist/types/src/core/plugin-manifest.test.d.ts +2 -0
  114. package/dist/types/src/core/plugin-manifest.test.d.ts.map +1 -0
  115. package/dist/types/src/core/plugin.d.ts +107 -6
  116. package/dist/types/src/core/plugin.d.ts.map +1 -1
  117. package/dist/types/src/core/url-loader.d.ts +90 -3
  118. package/dist/types/src/core/url-loader.d.ts.map +1 -1
  119. package/dist/types/src/helpers.d.ts.map +1 -1
  120. package/dist/types/src/plugin-operation/history/capability.d.ts.map +1 -1
  121. package/dist/types/src/plugin-operation/history/errors.d.ts +6 -6
  122. package/dist/types/src/plugin-operation/history/errors.d.ts.map +1 -1
  123. package/dist/types/src/plugin-operation/history/history-tracker.d.ts +1 -1
  124. package/dist/types/src/plugin-operation/history/history-tracker.d.ts.map +1 -1
  125. package/dist/types/src/plugin-operation/history/types.d.ts +1 -1
  126. package/dist/types/src/plugin-operation/history/types.d.ts.map +1 -1
  127. package/dist/types/src/plugin-operation/history/undo-mapping.d.ts +1 -1
  128. package/dist/types/src/plugin-operation/history/undo-mapping.d.ts.map +1 -1
  129. package/dist/types/src/plugin-operation/history/undo-registry.d.ts +1 -1
  130. package/dist/types/src/plugin-operation/history/undo-registry.d.ts.map +1 -1
  131. package/dist/types/src/plugin-operation/invoker-capability.d.ts +1 -1
  132. package/dist/types/src/plugin-operation/invoker-capability.d.ts.map +1 -1
  133. package/dist/types/src/plugin-operation/testing.d.ts +2 -1
  134. package/dist/types/src/plugin-operation/testing.d.ts.map +1 -1
  135. package/dist/types/src/plugin-runtime/capability.d.ts +1 -1
  136. package/dist/types/src/plugin-runtime/capability.d.ts.map +1 -1
  137. package/dist/types/src/testing/harness.d.ts +67 -0
  138. package/dist/types/src/testing/harness.d.ts.map +1 -0
  139. package/dist/types/src/testing/index.d.ts +1 -0
  140. package/dist/types/src/testing/index.d.ts.map +1 -1
  141. package/dist/types/src/testing/react.d.ts +27 -0
  142. package/dist/types/src/testing/react.d.ts.map +1 -0
  143. package/dist/types/src/testing/react.test.d.ts +2 -0
  144. package/dist/types/src/testing/react.test.d.ts.map +1 -0
  145. package/dist/types/src/testing/service.d.ts.map +1 -1
  146. package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
  147. package/dist/types/src/testing/withPluginManager.stories.d.ts.map +1 -1
  148. package/dist/types/src/ui/components/App/App.d.ts.map +1 -1
  149. package/dist/types/src/ui/components/App/App.stories.d.ts.map +1 -1
  150. package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts +64 -0
  151. package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts.map +1 -0
  152. package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts +19 -0
  153. package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts.map +1 -0
  154. package/dist/types/src/ui/components/Placeholder/index.d.ts +2 -0
  155. package/dist/types/src/ui/components/Placeholder/index.d.ts.map +1 -0
  156. package/dist/types/src/ui/components/PluginManager/PluginManagerContext.stories.d.ts.map +1 -1
  157. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts +16 -4
  158. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts.map +1 -1
  159. package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts.map +1 -1
  160. package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts.map +1 -1
  161. package/dist/types/src/ui/components/Surface/index.d.ts +16 -6
  162. package/dist/types/src/ui/components/Surface/index.d.ts.map +1 -1
  163. package/dist/types/src/ui/components/Surface/types.d.ts +110 -9
  164. package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
  165. package/dist/types/src/ui/components/Surface/types.test.d.ts +2 -0
  166. package/dist/types/src/ui/components/Surface/types.test.d.ts.map +1 -0
  167. package/dist/types/src/ui/components/index.d.ts +1 -0
  168. package/dist/types/src/ui/components/index.d.ts.map +1 -1
  169. package/dist/types/src/ui/hooks/useApp.d.ts +29 -3
  170. package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
  171. package/dist/types/src/ui/hooks/useCapabilities.d.ts.map +1 -1
  172. package/dist/types/src/ui/hooks/useLoading.d.ts.map +1 -1
  173. package/dist/types/src/ui/hooks/useSettingsState.d.ts.map +1 -1
  174. package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts +34 -0
  175. package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts.map +1 -0
  176. package/dist/types/src/vite-plugin/boot-loader/index.d.ts +52 -0
  177. package/dist/types/src/vite-plugin/boot-loader/index.d.ts.map +1 -0
  178. package/dist/types/src/vite-plugin/composer/index.d.ts +34 -0
  179. package/dist/types/src/vite-plugin/composer/index.d.ts.map +1 -0
  180. package/dist/types/src/vite-plugin/import-map/index.d.ts +28 -0
  181. package/dist/types/src/vite-plugin/import-map/index.d.ts.map +1 -0
  182. package/dist/types/src/vite-plugin/index.d.ts +4 -2
  183. package/dist/types/src/vite-plugin/index.d.ts.map +1 -1
  184. package/dist/types/src/vite-plugin/manifest.d.ts +37 -0
  185. package/dist/types/src/vite-plugin/manifest.d.ts.map +1 -0
  186. package/dist/types/src/vite-plugin/manifest.test.d.ts +2 -0
  187. package/dist/types/src/vite-plugin/manifest.test.d.ts.map +1 -0
  188. package/dist/types/src/vite-plugin/packages.d.ts +10 -4
  189. package/dist/types/src/vite-plugin/packages.d.ts.map +1 -1
  190. package/dist/types/tsconfig.tsbuildinfo +1 -1
  191. package/moon.yml +1 -0
  192. package/package.json +33 -59
  193. package/src/common/capabilities.ts +2 -1
  194. package/src/common/operations.ts +1 -1
  195. package/src/core/capability.ts +1 -1
  196. package/src/core/index.ts +2 -0
  197. package/src/core/plugin-asset-cache.ts +60 -0
  198. package/src/core/plugin-manager.test.ts +246 -5
  199. package/src/core/plugin-manager.ts +167 -25
  200. package/src/core/plugin-manifest.test.ts +48 -0
  201. package/src/core/plugin-manifest.ts +102 -0
  202. package/src/core/plugin.ts +135 -10
  203. package/src/core/url-loader.test.ts +104 -5
  204. package/src/core/url-loader.ts +226 -37
  205. package/src/plugin-operation/OperationPlugin.ts +2 -2
  206. package/src/plugin-operation/history/capability.ts +1 -1
  207. package/src/plugin-operation/history/history-tracker.test.ts +2 -1
  208. package/src/plugin-operation/history/history-tracker.ts +1 -1
  209. package/src/plugin-operation/history/types.ts +1 -1
  210. package/src/plugin-operation/history/undo-mapping.ts +1 -1
  211. package/src/plugin-operation/history/undo-registry.ts +1 -1
  212. package/src/plugin-operation/invoker-capability.ts +2 -1
  213. package/src/plugin-operation/testing.ts +2 -1
  214. package/src/plugin-runtime/RuntimePlugin.ts +2 -2
  215. package/src/testing/harness.ts +229 -0
  216. package/src/testing/index.ts +1 -0
  217. package/src/testing/react.test.tsx +48 -0
  218. package/src/testing/react.tsx +113 -0
  219. package/src/testing/withPluginManager.stories.tsx +1 -1
  220. package/src/ui/components/App/App.stories.tsx +1 -1
  221. package/src/ui/components/App/App.tsx +25 -2
  222. package/src/ui/components/Placeholder/Placeholder.stories.tsx +77 -0
  223. package/src/ui/components/Placeholder/Placeholder.tsx +155 -0
  224. package/src/ui/components/Placeholder/index.ts +5 -0
  225. package/src/ui/components/PluginManager/PluginManagerContext.stories.tsx +4 -2
  226. package/src/ui/components/Surface/SurfaceComponent.stories.tsx +1 -1
  227. package/src/ui/components/Surface/SurfaceComponent.tsx +83 -46
  228. package/src/ui/components/Surface/index.ts +20 -1
  229. package/src/ui/components/Surface/types.test.ts +126 -0
  230. package/src/ui/components/Surface/types.ts +164 -12
  231. package/src/ui/components/index.ts +1 -0
  232. package/src/ui/hooks/useApp.tsx +165 -41
  233. package/src/ui/hooks/useLoading.tsx +14 -6
  234. package/src/vite-plugin/boot-loader/BootLoader.stories.tsx +263 -0
  235. package/src/vite-plugin/boot-loader/boot-loader.css +294 -0
  236. package/src/vite-plugin/boot-loader/boot-loader.js +274 -0
  237. package/src/vite-plugin/boot-loader/index.ts +112 -0
  238. package/src/vite-plugin/composer/index.ts +277 -0
  239. package/src/vite-plugin/import-map/index.ts +524 -0
  240. package/src/vite-plugin/index.ts +6 -2
  241. package/src/vite-plugin/manifest.test.ts +24 -0
  242. package/src/vite-plugin/manifest.ts +50 -0
  243. package/src/vite-plugin/packages.ts +169 -10
  244. package/tsconfig.json +9 -0
  245. package/.swc/plugins/linux_x86_64_19.0.0/727453fb3a62f7f1d952a41e051ca8a6f88cadc45cee43c6a4d1aa45f9b75665.wasmer-v7 +0 -0
  246. package/dist/lib/browser/capability-OP63CD5N.mjs.map +0 -7
  247. package/dist/lib/browser/chunk-F7FW2RK2.mjs.map +0 -7
  248. package/dist/lib/browser/chunk-FU4GAFUQ.mjs.map +0 -7
  249. package/dist/lib/browser/chunk-GX4TUNM6.mjs.map +0 -7
  250. package/dist/lib/browser/chunk-JKWMHZP6.mjs.map +0 -7
  251. package/dist/lib/browser/chunk-LVJW5EFU.mjs +0 -157
  252. package/dist/lib/browser/chunk-LVJW5EFU.mjs.map +0 -7
  253. package/dist/lib/browser/chunk-RFSO3JRG.mjs +0 -1
  254. package/dist/lib/browser/chunk-WPE6AL7I.mjs +0 -905
  255. package/dist/lib/browser/chunk-WPE6AL7I.mjs.map +0 -7
  256. package/dist/lib/browser/invoker-capability-H5PPENOC.mjs.map +0 -7
  257. package/dist/lib/node-esm/capability-WFEG6CIZ.mjs.map +0 -7
  258. package/dist/lib/node-esm/chunk-4A3ZCMI3.mjs +0 -158
  259. package/dist/lib/node-esm/chunk-4A3ZCMI3.mjs.map +0 -7
  260. package/dist/lib/node-esm/chunk-BCEOLX47.mjs.map +0 -7
  261. package/dist/lib/node-esm/chunk-G3RTFSNG.mjs.map +0 -7
  262. package/dist/lib/node-esm/chunk-LQKOTNJW.mjs +0 -906
  263. package/dist/lib/node-esm/chunk-LQKOTNJW.mjs.map +0 -7
  264. package/dist/lib/node-esm/chunk-ULUEXB7Q.mjs.map +0 -7
  265. package/dist/lib/node-esm/chunk-URWHJQT2.mjs.map +0 -7
  266. package/dist/lib/node-esm/invoker-capability-S3ZA527J.mjs.map +0 -7
  267. package/dist/types/src/vite-plugin/composer-plugin.d.ts +0 -18
  268. package/dist/types/src/vite-plugin/composer-plugin.d.ts.map +0 -1
  269. package/dist/types/src/vite-plugin/import-map-plugin.d.ts +0 -16
  270. package/dist/types/src/vite-plugin/import-map-plugin.d.ts.map +0 -1
  271. package/src/vite-plugin/composer-plugin.ts +0 -128
  272. package/src/vite-plugin/import-map-plugin.ts +0 -314
  273. /package/dist/lib/browser/{capability-BBBBAPDI.mjs.map → capability-Q5XRXRD2.mjs.map} +0 -0
  274. /package/dist/lib/browser/{chunk-I34GF4NG.mjs.map → chunk-45CHLTBV.mjs.map} +0 -0
  275. /package/dist/lib/browser/{chunk-RFSO3JRG.mjs.map → chunk-G7SDBRKH.mjs.map} +0 -0
  276. /package/dist/lib/node-esm/{capability-AWBEMRYR.mjs.map → capability-EW5GJCI6.mjs.map} +0 -0
  277. /package/dist/lib/node-esm/{chunk-WZCSOX5Q.mjs.map → chunk-6XW6LET6.mjs.map} +0 -0
  278. /package/dist/lib/node-esm/{chunk-EL3R25OQ.mjs.map → chunk-OZ7DZA5Z.mjs.map} +0 -0
@@ -7,6 +7,7 @@ import * as Effect from 'effect/Effect';
7
7
  import * as Option from 'effect/Option';
8
8
  import * as Pipeable from 'effect/Pipeable';
9
9
 
10
+ import { BaseError } from '@dxos/errors';
10
11
  import { invariant } from '@dxos/invariant';
11
12
 
12
13
  import type * as ActivationEvent from './activation-event';
@@ -89,16 +90,33 @@ export interface PluginModule {
89
90
  activatesOn: ActivationEvent.Events;
90
91
 
91
92
  /**
92
- * Events which the plugin depends on being activated.
93
- * Plugin is marked as needing reset a plugin activated by a dependent event is removed.
94
- * Events are automatically activated before activation of the plugin.
93
+ * Events that this module fires *before* its own activation runs.
94
+ *
95
+ * When this module is asked to activate (via {@link activatesOn}), the
96
+ * plugin manager first activates every event listed here, ensuring any
97
+ * other modules that contribute to those events have completed before
98
+ * this module's {@link activate} body executes. These events are fired
99
+ * by the framework on this module's behalf — the module does not need
100
+ * to wait for some other code to fire them.
101
+ *
102
+ * The module is marked as needing reset if a module activated by one
103
+ * of these events is later removed.
104
+ *
105
+ * Read as: "this module fires these events before [its] activation".
95
106
  */
96
- activatesBefore?: ActivationEvent.ActivationEvent[];
107
+ firesBeforeActivation?: ActivationEvent.ActivationEvent[];
97
108
 
98
109
  /**
99
- * Events which this plugin triggers upon activation.
110
+ * Events that this module fires *after* its own activation completes.
111
+ *
112
+ * Once this module's {@link activate} body has finished executing, the
113
+ * plugin manager activates every event listed here, causing any modules
114
+ * listening on those events to run. These events are fired by the
115
+ * framework on this module's behalf as part of this module's lifecycle.
116
+ *
117
+ * Read as: "this module fires these events after [its] activation".
100
118
  */
101
- activatesAfter?: ActivationEvent.ActivationEvent[];
119
+ firesAfterActivation?: ActivationEvent.ActivationEvent[];
102
120
 
103
121
  /**
104
122
  * Called when the module is activated.
@@ -116,15 +134,15 @@ class PluginModuleImpl implements PluginModule {
116
134
  readonly [PluginModuleTypeId]: PluginModuleTypeId = PluginModuleTypeId;
117
135
  readonly id: PluginModule['id'];
118
136
  readonly activatesOn: PluginModule['activatesOn'];
119
- readonly activatesBefore?: PluginModule['activatesBefore'];
120
- readonly activatesAfter?: PluginModule['activatesAfter'];
137
+ readonly firesBeforeActivation?: PluginModule['firesBeforeActivation'];
138
+ readonly firesAfterActivation?: PluginModule['firesAfterActivation'];
121
139
  readonly activate: PluginModule['activate'];
122
140
 
123
141
  constructor(options: Omit<PluginModule, typeof PluginModuleTypeId>) {
124
142
  this.id = options.id;
125
143
  this.activatesOn = options.activatesOn;
126
- this.activatesBefore = options.activatesBefore;
127
- this.activatesAfter = options.activatesAfter;
144
+ this.firesBeforeActivation = options.firesBeforeActivation;
145
+ this.firesAfterActivation = options.firesAfterActivation;
128
146
  this.activate = options.activate;
129
147
  }
130
148
  }
@@ -149,6 +167,11 @@ export type Meta = {
149
167
  */
150
168
  description?: string;
151
169
 
170
+ /**
171
+ * Name of the author or organization that created the plugin.
172
+ */
173
+ author?: string;
174
+
152
175
  /**
153
176
  * URL of home page.
154
177
  */
@@ -331,3 +354,105 @@ export function make<T>(builder: PluginBuilder<T>): PluginFactory<T> {
331
354
 
332
355
  return Object.assign(factory, { meta });
333
356
  }
357
+
358
+ //
359
+ // Lazy plugin loading
360
+ //
361
+
362
+ /**
363
+ * Symbol used to tag lazy plugin stubs with their loader closure.
364
+ * Hidden from enumeration so plugin manager iteration / serialization paths
365
+ * don't trip over it.
366
+ */
367
+ const LazyTag: unique symbol = Symbol.for('@dxos/app-framework/Plugin/Lazy');
368
+
369
+ /**
370
+ * Async loader for a lazy plugin's real implementation.
371
+ * The default export of the loaded module must be a `PluginFactory<T>` —
372
+ * i.e. the same shape `Plugin.make` produces.
373
+ */
374
+ export type LazyLoader<T = void> = () => Promise<{ default: PluginFactory<T> }>;
375
+
376
+ /** Internal: payload carried on a lazy stub. */
377
+ type LazyPayload = { loader: LazyLoader<any>; options: unknown };
378
+
379
+ /**
380
+ * Defines a lazy plugin whose body is loaded on first enable.
381
+ *
382
+ * The returned factory produces a stub `Plugin` that exposes `meta`
383
+ * synchronously (so callers can read `Plugin.meta.id` for free) but defers
384
+ * loading the real plugin's modules until the manager calls
385
+ * `Plugin.resolveLazy`. This lets the plugin's main entry point ship as a
386
+ * tiny meta-only chunk — the heavy capabilities, schema, React surfaces,
387
+ * etc. live behind the dynamic `import()` and become a separate Rollup
388
+ * chunk that is only fetched when the plugin is enabled.
389
+ *
390
+ * @example
391
+ * ```ts
392
+ * // plugin-markdown/src/index.ts
393
+ * import { Plugin } from '@dxos/app-framework';
394
+ * import { meta } from './meta';
395
+ *
396
+ * export const MarkdownPlugin = Plugin.lazy(meta, () => import('./MarkdownPlugin'));
397
+ *
398
+ * // plugin-markdown/src/MarkdownPlugin.tsx
399
+ * export const MarkdownPlugin = Plugin.define(meta).pipe(...heavy modules..., Plugin.make);
400
+ * export default MarkdownPlugin;
401
+ * ```
402
+ */
403
+ export const lazy = <T = void>(meta: Meta, loader: LazyLoader<T>): PluginFactory<T> => {
404
+ const factory = (options: T): Plugin => {
405
+ const stub = new PluginImpl(meta, []);
406
+ Object.defineProperty(stub, LazyTag, {
407
+ value: { loader, options } satisfies LazyPayload,
408
+ enumerable: false,
409
+ });
410
+ return stub;
411
+ };
412
+ return Object.assign(factory, { meta });
413
+ };
414
+
415
+ /**
416
+ * Type guard for lazy plugin stubs produced by {@link lazy}.
417
+ */
418
+ export const isLazy = (plugin: Plugin): boolean => LazyTag in plugin;
419
+
420
+ /**
421
+ * Tagged error for failures during lazy plugin resolution. `context.id` is
422
+ * the lazy plugin's `meta.id`; `context.reason` discriminates the failure
423
+ * mode (`'load-failed' | 'missing-default' | 'invalid-plugin' |
424
+ * 'meta-mismatch'`) so callers can route on it.
425
+ */
426
+ export class LazyPluginError extends BaseError.extend('LazyPluginError', 'Failed to resolve lazy plugin') {}
427
+
428
+ /**
429
+ * Resolves a lazy plugin stub to its real plugin.
430
+ * Returns the plugin unchanged if it is not lazy. Failures surface as
431
+ * {@link LazyPluginError} with `context.reason` indicating the failure mode
432
+ * and (for loader failures) `cause` set to the original error.
433
+ */
434
+ export const resolveLazy = (plugin: Plugin): Effect.Effect<Plugin, LazyPluginError> =>
435
+ Effect.gen(function* () {
436
+ if (!isLazy(plugin)) {
437
+ return plugin;
438
+ }
439
+ const id = plugin.meta.id;
440
+ const { loader, options } = (plugin as unknown as { [LazyTag]: LazyPayload })[LazyTag];
441
+ const mod = yield* Effect.tryPromise({
442
+ try: loader,
443
+ catch: (error) => new LazyPluginError({ context: { id, reason: 'load-failed' }, cause: error }),
444
+ });
445
+ if (!mod || typeof mod.default !== 'function') {
446
+ return yield* Effect.fail(new LazyPluginError({ context: { id, reason: 'missing-default' } }));
447
+ }
448
+ const result = mod.default(options);
449
+ if (!isPlugin(result)) {
450
+ return yield* Effect.fail(new LazyPluginError({ context: { id, reason: 'invalid-plugin' } }));
451
+ }
452
+ if (result.meta.id !== id) {
453
+ return yield* Effect.fail(
454
+ new LazyPluginError({ context: { id, reason: 'meta-mismatch', returnedId: result.meta.id } }),
455
+ );
456
+ }
457
+ return result;
458
+ });
@@ -5,12 +5,83 @@
5
5
  import { assert, describe, it } from '@effect/vitest';
6
6
  import * as Effect from 'effect/Effect';
7
7
 
8
+ import { runAndForwardErrors } from '@dxos/effect';
9
+
8
10
  import * as Plugin from './plugin';
11
+ import * as PluginAssetCache from './plugin-asset-cache';
9
12
  import * as UrlLoader from './url-loader';
10
13
 
11
14
  const testMeta = { id: 'org.dxos.plugin.test', name: 'Test' };
12
15
 
16
+ const memoryStorage = (initial: string | null = null): UrlLoader.Storage => {
17
+ let value = initial;
18
+ return {
19
+ get: () => value,
20
+ set: (_key, next) => {
21
+ value = next;
22
+ },
23
+ };
24
+ };
25
+
26
+ type CacheCall = { method: 'cache' | 'evict' | 'resolve'; pluginId: string; urls?: readonly string[]; url?: string };
27
+
28
+ const recordingCache = (): { cache: PluginAssetCache.Cache; calls: CacheCall[] } => {
29
+ const calls: CacheCall[] = [];
30
+ return {
31
+ calls,
32
+ cache: {
33
+ cache: (pluginId, urls) =>
34
+ Effect.sync(() => {
35
+ calls.push({ method: 'cache', pluginId, urls });
36
+ }),
37
+ evict: (pluginId) =>
38
+ Effect.sync(() => {
39
+ calls.push({ method: 'evict', pluginId });
40
+ }),
41
+ resolve: (pluginId, url) =>
42
+ Effect.sync(() => {
43
+ calls.push({ method: 'resolve', pluginId, url });
44
+ return url;
45
+ }),
46
+ list: () => Effect.succeed([] as readonly string[]),
47
+ },
48
+ };
49
+ };
50
+
13
51
  describe('UrlLoader', () => {
52
+ describe('isLocalUrl', () => {
53
+ it('matches localhost, 127.0.0.1, and ::1', ({ expect }) => {
54
+ expect(UrlLoader.isLocalUrl('http://localhost:5173/plugin.mjs')).toBe(true);
55
+ expect(UrlLoader.isLocalUrl('https://LOCALHOST/plugin.mjs')).toBe(true);
56
+ expect(UrlLoader.isLocalUrl('http://127.0.0.1:8080/plugin.mjs')).toBe(true);
57
+ expect(UrlLoader.isLocalUrl('http://[::1]:8080/plugin.mjs')).toBe(true);
58
+ });
59
+
60
+ it('rejects public and malformed URLs', ({ expect }) => {
61
+ expect(UrlLoader.isLocalUrl('https://example.com/plugin.mjs')).toBe(false);
62
+ expect(UrlLoader.isLocalUrl('https://192.168.1.10/plugin.mjs')).toBe(false);
63
+ expect(UrlLoader.isLocalUrl('not a url')).toBe(false);
64
+ expect(UrlLoader.isLocalUrl('')).toBe(false);
65
+ });
66
+ });
67
+
68
+ describe('getRemoteEntries', () => {
69
+ it('returns persisted entries from storage', ({ expect }) => {
70
+ const storage: UrlLoader.Storage = {
71
+ get: () => '[{"id":"p1","url":"http://localhost:5173/p.mjs"}]',
72
+ set: () => {},
73
+ };
74
+ expect(UrlLoader.getRemoteEntries({ storage })).toEqual([{ id: 'p1', url: 'http://localhost:5173/p.mjs' }]);
75
+ });
76
+
77
+ it('returns an empty array when storage is empty or malformed', ({ expect }) => {
78
+ const empty: UrlLoader.Storage = { get: () => null, set: () => {} };
79
+ const malformed: UrlLoader.Storage = { get: () => '{not json', set: () => {} };
80
+ expect(UrlLoader.getRemoteEntries({ storage: empty })).toEqual([]);
81
+ expect(UrlLoader.getRemoteEntries({ storage: malformed })).toEqual([]);
82
+ });
83
+ });
84
+
14
85
  describe('make', () => {
15
86
  it.effect('resolves built-in plugins by meta.id', () =>
16
87
  Effect.gen(function* () {
@@ -36,7 +107,7 @@ describe('UrlLoader', () => {
36
107
  get: () => null,
37
108
  set: () => {},
38
109
  };
39
- const result = await UrlLoader.preload({ storage });
110
+ const result = await runAndForwardErrors(UrlLoader.preload({ storage }));
40
111
  expect(result).toEqual([]);
41
112
  });
42
113
 
@@ -45,7 +116,7 @@ describe('UrlLoader', () => {
45
116
  get: () => '{{invalid json',
46
117
  set: () => {},
47
118
  };
48
- const result = await UrlLoader.preload({ storage });
119
+ const result = await runAndForwardErrors(UrlLoader.preload({ storage }));
49
120
  expect(result).toEqual([]);
50
121
  });
51
122
 
@@ -54,7 +125,7 @@ describe('UrlLoader', () => {
54
125
  get: () => 'null',
55
126
  set: () => {},
56
127
  };
57
- const result = await UrlLoader.preload({ storage });
128
+ const result = await runAndForwardErrors(UrlLoader.preload({ storage }));
58
129
  expect(result).toEqual([]);
59
130
  });
60
131
 
@@ -63,7 +134,7 @@ describe('UrlLoader', () => {
63
134
  get: () => '{}',
64
135
  set: () => {},
65
136
  };
66
- const result = await UrlLoader.preload({ storage });
137
+ const result = await runAndForwardErrors(UrlLoader.preload({ storage }));
67
138
  expect(result).toEqual([]);
68
139
  });
69
140
 
@@ -72,8 +143,36 @@ describe('UrlLoader', () => {
72
143
  get: () => '[{"title":"no url"}]',
73
144
  set: () => {},
74
145
  };
75
- const result = await UrlLoader.preload({ storage });
146
+ const result = await runAndForwardErrors(UrlLoader.preload({ storage }));
76
147
  expect(result).toEqual([]);
77
148
  });
78
149
  });
150
+
151
+ describe('uninstall', () => {
152
+ it('removes the persisted entry and evicts cached assets', async ({ expect }) => {
153
+ const storage = memoryStorage('[{"id":"p1","url":"https://x/p1.json"},{"id":"p2","url":"https://x/p2.json"}]');
154
+ const { cache, calls } = recordingCache();
155
+ await runAndForwardErrors(UrlLoader.uninstall('p1', { storage, cache }));
156
+ expect(UrlLoader.getRemoteEntries({ storage })).toEqual([{ id: 'p2', url: 'https://x/p2.json' }]);
157
+ expect(calls).toEqual([{ method: 'evict', pluginId: 'p1' }]);
158
+ });
159
+
160
+ it('still removes entry when cache eviction fails', async ({ expect }) => {
161
+ const storage = memoryStorage('[{"id":"p1","url":"https://x/p1.json"}]');
162
+ const cache: PluginAssetCache.Cache = {
163
+ cache: () => Effect.void,
164
+ evict: () =>
165
+ Effect.fail(
166
+ new PluginAssetCache.PluginAssetCacheError({
167
+ context: { operation: 'evict', pluginId: 'p1' },
168
+ cause: 'boom',
169
+ }),
170
+ ),
171
+ resolve: (_id, url) => Effect.succeed(url),
172
+ list: () => Effect.succeed([] as readonly string[]),
173
+ };
174
+ await runAndForwardErrors(UrlLoader.uninstall('p1', { storage, cache }));
175
+ expect(UrlLoader.getRemoteEntries({ storage })).toEqual([]);
176
+ });
177
+ });
79
178
  });
@@ -3,13 +3,25 @@
3
3
  //
4
4
 
5
5
  import * as Effect from 'effect/Effect';
6
+ import * as Option from 'effect/Option';
6
7
 
8
+ import { BaseError } from '@dxos/errors';
7
9
  import { log } from '@dxos/log';
8
10
 
9
11
  import * as Plugin from './plugin';
12
+ import * as PluginAssetCache from './plugin-asset-cache';
13
+ import * as PluginManifest from './plugin-manifest';
10
14
 
11
15
  const DEFAULT_KEY = 'org.dxos.composer.remote-plugins';
12
16
 
17
+ /**
18
+ * Tagged error for any failure during remote plugin loading. Construction sites
19
+ * set `context.locator` and `context.reason` (one of `'invalid-locator' |
20
+ * 'manifest-error' | 'cache-error' | 'import-failed' | 'meta-missing' |
21
+ * 'meta-mismatch' | 'duplicate-id'`) so handlers can route on the failure mode.
22
+ */
23
+ export class RemotePluginLoadError extends BaseError.extend('RemotePluginLoadError', 'Failed to load remote plugin') {}
24
+
13
25
  /**
14
26
  * Abstraction over key-value storage (defaults to localStorage).
15
27
  */
@@ -24,9 +36,33 @@ export type Storage = {
24
36
  export type Options = {
25
37
  storage?: Storage;
26
38
  key?: string;
39
+ /**
40
+ * Per-platform offline asset cache. Defaults to a no-op cache (no offline support).
41
+ */
42
+ cache?: PluginAssetCache.Cache;
27
43
  };
28
44
 
29
- type RemotePluginEntry = { id: string; url: string };
45
+ /**
46
+ * Options for the preload entry point. Adds an optional progress hook so hosts
47
+ * can drive a boot-loader counter (`Loading plugins (3/12)…`) as remote plugin
48
+ * manifests resolve. Resolution order is *not* deterministic — each entry races
49
+ * its own network fetch — so callers should treat `loaded` as a count, not an
50
+ * index. Failures are still swallowed; `loaded` advances on both success and
51
+ * recoverable failure so the counter always reaches `total`.
52
+ */
53
+ export type PreloadOptions = Options & {
54
+ onPluginLoaded?: (loaded: number, total: number) => void;
55
+ };
56
+
57
+ /**
58
+ * Persisted record of a remote plugin that has been loaded previously.
59
+ *
60
+ * `url` is the URL of the plugin manifest (`plugin.json`), not the entry module.
61
+ */
62
+ export type RemotePluginEntry = {
63
+ id: string;
64
+ url: string;
65
+ };
30
66
 
31
67
  const defaultStorage = (): Storage => ({
32
68
  get: (key) => localStorage.getItem(key),
@@ -58,6 +94,15 @@ const persistRemotePlugin = (storage: Storage, key: string, entry: RemotePluginE
58
94
  }
59
95
  };
60
96
 
97
+ const removePersistedRemotePlugin = (storage: Storage, key: string, pluginId: string): void => {
98
+ try {
99
+ const entries = getPersistedRemotePlugins(storage, key).filter((existing) => existing.id !== pluginId);
100
+ storage.set(key, JSON.stringify(entries));
101
+ } catch (error) {
102
+ log.warn('failed to remove remote plugin entry', { pluginId, error });
103
+ }
104
+ };
105
+
61
106
  const isUrl = (locator: string): boolean => {
62
107
  try {
63
108
  const url = new URL(locator);
@@ -67,6 +112,30 @@ const isUrl = (locator: string): boolean => {
67
112
  }
68
113
  };
69
114
 
115
+ /**
116
+ * Returns true when the URL's hostname is the local host (localhost, 127.0.0.1, or ::1).
117
+ */
118
+ export const isLocalUrl = (locator: string): boolean => {
119
+ try {
120
+ const hostname = new URL(locator).hostname.toLowerCase();
121
+ // WHATWG URL returns '::1' without brackets in Node; some browsers return '[::1]'.
122
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '[::1]';
123
+ } catch {
124
+ return false;
125
+ }
126
+ };
127
+
128
+ /**
129
+ * Returns the list of remote plugin entries previously persisted by {@link make}.
130
+ * Useful for UI code that needs to know which loaded plugins were installed from a URL
131
+ * (e.g. to surface a tag on remote or localhost-hosted plugins).
132
+ */
133
+ export const getRemoteEntries = (options: Options = {}): readonly RemotePluginEntry[] => {
134
+ const storage = options.storage ?? defaultStorage();
135
+ const key = options.key ?? DEFAULT_KEY;
136
+ return getPersistedRemotePlugins(storage, key);
137
+ };
138
+
70
139
  const normalizePluginExport = (mod: Record<string, unknown>): Plugin.Plugin => {
71
140
  const exported = mod.default;
72
141
  if (Plugin.isPlugin(exported)) {
@@ -81,68 +150,188 @@ const normalizePluginExport = (mod: Record<string, unknown>): Plugin.Plugin => {
81
150
  throw new Error('Remote module default export is not a Plugin or a zero-arg plugin factory.');
82
151
  };
83
152
 
84
- const loadRemotePlugin = async (url: string): Promise<Plugin.Plugin> => {
85
- log.info('loading remote plugin', { url });
86
- const mod = await import(/* @vite-ignore */ url);
87
- const plugin = normalizePluginExport(mod);
88
- if (!plugin.meta.id || !plugin.meta.name) {
89
- throw new Error(`Remote plugin at ${url} is missing required meta.id or meta.name.`);
90
- }
91
- return plugin;
92
- };
153
+ /**
154
+ * Loads stylesheets declared in the manifest by appending `<link rel="stylesheet">` elements to the host document.
155
+ * Each link is tagged with `data-dxos-plugin-id` so `uninstall` can clean them up.
156
+ */
157
+ const loadStylesheets = (
158
+ manifest: PluginManifest.ResolvedManifest,
159
+ cache: PluginAssetCache.Cache,
160
+ ): Effect.Effect<void, PluginAssetCache.PluginAssetCacheError> =>
161
+ Effect.gen(function* () {
162
+ if (typeof document === 'undefined') {
163
+ return;
164
+ }
165
+ const cssUrls = manifest.assetUrls.filter((url) => url.endsWith('.css'));
166
+ for (const url of cssUrls) {
167
+ const resolved = yield* cache.resolve(manifest.id, url);
168
+ if (document.querySelector(`link[data-dxos-plugin-id="${manifest.id}"][href="${resolved}"]`)) {
169
+ continue;
170
+ }
171
+ const link = document.createElement('link');
172
+ link.rel = 'stylesheet';
173
+ link.href = resolved;
174
+ link.dataset.dxosPluginId = manifest.id;
175
+ document.head.appendChild(link);
176
+ }
177
+ });
178
+
179
+ const loadFromManifest = (
180
+ manifestUrl: string,
181
+ cache: PluginAssetCache.Cache,
182
+ ): Effect.Effect<{ plugin: Plugin.Plugin; manifest: PluginManifest.ResolvedManifest }, RemotePluginLoadError> =>
183
+ Effect.gen(function* () {
184
+ log.info('loading remote plugin', { manifestUrl });
185
+ const manifest = yield* PluginManifest.fetchManifest(manifestUrl).pipe(
186
+ Effect.mapError(
187
+ (cause) => new RemotePluginLoadError({ context: { locator: manifestUrl, reason: 'manifest-error' }, cause }),
188
+ ),
189
+ );
190
+ // Cache the manifest URL alongside its declared assets. Without it, `preload` on a
191
+ // subsequent reload would fetch the manifest from the network — and fail when the
192
+ // plugin's host is offline, dropping the plugin from the runtime.
193
+ const cachedUrls =
194
+ manifest.assetUrls.indexOf(manifestUrl) === -1 ? [manifestUrl, ...manifest.assetUrls] : manifest.assetUrls;
195
+ const wrapCacheError = Effect.mapError(
196
+ (cause: PluginAssetCache.PluginAssetCacheError) =>
197
+ new RemotePluginLoadError({ context: { locator: manifestUrl, reason: 'cache-error' }, cause }),
198
+ );
199
+ yield* cache.cache(manifest.id, cachedUrls).pipe(wrapCacheError);
200
+ const entryUrl = yield* cache.resolve(manifest.id, manifest.entryUrl).pipe(wrapCacheError);
201
+ const mod = yield* Effect.tryPromise({
202
+ try: () => import(/* @vite-ignore */ entryUrl),
203
+ catch: (cause) =>
204
+ new RemotePluginLoadError({ context: { locator: manifestUrl, reason: 'import-failed' }, cause }),
205
+ });
206
+ const plugin = normalizePluginExport(mod);
207
+ if (!plugin.meta.id || !plugin.meta.name) {
208
+ return yield* Effect.fail(
209
+ new RemotePluginLoadError({ context: { locator: manifestUrl, reason: 'meta-missing' } }),
210
+ );
211
+ }
212
+ if (plugin.meta.id !== manifest.id) {
213
+ return yield* Effect.fail(
214
+ new RemotePluginLoadError({
215
+ context: {
216
+ locator: manifestUrl,
217
+ reason: 'meta-mismatch',
218
+ metaId: plugin.meta.id,
219
+ manifestId: manifest.id,
220
+ },
221
+ }),
222
+ );
223
+ }
224
+ // Append stylesheets only after the entry imports cleanly and meta validation
225
+ // passes. If we did this earlier and the import or meta checks failed, the
226
+ // `<link>` tags would leak into the host DOM with no plugin to own their
227
+ // teardown — `uninstall` only runs for plugins the manager actually accepted.
228
+ yield* loadStylesheets(manifest, cache).pipe(wrapCacheError);
229
+ return { plugin, manifest };
230
+ });
93
231
 
94
232
  /**
95
- * Preloads previously persisted remote plugins from storage.
233
+ * Preloads previously persisted remote plugins from storage. Per-entry failures
234
+ * are logged and swallowed — the returned effect always succeeds with whichever
235
+ * plugins loaded cleanly, so a single bad entry can't block the host's startup.
96
236
  */
97
- export const preload = async (options: Options = {}): Promise<Plugin.Plugin[]> => {
98
- const storage = options.storage ?? defaultStorage();
99
- const key = options.key ?? DEFAULT_KEY;
237
+ export const preload = (options: PreloadOptions = {}): Effect.Effect<Plugin.Plugin[], never> =>
238
+ Effect.gen(function* () {
239
+ const storage = options.storage ?? defaultStorage();
240
+ const key = options.key ?? DEFAULT_KEY;
241
+ const cache = options.cache ?? PluginAssetCache.noop();
242
+ const onPluginLoaded = options.onPluginLoaded;
100
243
 
101
- const entries = getPersistedRemotePlugins(storage, key);
102
- if (entries.length === 0) {
103
- return [];
104
- }
105
- log.info('preloading remote plugins', { count: entries.length });
106
- const results = await Promise.allSettled(entries.map((entry) => loadRemotePlugin(entry.url)));
107
- const plugins: Plugin.Plugin[] = [];
108
- for (let index = 0; index < results.length; index++) {
109
- const result = results[index];
110
- if (result.status === 'fulfilled') {
111
- plugins.push(result.value);
112
- } else {
113
- log.warn('failed to preload remote plugin', { entry: entries[index], error: result.reason });
244
+ const entries = getPersistedRemotePlugins(storage, key);
245
+ if (entries.length === 0) {
246
+ return [];
114
247
  }
115
- }
116
- return plugins;
117
- };
248
+ log.info('preloading remote plugins', { count: entries.length });
249
+ const total = entries.length;
250
+ let loaded = 0;
251
+ const results = yield* Effect.all(
252
+ entries.map((entry) =>
253
+ loadFromManifest(entry.url, cache).pipe(
254
+ Effect.tapError((error) => Effect.sync(() => log.warn('failed to preload remote plugin', { entry, error }))),
255
+ Effect.option,
256
+ // Tick the progress hook on both success and recoverable failure so
257
+ // the counter monotonically reaches `total`. The host-supplied
258
+ // callback is best-effort: any synchronous throw flows through
259
+ // `Effect.try` and gets logged + ignored so a buggy hook can't
260
+ // derail the preload.
261
+ Effect.tap(() =>
262
+ Effect.sync(() => {
263
+ loaded += 1;
264
+ }).pipe(
265
+ Effect.andThen(Effect.try(() => onPluginLoaded?.(loaded, total))),
266
+ Effect.tapError((error) => Effect.sync(() => log.warn('onPluginLoaded threw', { loaded, total, error }))),
267
+ Effect.ignore,
268
+ ),
269
+ ),
270
+ ),
271
+ ),
272
+ { concurrency: 'unbounded' },
273
+ );
274
+ return results.flatMap((result) =>
275
+ Option.match(result, {
276
+ onNone: () => [],
277
+ onSome: ({ plugin }) => [plugin],
278
+ }),
279
+ );
280
+ });
118
281
 
119
282
  /**
120
283
  * Creates a plugin loader that resolves built-in plugins by ID or loads remote plugins from URLs.
284
+ *
285
+ * Remote URLs must point at a plugin manifest (`plugin.json`). The loader fetches the manifest,
286
+ * eagerly persists every declared asset via the configured `PluginAssetCache`, then dynamic-imports
287
+ * the entry module.
121
288
  */
122
289
  export const make = (builtinPlugins: Plugin.Plugin[], options: Options = {}) => {
123
290
  const storage = options.storage ?? defaultStorage();
124
291
  const key = options.key ?? DEFAULT_KEY;
292
+ const cache = options.cache ?? PluginAssetCache.noop();
125
293
 
126
- return (locator: string): Effect.Effect<Plugin.Plugin, Error> =>
294
+ return (locator: string): Effect.Effect<Plugin.Plugin, RemotePluginLoadError> =>
127
295
  Effect.gen(function* () {
128
296
  const builtin = builtinPlugins.find((plugin) => plugin.meta.id === locator);
129
297
  if (builtin) {
130
298
  return builtin;
131
299
  }
132
300
  if (!isUrl(locator)) {
133
- return yield* Effect.fail(new Error(`Plugin not found and locator is not a URL: ${locator}`));
301
+ return yield* Effect.fail(new RemotePluginLoadError({ context: { locator, reason: 'invalid-locator' } }));
134
302
  }
135
- const plugin = yield* Effect.tryPromise({
136
- try: () => loadRemotePlugin(locator),
137
- catch: (error) => new Error(`Failed to load remote plugin from ${locator}: ${error}`),
138
- });
303
+ const { plugin } = yield* loadFromManifest(locator, cache);
139
304
  const duplicate = builtinPlugins.find((existing) => existing.meta.id === plugin.meta.id);
140
305
  if (duplicate) {
141
306
  return yield* Effect.fail(
142
- new Error(`Remote plugin ${plugin.meta.id} conflicts with built-in plugin of the same id.`),
307
+ new RemotePluginLoadError({ context: { locator, reason: 'duplicate-id', id: plugin.meta.id } }),
143
308
  );
144
309
  }
145
310
  persistRemotePlugin(storage, key, { id: plugin.meta.id, url: locator });
146
311
  return plugin;
147
312
  });
148
313
  };
314
+
315
+ /**
316
+ * Removes a previously installed remote plugin: drops the persisted entry, evicts cached
317
+ * assets, and removes any stylesheet `<link>` tags that {@link loadFromManifest} appended.
318
+ *
319
+ * Cache eviction failures are logged and swallowed — the persisted entry has already been
320
+ * dropped so the user-visible state is consistent regardless of whether the platform
321
+ * cache cooperated.
322
+ */
323
+ export const uninstall = (pluginId: string, options: Options = {}): Effect.Effect<void, never> =>
324
+ Effect.gen(function* () {
325
+ const storage = options.storage ?? defaultStorage();
326
+ const key = options.key ?? DEFAULT_KEY;
327
+ const cache = options.cache ?? PluginAssetCache.noop();
328
+
329
+ removePersistedRemotePlugin(storage, key, pluginId);
330
+ if (typeof document !== 'undefined') {
331
+ document.querySelectorAll(`link[data-dxos-plugin-id="${pluginId}"]`).forEach((node) => node.remove());
332
+ }
333
+ yield* cache.evict(pluginId).pipe(
334
+ Effect.tapError((error) => Effect.sync(() => log.warn('failed to evict plugin assets', { pluginId, error }))),
335
+ Effect.ignore,
336
+ );
337
+ });
@@ -12,8 +12,8 @@ const HistoryCapabilities = Capability.lazy('HistoryCapabilities', () => import(
12
12
  export const OperationPlugin = Plugin.define(meta).pipe(
13
13
  Plugin.addModule({
14
14
  activatesOn: ActivationEvents.ManagedRuntimeReady,
15
- activatesBefore: [ActivationEvents.SetupOperationHandler],
16
- activatesAfter: [ActivationEvents.OperationInvokerReady],
15
+ firesBeforeActivation: [ActivationEvents.SetupOperationHandler],
16
+ firesAfterActivation: [ActivationEvents.OperationInvokerReady],
17
17
  activate: OperationInvoker,
18
18
  }),
19
19
  Plugin.addModule({