@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.
- package/dist/lib/browser/{capability-BBBBAPDI.mjs → capability-Q5XRXRD2.mjs} +10 -10
- package/dist/lib/browser/{capability-OP63CD5N.mjs → capability-V7LR4LQN.mjs} +11 -11
- package/dist/lib/browser/capability-V7LR4LQN.mjs.map +7 -0
- package/dist/lib/browser/{chunk-T3Y4AEKX.mjs → chunk-23D4SJUE.mjs} +3 -3
- package/dist/lib/browser/{chunk-T3Y4AEKX.mjs.map → chunk-23D4SJUE.mjs.map} +1 -1
- package/dist/lib/browser/{chunk-2CKCJ6PN.mjs → chunk-3JWJXGLK.mjs} +1 -1
- package/dist/lib/browser/{chunk-2CKCJ6PN.mjs.map → chunk-3JWJXGLK.mjs.map} +1 -1
- package/dist/lib/browser/{chunk-GX4TUNM6.mjs → chunk-3ZS2A3DN.mjs} +170 -226
- package/dist/lib/browser/chunk-3ZS2A3DN.mjs.map +7 -0
- package/dist/lib/browser/{chunk-I34GF4NG.mjs → chunk-45CHLTBV.mjs} +2 -2
- package/dist/lib/browser/chunk-5LAIGWLU.mjs +467 -0
- package/dist/lib/browser/chunk-5LAIGWLU.mjs.map +7 -0
- package/dist/lib/browser/{chunk-QSXYHXCE.mjs → chunk-66IXTIVK.mjs} +1 -1
- package/dist/lib/browser/{chunk-QSXYHXCE.mjs.map → chunk-66IXTIVK.mjs.map} +2 -2
- package/dist/lib/browser/{chunk-TGX63LTL.mjs → chunk-FJ4765WW.mjs} +1 -1
- package/dist/lib/browser/{chunk-TGX63LTL.mjs.map → chunk-FJ4765WW.mjs.map} +2 -2
- package/dist/lib/browser/chunk-G7SDBRKH.mjs +1 -0
- package/dist/lib/browser/chunk-JXCBZSBJ.mjs +372 -0
- package/dist/lib/browser/chunk-JXCBZSBJ.mjs.map +7 -0
- package/dist/lib/browser/chunk-MX5DKEJH.mjs +584 -0
- package/dist/lib/browser/chunk-MX5DKEJH.mjs.map +7 -0
- package/dist/lib/browser/{chunk-JKWMHZP6.mjs → chunk-WBHCSOBW.mjs} +2 -2
- package/dist/lib/browser/chunk-WBHCSOBW.mjs.map +7 -0
- package/dist/lib/browser/{chunk-FU4GAFUQ.mjs → chunk-Z55LVAGN.mjs} +80 -15
- package/dist/lib/browser/chunk-Z55LVAGN.mjs.map +7 -0
- package/dist/lib/browser/{chunk-F7FW2RK2.mjs → chunk-ZGJAZSNE.mjs} +7 -32
- package/dist/lib/browser/chunk-ZGJAZSNE.mjs.map +7 -0
- package/dist/lib/browser/cli/index.mjs +11 -27
- package/dist/lib/browser/cli/index.mjs.map +2 -2
- package/dist/lib/browser/common/activation-events.mjs +7 -7
- package/dist/lib/browser/common/capabilities.mjs +7 -7
- package/dist/lib/browser/core/activation-event.mjs +1 -1
- package/dist/lib/browser/core/capability.mjs +1 -1
- package/dist/lib/browser/core/plugin-manager.mjs +6 -4
- package/dist/lib/browser/core/plugin.mjs +10 -2
- package/dist/lib/browser/core/url-loader.mjs +13 -5
- package/dist/lib/browser/index.mjs +22 -18
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/{invoker-capability-H5PPENOC.mjs → invoker-capability-LNX4CGIV.mjs} +12 -11
- package/dist/lib/browser/invoker-capability-LNX4CGIV.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +144 -27
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/browser/testing/react.mjs +78 -0
- package/dist/lib/browser/testing/react.mjs.map +7 -0
- package/dist/lib/browser/ui/index.mjs +18 -14
- package/dist/lib/node-esm/{capability-AWBEMRYR.mjs → capability-EW5GJCI6.mjs} +10 -10
- package/dist/lib/node-esm/{capability-WFEG6CIZ.mjs → capability-YKBMMD53.mjs} +11 -11
- package/dist/lib/node-esm/capability-YKBMMD53.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-FKE4Z3D6.mjs → chunk-37Z53PXZ.mjs} +1 -1
- package/dist/lib/node-esm/{chunk-FKE4Z3D6.mjs.map → chunk-37Z53PXZ.mjs.map} +2 -2
- package/dist/lib/node-esm/{chunk-WZCSOX5Q.mjs → chunk-6XW6LET6.mjs} +2 -2
- package/dist/lib/node-esm/{chunk-URWHJQT2.mjs → chunk-D347W3KO.mjs} +7 -32
- package/dist/lib/node-esm/chunk-D347W3KO.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-D5PO2WXX.mjs +373 -0
- package/dist/lib/node-esm/chunk-D5PO2WXX.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-ULUEXB7Q.mjs → chunk-HTBJU5FX.mjs} +80 -15
- package/dist/lib/node-esm/chunk-HTBJU5FX.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-KM2F6GH6.mjs +468 -0
- package/dist/lib/node-esm/chunk-KM2F6GH6.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-EL3R25OQ.mjs → chunk-OZ7DZA5Z.mjs} +1 -1
- package/dist/lib/node-esm/{chunk-BCEOLX47.mjs → chunk-Q7XBFII4.mjs} +170 -226
- package/dist/lib/node-esm/chunk-Q7XBFII4.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-VKHGNEDB.mjs → chunk-SBS2YMPT.mjs} +3 -3
- package/dist/lib/node-esm/{chunk-VKHGNEDB.mjs.map → chunk-SBS2YMPT.mjs.map} +1 -1
- package/dist/lib/node-esm/{chunk-42KBWDE4.mjs → chunk-SDJ4B2LU.mjs} +1 -1
- package/dist/lib/node-esm/{chunk-42KBWDE4.mjs.map → chunk-SDJ4B2LU.mjs.map} +1 -1
- package/dist/lib/node-esm/{chunk-G3RTFSNG.mjs → chunk-WFSRZKBP.mjs} +2 -2
- package/dist/lib/node-esm/chunk-WFSRZKBP.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-WKTLE7MG.mjs +585 -0
- package/dist/lib/node-esm/chunk-WKTLE7MG.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-ZZ7CKK6W.mjs → chunk-XOCUANHO.mjs} +1 -1
- package/dist/lib/node-esm/{chunk-ZZ7CKK6W.mjs.map → chunk-XOCUANHO.mjs.map} +2 -2
- package/dist/lib/node-esm/cli/index.mjs +11 -27
- package/dist/lib/node-esm/cli/index.mjs.map +2 -2
- package/dist/lib/node-esm/common/activation-events.mjs +7 -7
- package/dist/lib/node-esm/common/capabilities.mjs +7 -7
- package/dist/lib/node-esm/core/activation-event.mjs +1 -1
- package/dist/lib/node-esm/core/capability.mjs +1 -1
- package/dist/lib/node-esm/core/plugin-manager.mjs +6 -4
- package/dist/lib/node-esm/core/plugin.mjs +10 -2
- package/dist/lib/node-esm/core/url-loader.mjs +13 -5
- package/dist/lib/node-esm/index.mjs +22 -18
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/{invoker-capability-S3ZA527J.mjs → invoker-capability-O4T5PHLA.mjs} +12 -11
- package/dist/lib/node-esm/invoker-capability-O4T5PHLA.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +144 -27
- package/dist/lib/node-esm/testing/index.mjs.map +4 -4
- package/dist/lib/node-esm/testing/react.mjs +79 -0
- package/dist/lib/node-esm/testing/react.mjs.map +7 -0
- package/dist/lib/node-esm/ui/index.mjs +18 -14
- package/dist/plugin/node-esm/index.mjs +480 -32
- package/dist/plugin/node-esm/index.mjs.map +4 -4
- package/dist/plugin/node-esm/meta.json +1 -1
- package/dist/types/src/common/capabilities.d.ts +2 -1
- package/dist/types/src/common/capabilities.d.ts.map +1 -1
- package/dist/types/src/common/operations.d.ts +1 -1
- package/dist/types/src/common/operations.d.ts.map +1 -1
- package/dist/types/src/core/activation-event.d.ts +4 -4
- package/dist/types/src/core/activation-event.d.ts.map +1 -1
- package/dist/types/src/core/capability-manager.d.ts.map +1 -1
- package/dist/types/src/core/capability.d.ts +2 -2
- package/dist/types/src/core/capability.d.ts.map +1 -1
- package/dist/types/src/core/index.d.ts +2 -0
- package/dist/types/src/core/index.d.ts.map +1 -1
- package/dist/types/src/core/plugin-asset-cache.d.ts +71 -0
- package/dist/types/src/core/plugin-asset-cache.d.ts.map +1 -0
- package/dist/types/src/core/plugin-manager.d.ts +51 -2
- package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
- package/dist/types/src/core/plugin-manifest.d.ts +76 -0
- package/dist/types/src/core/plugin-manifest.d.ts.map +1 -0
- package/dist/types/src/core/plugin-manifest.test.d.ts +2 -0
- package/dist/types/src/core/plugin-manifest.test.d.ts.map +1 -0
- package/dist/types/src/core/plugin.d.ts +107 -6
- package/dist/types/src/core/plugin.d.ts.map +1 -1
- package/dist/types/src/core/url-loader.d.ts +90 -3
- package/dist/types/src/core/url-loader.d.ts.map +1 -1
- package/dist/types/src/helpers.d.ts.map +1 -1
- package/dist/types/src/plugin-operation/history/capability.d.ts.map +1 -1
- package/dist/types/src/plugin-operation/history/errors.d.ts +6 -6
- package/dist/types/src/plugin-operation/history/errors.d.ts.map +1 -1
- package/dist/types/src/plugin-operation/history/history-tracker.d.ts +1 -1
- package/dist/types/src/plugin-operation/history/history-tracker.d.ts.map +1 -1
- package/dist/types/src/plugin-operation/history/types.d.ts +1 -1
- package/dist/types/src/plugin-operation/history/types.d.ts.map +1 -1
- package/dist/types/src/plugin-operation/history/undo-mapping.d.ts +1 -1
- package/dist/types/src/plugin-operation/history/undo-mapping.d.ts.map +1 -1
- package/dist/types/src/plugin-operation/history/undo-registry.d.ts +1 -1
- package/dist/types/src/plugin-operation/history/undo-registry.d.ts.map +1 -1
- package/dist/types/src/plugin-operation/invoker-capability.d.ts +1 -1
- package/dist/types/src/plugin-operation/invoker-capability.d.ts.map +1 -1
- package/dist/types/src/plugin-operation/testing.d.ts +2 -1
- package/dist/types/src/plugin-operation/testing.d.ts.map +1 -1
- package/dist/types/src/plugin-runtime/capability.d.ts +1 -1
- package/dist/types/src/plugin-runtime/capability.d.ts.map +1 -1
- package/dist/types/src/testing/harness.d.ts +67 -0
- package/dist/types/src/testing/harness.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +1 -0
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/react.d.ts +27 -0
- package/dist/types/src/testing/react.d.ts.map +1 -0
- package/dist/types/src/testing/react.test.d.ts +2 -0
- package/dist/types/src/testing/react.test.d.ts.map +1 -0
- package/dist/types/src/testing/service.d.ts.map +1 -1
- package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
- package/dist/types/src/testing/withPluginManager.stories.d.ts.map +1 -1
- package/dist/types/src/ui/components/App/App.d.ts.map +1 -1
- package/dist/types/src/ui/components/App/App.stories.d.ts.map +1 -1
- package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts +64 -0
- package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts.map +1 -0
- package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts +19 -0
- package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts.map +1 -0
- package/dist/types/src/ui/components/Placeholder/index.d.ts +2 -0
- package/dist/types/src/ui/components/Placeholder/index.d.ts.map +1 -0
- package/dist/types/src/ui/components/PluginManager/PluginManagerContext.stories.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts +16 -4
- package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/index.d.ts +16 -6
- package/dist/types/src/ui/components/Surface/index.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/types.d.ts +110 -9
- package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/types.test.d.ts +2 -0
- package/dist/types/src/ui/components/Surface/types.test.d.ts.map +1 -0
- package/dist/types/src/ui/components/index.d.ts +1 -0
- package/dist/types/src/ui/components/index.d.ts.map +1 -1
- package/dist/types/src/ui/hooks/useApp.d.ts +29 -3
- package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
- package/dist/types/src/ui/hooks/useCapabilities.d.ts.map +1 -1
- package/dist/types/src/ui/hooks/useLoading.d.ts.map +1 -1
- package/dist/types/src/ui/hooks/useSettingsState.d.ts.map +1 -1
- package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts +34 -0
- package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts.map +1 -0
- package/dist/types/src/vite-plugin/boot-loader/index.d.ts +52 -0
- package/dist/types/src/vite-plugin/boot-loader/index.d.ts.map +1 -0
- package/dist/types/src/vite-plugin/composer/index.d.ts +34 -0
- package/dist/types/src/vite-plugin/composer/index.d.ts.map +1 -0
- package/dist/types/src/vite-plugin/import-map/index.d.ts +28 -0
- package/dist/types/src/vite-plugin/import-map/index.d.ts.map +1 -0
- package/dist/types/src/vite-plugin/index.d.ts +4 -2
- package/dist/types/src/vite-plugin/index.d.ts.map +1 -1
- package/dist/types/src/vite-plugin/manifest.d.ts +37 -0
- package/dist/types/src/vite-plugin/manifest.d.ts.map +1 -0
- package/dist/types/src/vite-plugin/manifest.test.d.ts +2 -0
- package/dist/types/src/vite-plugin/manifest.test.d.ts.map +1 -0
- package/dist/types/src/vite-plugin/packages.d.ts +10 -4
- package/dist/types/src/vite-plugin/packages.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/moon.yml +1 -0
- package/package.json +33 -59
- package/src/common/capabilities.ts +2 -1
- package/src/common/operations.ts +1 -1
- package/src/core/capability.ts +1 -1
- package/src/core/index.ts +2 -0
- package/src/core/plugin-asset-cache.ts +60 -0
- package/src/core/plugin-manager.test.ts +246 -5
- package/src/core/plugin-manager.ts +167 -25
- package/src/core/plugin-manifest.test.ts +48 -0
- package/src/core/plugin-manifest.ts +102 -0
- package/src/core/plugin.ts +135 -10
- package/src/core/url-loader.test.ts +104 -5
- package/src/core/url-loader.ts +226 -37
- package/src/plugin-operation/OperationPlugin.ts +2 -2
- package/src/plugin-operation/history/capability.ts +1 -1
- package/src/plugin-operation/history/history-tracker.test.ts +2 -1
- package/src/plugin-operation/history/history-tracker.ts +1 -1
- package/src/plugin-operation/history/types.ts +1 -1
- package/src/plugin-operation/history/undo-mapping.ts +1 -1
- package/src/plugin-operation/history/undo-registry.ts +1 -1
- package/src/plugin-operation/invoker-capability.ts +2 -1
- package/src/plugin-operation/testing.ts +2 -1
- package/src/plugin-runtime/RuntimePlugin.ts +2 -2
- package/src/testing/harness.ts +229 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/react.test.tsx +48 -0
- package/src/testing/react.tsx +113 -0
- package/src/testing/withPluginManager.stories.tsx +1 -1
- package/src/ui/components/App/App.stories.tsx +1 -1
- package/src/ui/components/App/App.tsx +25 -2
- package/src/ui/components/Placeholder/Placeholder.stories.tsx +77 -0
- package/src/ui/components/Placeholder/Placeholder.tsx +155 -0
- package/src/ui/components/Placeholder/index.ts +5 -0
- package/src/ui/components/PluginManager/PluginManagerContext.stories.tsx +4 -2
- package/src/ui/components/Surface/SurfaceComponent.stories.tsx +1 -1
- package/src/ui/components/Surface/SurfaceComponent.tsx +83 -46
- package/src/ui/components/Surface/index.ts +20 -1
- package/src/ui/components/Surface/types.test.ts +126 -0
- package/src/ui/components/Surface/types.ts +164 -12
- package/src/ui/components/index.ts +1 -0
- package/src/ui/hooks/useApp.tsx +165 -41
- package/src/ui/hooks/useLoading.tsx +14 -6
- package/src/vite-plugin/boot-loader/BootLoader.stories.tsx +263 -0
- package/src/vite-plugin/boot-loader/boot-loader.css +294 -0
- package/src/vite-plugin/boot-loader/boot-loader.js +274 -0
- package/src/vite-plugin/boot-loader/index.ts +112 -0
- package/src/vite-plugin/composer/index.ts +277 -0
- package/src/vite-plugin/import-map/index.ts +524 -0
- package/src/vite-plugin/index.ts +6 -2
- package/src/vite-plugin/manifest.test.ts +24 -0
- package/src/vite-plugin/manifest.ts +50 -0
- package/src/vite-plugin/packages.ts +169 -10
- package/tsconfig.json +9 -0
- package/.swc/plugins/linux_x86_64_19.0.0/727453fb3a62f7f1d952a41e051ca8a6f88cadc45cee43c6a4d1aa45f9b75665.wasmer-v7 +0 -0
- package/dist/lib/browser/capability-OP63CD5N.mjs.map +0 -7
- package/dist/lib/browser/chunk-F7FW2RK2.mjs.map +0 -7
- package/dist/lib/browser/chunk-FU4GAFUQ.mjs.map +0 -7
- package/dist/lib/browser/chunk-GX4TUNM6.mjs.map +0 -7
- package/dist/lib/browser/chunk-JKWMHZP6.mjs.map +0 -7
- package/dist/lib/browser/chunk-LVJW5EFU.mjs +0 -157
- package/dist/lib/browser/chunk-LVJW5EFU.mjs.map +0 -7
- package/dist/lib/browser/chunk-RFSO3JRG.mjs +0 -1
- package/dist/lib/browser/chunk-WPE6AL7I.mjs +0 -905
- package/dist/lib/browser/chunk-WPE6AL7I.mjs.map +0 -7
- package/dist/lib/browser/invoker-capability-H5PPENOC.mjs.map +0 -7
- package/dist/lib/node-esm/capability-WFEG6CIZ.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-4A3ZCMI3.mjs +0 -158
- package/dist/lib/node-esm/chunk-4A3ZCMI3.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-BCEOLX47.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-G3RTFSNG.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-LQKOTNJW.mjs +0 -906
- package/dist/lib/node-esm/chunk-LQKOTNJW.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-ULUEXB7Q.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-URWHJQT2.mjs.map +0 -7
- package/dist/lib/node-esm/invoker-capability-S3ZA527J.mjs.map +0 -7
- package/dist/types/src/vite-plugin/composer-plugin.d.ts +0 -18
- package/dist/types/src/vite-plugin/composer-plugin.d.ts.map +0 -1
- package/dist/types/src/vite-plugin/import-map-plugin.d.ts +0 -16
- package/dist/types/src/vite-plugin/import-map-plugin.d.ts.map +0 -1
- package/src/vite-plugin/composer-plugin.ts +0 -128
- package/src/vite-plugin/import-map-plugin.ts +0 -314
- /package/dist/lib/browser/{capability-BBBBAPDI.mjs.map → capability-Q5XRXRD2.mjs.map} +0 -0
- /package/dist/lib/browser/{chunk-I34GF4NG.mjs.map → chunk-45CHLTBV.mjs.map} +0 -0
- /package/dist/lib/browser/{chunk-RFSO3JRG.mjs.map → chunk-G7SDBRKH.mjs.map} +0 -0
- /package/dist/lib/node-esm/{capability-AWBEMRYR.mjs.map → capability-EW5GJCI6.mjs.map} +0 -0
- /package/dist/lib/node-esm/{chunk-WZCSOX5Q.mjs.map → chunk-6XW6LET6.mjs.map} +0 -0
- /package/dist/lib/node-esm/{chunk-EL3R25OQ.mjs.map → chunk-OZ7DZA5Z.mjs.map} +0 -0
package/src/core/plugin.ts
CHANGED
|
@@ -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
|
|
93
|
-
*
|
|
94
|
-
*
|
|
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
|
-
|
|
107
|
+
firesBeforeActivation?: ActivationEvent.ActivationEvent[];
|
|
97
108
|
|
|
98
109
|
/**
|
|
99
|
-
* Events
|
|
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
|
-
|
|
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
|
|
120
|
-
readonly
|
|
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.
|
|
127
|
-
this.
|
|
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
|
});
|
package/src/core/url-loader.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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 =
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
301
|
+
return yield* Effect.fail(new RemotePluginLoadError({ context: { locator, reason: 'invalid-locator' } }));
|
|
134
302
|
}
|
|
135
|
-
const plugin = yield*
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
15
|
+
firesBeforeActivation: [ActivationEvents.SetupOperationHandler],
|
|
16
|
+
firesAfterActivation: [ActivationEvents.OperationInvokerReady],
|
|
17
17
|
activate: OperationInvoker,
|
|
18
18
|
}),
|
|
19
19
|
Plugin.addModule({
|