@dxos/app-framework 0.10.0 → 0.11.1
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/.storybook/main.mts +1 -1
- package/.storybook/preview.mts +2 -2
- package/CHANGELOG.md +120 -0
- package/TASKS.md +11 -0
- package/dist/lib/chunk-SurfaceManager.mjs +960 -0
- package/dist/lib/chunk-SurfaceManager.mjs.map +1 -0
- package/dist/lib/chunk-capability.mjs +15 -0
- package/dist/lib/chunk-capability.mjs.map +1 -0
- package/dist/lib/{browser/chunk-FJ4765WW.mjs → chunk-context.mjs} +4 -5
- package/dist/lib/chunk-context.mjs.map +1 -0
- package/dist/lib/chunk-history-tracker.mjs +160 -0
- package/dist/lib/chunk-history-tracker.mjs.map +1 -0
- package/dist/lib/chunk-plugin-manager.mjs +1390 -0
- package/dist/lib/chunk-plugin-manager.mjs.map +1 -0
- package/dist/lib/chunk-plugin-manifest.mjs +125 -0
- package/dist/lib/chunk-plugin-manifest.mjs.map +1 -0
- package/dist/lib/chunk-process-manager-capability.mjs +126 -0
- package/dist/lib/chunk-process-manager-capability.mjs.map +1 -0
- package/dist/lib/chunk-rolldown-runtime.mjs +13 -0
- package/dist/lib/chunk-url-loader.mjs +347 -0
- package/dist/lib/chunk-url-loader.mjs.map +1 -0
- package/dist/lib/cli.mjs +78 -0
- package/dist/lib/cli.mjs.map +1 -0
- package/dist/lib/common/activation-events.mjs +35 -0
- package/dist/lib/common/activation-events.mjs.map +1 -0
- package/dist/lib/common/capabilities.mjs +184 -0
- package/dist/lib/common/capabilities.mjs.map +1 -0
- package/dist/lib/config.mjs +2 -0
- package/dist/lib/core/activation-event.mjs +55 -0
- package/dist/lib/core/activation-event.mjs.map +1 -0
- package/dist/lib/core/capability.mjs +189 -0
- package/dist/lib/core/capability.mjs.map +1 -0
- package/dist/lib/core/plugin-manager.mjs +2 -0
- package/dist/lib/core/plugin.mjs +340 -0
- package/dist/lib/core/plugin.mjs.map +1 -0
- package/dist/lib/core/url-loader.mjs +2 -0
- package/dist/lib/index.mjs +134 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/testing/react.mjs +70 -0
- package/dist/lib/testing/react.mjs.map +1 -0
- package/dist/lib/testing.mjs +399 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/lib/ui.mjs +636 -0
- package/dist/lib/ui.mjs.map +1 -0
- package/dist/lib/vite-plugin.mjs +986 -0
- package/dist/lib/vite-plugin.mjs.map +1 -0
- package/dist/plugin/node-esm/index.mjs +4 -3
- package/dist/plugin/node-esm/index.mjs.map +3 -3
- package/dist/plugin/node-esm/meta.json +1 -1
- package/dist/types/src/common/Role.d.ts +31 -0
- package/dist/types/src/common/Role.d.ts.map +1 -0
- package/dist/types/src/common/Role.test.d.ts +2 -0
- package/dist/types/src/common/Role.test.d.ts.map +1 -0
- package/dist/types/src/common/capabilities.d.ts +11 -2
- package/dist/types/src/common/capabilities.d.ts.map +1 -1
- package/dist/types/src/common/index.d.ts +1 -0
- package/dist/types/src/common/index.d.ts.map +1 -1
- package/dist/types/src/core/capability-manager.d.ts +1 -1
- package/dist/types/src/core/capability-manager.d.ts.map +1 -1
- package/dist/types/src/core/capability.d.ts +38 -5
- package/dist/types/src/core/capability.d.ts.map +1 -1
- package/dist/types/src/core/plugin-manager.d.ts +1 -1
- package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
- package/dist/types/src/core/plugin.d.ts +14 -1
- package/dist/types/src/core/plugin.d.ts.map +1 -1
- package/dist/types/src/core/registry.d.ts +1 -1
- package/dist/types/src/core/registry.d.ts.map +1 -1
- package/dist/types/src/plugin-process-manager/history/capability.d.ts +1 -1
- package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts +1 -1
- package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts.map +1 -1
- package/dist/types/src/testing/StorybookErrorFallback.d.ts +15 -0
- package/dist/types/src/testing/StorybookErrorFallback.d.ts.map +1 -0
- package/dist/types/src/testing/harness.d.ts +1 -1
- package/dist/types/src/testing/harness.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/react.d.ts +4 -4
- package/dist/types/src/testing/react.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 +6 -0
- package/dist/types/src/testing/withPluginManager.stories.d.ts.map +1 -1
- package/dist/types/src/testing/withSurfaceDebug.d.ts +10 -0
- package/dist/types/src/testing/withSurfaceDebug.d.ts.map +1 -0
- package/dist/types/src/ui/components/App/App.d.ts +0 -32
- package/dist/types/src/ui/components/App/App.d.ts.map +1 -1
- package/dist/types/src/ui/components/App/index.d.ts +1 -0
- package/dist/types/src/ui/components/App/index.d.ts.map +1 -1
- package/dist/types/src/ui/components/App/loader.d.ts +35 -0
- package/dist/types/src/ui/components/App/loader.d.ts.map +1 -0
- package/dist/types/src/ui/components/HomeSection/HomeSection.d.ts +26 -0
- package/dist/types/src/ui/components/HomeSection/HomeSection.d.ts.map +1 -0
- package/dist/types/src/ui/components/HomeSection/HomeSection.stories.d.ts +15 -0
- package/dist/types/src/ui/components/HomeSection/HomeSection.stories.d.ts.map +1 -0
- package/dist/types/src/ui/components/HomeSection/index.d.ts +2 -0
- package/dist/types/src/ui/components/HomeSection/index.d.ts.map +1 -0
- package/dist/types/src/ui/components/NamePopover/NamePopover.d.ts +14 -0
- package/dist/types/src/ui/components/NamePopover/NamePopover.d.ts.map +1 -0
- package/dist/types/src/ui/components/NamePopover/index.d.ts +2 -0
- package/dist/types/src/ui/components/NamePopover/index.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts +21 -9
- package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts +4 -1
- package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/SurfaceComponent.test.d.ts +2 -0
- package/dist/types/src/ui/components/Surface/SurfaceComponent.test.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceDebug.d.ts +40 -0
- package/dist/types/src/ui/components/Surface/SurfaceDebug.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceManager.d.ts +23 -0
- package/dist/types/src/ui/components/Surface/SurfaceManager.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceManagerContext.d.ts +11 -0
- package/dist/types/src/ui/components/Surface/SurfaceManagerContext.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceMetrics.d.ts +72 -0
- package/dist/types/src/ui/components/Surface/SurfaceMetrics.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/index.d.ts +20 -25
- package/dist/types/src/ui/components/Surface/index.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/types.d.ts +56 -52
- package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
- package/dist/types/src/ui/components/index.d.ts +2 -0
- package/dist/types/src/ui/components/index.d.ts.map +1 -1
- package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/moon.yml +25 -18
- package/package.json +58 -73
- package/src/common/Role.test.ts +41 -0
- package/src/common/Role.ts +54 -0
- package/src/common/capabilities.ts +17 -2
- package/src/common/index.ts +1 -0
- package/src/core/capability-manager.test.ts +1 -1
- package/src/core/capability-manager.ts +1 -1
- package/src/core/capability.ts +45 -6
- package/src/core/plugin-manager.test.ts +38 -3
- package/src/core/plugin-manager.ts +18 -1
- package/src/core/plugin.ts +16 -1
- package/src/core/registry.ts +1 -1
- package/src/plugin-process-manager/process-manager-capability.ts +25 -2
- package/src/testing/StorybookErrorFallback.tsx +40 -0
- package/src/testing/harness.ts +1 -1
- package/src/testing/index.ts +2 -0
- package/src/testing/react.test.tsx +2 -1
- package/src/testing/react.tsx +14 -8
- package/src/testing/withPluginManager.stories.tsx +16 -1
- package/src/testing/withPluginManager.tsx +13 -2
- package/src/testing/withSurfaceDebug.tsx +22 -0
- package/src/ui/components/App/App.tsx +7 -35
- package/src/ui/components/App/index.ts +1 -0
- package/src/ui/components/App/loader.ts +39 -0
- package/src/ui/components/HomeSection/HomeSection.stories.tsx +43 -0
- package/src/ui/components/HomeSection/HomeSection.tsx +69 -0
- package/src/ui/components/HomeSection/index.ts +5 -0
- package/src/ui/components/NamePopover/NamePopover.tsx +70 -0
- package/src/ui/components/NamePopover/index.ts +5 -0
- package/src/ui/components/Surface/DESIGN.md +136 -0
- package/src/ui/components/Surface/SurfaceComponent.stories.tsx +70 -41
- package/src/ui/components/Surface/SurfaceComponent.test.tsx +403 -0
- package/src/ui/components/Surface/SurfaceComponent.tsx +209 -189
- package/src/ui/components/Surface/SurfaceDebug.tsx +435 -0
- package/src/ui/components/Surface/SurfaceManager.ts +95 -0
- package/src/ui/components/Surface/SurfaceManagerContext.ts +23 -0
- package/src/ui/components/Surface/SurfaceMetrics.ts +202 -0
- package/src/ui/components/Surface/SurfaceProfilerContext.tsx +12 -2
- package/src/ui/components/Surface/index.ts +24 -22
- package/src/ui/components/Surface/types.test.ts +47 -54
- package/src/ui/components/Surface/types.ts +66 -91
- package/src/ui/components/index.ts +2 -0
- package/src/ui/hooks/useApp.tsx +7 -3
- package/src/vite-plugin/boot-loader/loader-app/boot-loader.css +1 -1
- package/tsconfig.json +3 -0
- package/vite.config.ts +26 -0
- package/dist/lib/browser/capability-S44TE2H7.mjs +0 -35
- package/dist/lib/browser/capability-S44TE2H7.mjs.map +0 -7
- package/dist/lib/browser/chunk-3NQLTQCP.mjs +0 -47
- package/dist/lib/browser/chunk-3NQLTQCP.mjs.map +0 -7
- package/dist/lib/browser/chunk-53HJHMS5.mjs +0 -7
- package/dist/lib/browser/chunk-53HJHMS5.mjs.map +0 -7
- package/dist/lib/browser/chunk-FJ4765WW.mjs.map +0 -7
- package/dist/lib/browser/chunk-J5LGTIGS.mjs +0 -10
- package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +0 -7
- package/dist/lib/browser/chunk-KRJ4KK2W.mjs +0 -505
- package/dist/lib/browser/chunk-KRJ4KK2W.mjs.map +0 -7
- package/dist/lib/browser/chunk-LUH4Y5F2.mjs +0 -430
- package/dist/lib/browser/chunk-LUH4Y5F2.mjs.map +0 -7
- package/dist/lib/browser/chunk-NKVRSMFN.mjs +0 -95
- package/dist/lib/browser/chunk-NKVRSMFN.mjs.map +0 -7
- package/dist/lib/browser/chunk-OO53M5UK.mjs +0 -260
- package/dist/lib/browser/chunk-OO53M5UK.mjs.map +0 -7
- package/dist/lib/browser/chunk-OPTS3ZSN.mjs +0 -83
- package/dist/lib/browser/chunk-OPTS3ZSN.mjs.map +0 -7
- package/dist/lib/browser/chunk-Q2GLJTVV.mjs +0 -12
- package/dist/lib/browser/chunk-Q2GLJTVV.mjs.map +0 -7
- package/dist/lib/browser/chunk-SYXIYT6T.mjs +0 -143
- package/dist/lib/browser/chunk-SYXIYT6T.mjs.map +0 -7
- package/dist/lib/browser/chunk-U5MESPM5.mjs +0 -603
- package/dist/lib/browser/chunk-U5MESPM5.mjs.map +0 -7
- package/dist/lib/browser/chunk-UGYCLOXE.mjs +0 -1471
- package/dist/lib/browser/chunk-UGYCLOXE.mjs.map +0 -7
- package/dist/lib/browser/chunk-VF37YTXJ.mjs +0 -28
- package/dist/lib/browser/chunk-VF37YTXJ.mjs.map +0 -7
- package/dist/lib/browser/chunk-YUVQIOTB.mjs +0 -60
- package/dist/lib/browser/chunk-YUVQIOTB.mjs.map +0 -7
- package/dist/lib/browser/cli/index.mjs +0 -74
- package/dist/lib/browser/cli/index.mjs.map +0 -7
- package/dist/lib/browser/common/activation-events.mjs +0 -20
- package/dist/lib/browser/common/activation-events.mjs.map +0 -7
- package/dist/lib/browser/common/capabilities.mjs +0 -56
- package/dist/lib/browser/common/capabilities.mjs.map +0 -7
- package/dist/lib/browser/config/index.mjs +0 -8
- package/dist/lib/browser/config/index.mjs.map +0 -7
- package/dist/lib/browser/core/activation-event.mjs +0 -20
- package/dist/lib/browser/core/activation-event.mjs.map +0 -7
- package/dist/lib/browser/core/capability.mjs +0 -32
- package/dist/lib/browser/core/capability.mjs.map +0 -7
- package/dist/lib/browser/core/plugin-manager.mjs +0 -19
- package/dist/lib/browser/core/plugin-manager.mjs.map +0 -7
- package/dist/lib/browser/core/plugin.mjs +0 -51
- package/dist/lib/browser/core/plugin.mjs.map +0 -7
- package/dist/lib/browser/core/url-loader.mjs +0 -24
- package/dist/lib/browser/core/url-loader.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -98
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/process-manager-capability-QK4BMKMN.mjs +0 -132
- package/dist/lib/browser/process-manager-capability-QK4BMKMN.mjs.map +0 -7
- package/dist/lib/browser/testing/index.mjs +0 -359
- package/dist/lib/browser/testing/index.mjs.map +0 -7
- package/dist/lib/browser/testing/react.mjs +0 -78
- package/dist/lib/browser/testing/react.mjs.map +0 -7
- package/dist/lib/browser/ui/index.mjs +0 -60
- package/dist/lib/browser/ui/index.mjs.map +0 -7
- package/dist/lib/node-esm/capability-3YRF77LV.mjs +0 -36
- package/dist/lib/node-esm/capability-3YRF77LV.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-2UPUZXGU.mjs +0 -9
- package/dist/lib/node-esm/chunk-2UPUZXGU.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-37Z53PXZ.mjs +0 -10
- package/dist/lib/node-esm/chunk-37Z53PXZ.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-4SE7SX7N.mjs +0 -261
- package/dist/lib/node-esm/chunk-4SE7SX7N.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-BMW6P6QA.mjs +0 -48
- package/dist/lib/node-esm/chunk-BMW6P6QA.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-COHOWGGJ.mjs +0 -144
- package/dist/lib/node-esm/chunk-COHOWGGJ.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-DRT75WGD.mjs +0 -604
- package/dist/lib/node-esm/chunk-DRT75WGD.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-FKMVORZN.mjs +0 -431
- package/dist/lib/node-esm/chunk-FKMVORZN.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-FPW45EZH.mjs +0 -14
- package/dist/lib/node-esm/chunk-FPW45EZH.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-GLH4LGHZ.mjs +0 -84
- package/dist/lib/node-esm/chunk-GLH4LGHZ.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-IL7O7IRX.mjs +0 -1472
- package/dist/lib/node-esm/chunk-IL7O7IRX.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-ND2HETHD.mjs +0 -506
- package/dist/lib/node-esm/chunk-ND2HETHD.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-TLKYZKPW.mjs +0 -29
- package/dist/lib/node-esm/chunk-TLKYZKPW.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-VCPGX7WN.mjs +0 -62
- package/dist/lib/node-esm/chunk-VCPGX7WN.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-XSZMX53L.mjs +0 -96
- package/dist/lib/node-esm/chunk-XSZMX53L.mjs.map +0 -7
- package/dist/lib/node-esm/cli/index.mjs +0 -75
- package/dist/lib/node-esm/cli/index.mjs.map +0 -7
- package/dist/lib/node-esm/common/activation-events.mjs +0 -21
- package/dist/lib/node-esm/common/activation-events.mjs.map +0 -7
- package/dist/lib/node-esm/common/capabilities.mjs +0 -57
- package/dist/lib/node-esm/common/capabilities.mjs.map +0 -7
- package/dist/lib/node-esm/config/index.mjs +0 -9
- package/dist/lib/node-esm/config/index.mjs.map +0 -7
- package/dist/lib/node-esm/core/activation-event.mjs +0 -21
- package/dist/lib/node-esm/core/activation-event.mjs.map +0 -7
- package/dist/lib/node-esm/core/capability.mjs +0 -33
- package/dist/lib/node-esm/core/capability.mjs.map +0 -7
- package/dist/lib/node-esm/core/plugin-manager.mjs +0 -20
- package/dist/lib/node-esm/core/plugin-manager.mjs.map +0 -7
- package/dist/lib/node-esm/core/plugin.mjs +0 -52
- package/dist/lib/node-esm/core/plugin.mjs.map +0 -7
- package/dist/lib/node-esm/core/url-loader.mjs +0 -25
- package/dist/lib/node-esm/core/url-loader.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -99
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/process-manager-capability-5S7WZ2JR.mjs +0 -133
- package/dist/lib/node-esm/process-manager-capability-5S7WZ2JR.mjs.map +0 -7
- package/dist/lib/node-esm/testing/index.mjs +0 -360
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- package/dist/lib/node-esm/testing/react.mjs +0 -79
- package/dist/lib/node-esm/testing/react.mjs.map +0 -7
- package/dist/lib/node-esm/ui/index.mjs +0 -61
- package/dist/lib/node-esm/ui/index.mjs.map +0 -7
- package/dist/types/src/ui/components/Surface/SurfaceInfo.d.ts +0 -11
- package/dist/types/src/ui/components/Surface/SurfaceInfo.d.ts.map +0 -1
- package/src/ui/components/Surface/SurfaceInfo.tsx +0 -106
- package/vitest.config.ts +0 -14
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Atom, Registry } from '@effect-atom/atom
|
|
5
|
+
import { Atom, Registry } from '@effect-atom/atom';
|
|
6
6
|
import * as Array from 'effect/Array';
|
|
7
7
|
import * as Cause from 'effect/Cause';
|
|
8
8
|
import * as Deferred from 'effect/Deferred';
|
|
9
9
|
import * as Duration from 'effect/Duration';
|
|
10
10
|
import * as Effect from 'effect/Effect';
|
|
11
|
+
import * as Exit from 'effect/Exit';
|
|
11
12
|
import * as Fiber from 'effect/Fiber';
|
|
12
13
|
import * as Function from 'effect/Function';
|
|
13
14
|
import * as HashSet from 'effect/HashSet';
|
|
14
15
|
import * as PubSub from 'effect/PubSub';
|
|
15
16
|
import * as Ref from 'effect/Ref';
|
|
17
|
+
import * as Scope from 'effect/Scope';
|
|
16
18
|
|
|
17
19
|
import { EffectEx, Performance } from '@dxos/effect';
|
|
18
20
|
import { BaseError } from '@dxos/errors';
|
|
@@ -298,6 +300,7 @@ class ManagerImpl implements PluginManager {
|
|
|
298
300
|
private readonly _loadTimeout: Duration.DurationInput;
|
|
299
301
|
private readonly _activationTimeout: Duration.DurationInput;
|
|
300
302
|
private readonly _capabilities = new Map<string, Capability.Any[]>();
|
|
303
|
+
private readonly _moduleScopes = new Map<string, Scope.CloseableScope>();
|
|
301
304
|
private readonly _moduleMemoMap = new Map<Plugin.PluginModule['id'], Deferred.Deferred<Capability.Any[], Error>>();
|
|
302
305
|
private readonly _moduleSemaphores = new Map<Plugin.PluginModule['id'], Effect.Semaphore>();
|
|
303
306
|
// Coalesces concurrent `_resolveLazyPlugin` calls per plugin id. Without
|
|
@@ -991,6 +994,10 @@ class ManagerImpl implements PluginManager {
|
|
|
991
994
|
this._set(this._eventsFiredAtom, []);
|
|
992
995
|
this._set(this._pendingResetAtom, []);
|
|
993
996
|
this._moduleMemoMap.clear();
|
|
997
|
+
for (const scope of this._moduleScopes.values()) {
|
|
998
|
+
yield* Scope.close(scope, Exit.void);
|
|
999
|
+
}
|
|
1000
|
+
this._moduleScopes.clear();
|
|
994
1001
|
yield* Ref.set(this._activatingEvents, []);
|
|
995
1002
|
yield* Ref.set(this._activatingModules, []);
|
|
996
1003
|
|
|
@@ -1534,6 +1541,7 @@ class ManagerImpl implements PluginManager {
|
|
|
1534
1541
|
const deferred = yield* Deferred.make<Capability.Any[], Error>();
|
|
1535
1542
|
this._moduleMemoMap.set(module.id, deferred);
|
|
1536
1543
|
|
|
1544
|
+
const scope = yield* Scope.make();
|
|
1537
1545
|
const loadEffect = Effect.gen(this, function* () {
|
|
1538
1546
|
log('loading module', { module: module.id, parentEvent });
|
|
1539
1547
|
performance.mark(`module:${module.id}:start`);
|
|
@@ -1542,6 +1550,7 @@ class ManagerImpl implements PluginManager {
|
|
|
1542
1550
|
const [duration, capabilities] = yield* module.activate().pipe(
|
|
1543
1551
|
Effect.provideService(Capability.Service, this.capabilities),
|
|
1544
1552
|
Effect.provideService(Plugin.Service, this),
|
|
1553
|
+
Scope.extend(scope),
|
|
1545
1554
|
// Cap activation so a single misbehaving module can't hold the
|
|
1546
1555
|
// event chain open. On timeout the failure is recorded against
|
|
1547
1556
|
// the plugin and surfaced as `PluginTimeoutError`.
|
|
@@ -1555,6 +1564,7 @@ class ManagerImpl implements PluginManager {
|
|
|
1555
1564
|
Effect.timed,
|
|
1556
1565
|
);
|
|
1557
1566
|
const normalized = capabilities == null ? [] : Array.isArray(capabilities) ? capabilities : [capabilities];
|
|
1567
|
+
this._moduleScopes.set(module.id, scope);
|
|
1558
1568
|
const elapsed = Duration.toMillis(duration);
|
|
1559
1569
|
performance.mark(`module:${module.id}:end`);
|
|
1560
1570
|
performance.measure(`module:${module.id}`, `module:${module.id}:start`, `module:${module.id}:end`);
|
|
@@ -1567,6 +1577,7 @@ class ManagerImpl implements PluginManager {
|
|
|
1567
1577
|
});
|
|
1568
1578
|
return normalized as Capability.Any[];
|
|
1569
1579
|
}).pipe(
|
|
1580
|
+
Effect.tapErrorCause(() => Scope.close(scope, Exit.void)),
|
|
1570
1581
|
Effect.withSpan('PluginManager._loadModule'),
|
|
1571
1582
|
together(
|
|
1572
1583
|
Effect.sleep(Duration.seconds(10)).pipe(
|
|
@@ -1654,6 +1665,12 @@ class ManagerImpl implements PluginManager {
|
|
|
1654
1665
|
this._capabilities.delete(id);
|
|
1655
1666
|
}
|
|
1656
1667
|
|
|
1668
|
+
const scope = this._moduleScopes.get(id);
|
|
1669
|
+
if (scope) {
|
|
1670
|
+
yield* Scope.close(scope, Exit.void);
|
|
1671
|
+
this._moduleScopes.delete(id);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1657
1674
|
const activeIndex = this._get(this._activeAtom).findIndex((event) => event === id);
|
|
1658
1675
|
if (activeIndex !== -1) {
|
|
1659
1676
|
this._update(this._activeAtom, (active) => active.filter((event) => event !== id));
|
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
|
import * as Schema from 'effect/Schema';
|
|
10
|
+
import type * as Scope from 'effect/Scope';
|
|
10
11
|
|
|
11
12
|
import { BaseError } from '@dxos/errors';
|
|
12
13
|
import { invariant } from '@dxos/invariant';
|
|
@@ -79,6 +80,18 @@ export const isPluginModule = (value: unknown): value is PluginModule => {
|
|
|
79
80
|
/**
|
|
80
81
|
* A unit of containment of modular functionality that can be provided to an application.
|
|
81
82
|
* Activation of a module is async allowing for code to split and loaded lazily.
|
|
83
|
+
*
|
|
84
|
+
* Ordering between modules is expressed through shared activation events, not registration
|
|
85
|
+
* order: a module `activatesOn` an event, fires events *after* it activates via
|
|
86
|
+
* {@link PluginModule.firesAfterActivation} (publishing "I'm ready"), and forces prerequisites
|
|
87
|
+
* to complete *before* it activates via {@link PluginModule.firesBeforeActivation}. To run
|
|
88
|
+
* module B after module A, A declares `firesAfterActivation: [E]` and B declares
|
|
89
|
+
* `activatesOn: E`. See `plugin-client/src/ClientPlugin.ts` for a worked example.
|
|
90
|
+
*
|
|
91
|
+
* @idiom org.dxos.app-framework.moduleActivationOrdering
|
|
92
|
+
* applies: sequencing one plugin module's activation before or after another
|
|
93
|
+
* instead-of: assuming module registration order controls activation order
|
|
94
|
+
* uses: {@link PluginModule.firesAfterActivation}, {@link PluginModule.firesBeforeActivation}
|
|
82
95
|
*/
|
|
83
96
|
export interface PluginModule {
|
|
84
97
|
readonly [PluginModuleTypeId]: PluginModuleTypeId;
|
|
@@ -128,7 +141,9 @@ export interface PluginModule {
|
|
|
128
141
|
* @param props Optional props passed to the module.
|
|
129
142
|
* @returns The capabilities of the module.
|
|
130
143
|
*/
|
|
131
|
-
activate: (
|
|
144
|
+
activate: (
|
|
145
|
+
props?: any,
|
|
146
|
+
) => Effect.Effect<Capability.ModuleReturn, Error, Capability.Service | Service | Scope.Scope | never>;
|
|
132
147
|
}
|
|
133
148
|
|
|
134
149
|
export type PluginModuleOptions = Omit<PluginModule, 'id' | typeof PluginModuleTypeId> & { id?: string };
|
package/src/core/registry.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2026 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Atom, type Registry as AtomRegistry } from '@effect-atom/atom
|
|
5
|
+
import { Atom, type Registry as AtomRegistry } from '@effect-atom/atom';
|
|
6
6
|
import * as Effect from 'effect/Effect';
|
|
7
7
|
|
|
8
8
|
import { EffectEx } from '@dxos/effect';
|
|
@@ -8,8 +8,15 @@ import * as Layer from 'effect/Layer';
|
|
|
8
8
|
import * as ManagedRuntime from 'effect/ManagedRuntime';
|
|
9
9
|
|
|
10
10
|
import { LayerSpec, OperationHandlerSet, Process, ServiceResolver, Trace } from '@dxos/compute';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
LayerStack,
|
|
13
|
+
ProcessManager,
|
|
14
|
+
ProcessMonitor,
|
|
15
|
+
RemoteProcessManager,
|
|
16
|
+
RemoteTraceMonitor,
|
|
17
|
+
} from '@dxos/compute-runtime';
|
|
12
18
|
import { invariant } from '@dxos/invariant';
|
|
19
|
+
import { log } from '@dxos/log';
|
|
13
20
|
// Explicit import so the emitted `.d.ts` references the package via its public
|
|
14
21
|
// alias instead of a relative `node_modules` path (TS2883).
|
|
15
22
|
import { OperationInvoker } from '@dxos/operation';
|
|
@@ -50,6 +57,10 @@ export default Capability.makeModule(
|
|
|
50
57
|
|
|
51
58
|
const layerSpecs = yield* Capability.getAll(Capabilities.LayerSpec);
|
|
52
59
|
const traceSinkFactories = yield* Capability.getAll(Capabilities.TraceSink);
|
|
60
|
+
// Optional swarm-backed remote trace source (DX-1125); first contribution wins, else empty.
|
|
61
|
+
const remoteTraceMonitors = yield* Capability.getAll(Capabilities.RemoteTraceMonitor);
|
|
62
|
+
|
|
63
|
+
log.info('setup process manager', { traceSinkFactories });
|
|
53
64
|
|
|
54
65
|
// Forward reference to `ProcessManager.ProcessManagerService`. The runtime
|
|
55
66
|
// that owns the manager depends transitively on `ServiceResolver` (which is
|
|
@@ -121,7 +132,19 @@ export default Capability.makeModule(
|
|
|
121
132
|
Layer.provide(Layer.mergeAll(processManagerLayer, baseLayer)),
|
|
122
133
|
);
|
|
123
134
|
|
|
124
|
-
|
|
135
|
+
// App-framework has no EDGE runtime, so the remote process view is empty;
|
|
136
|
+
// the aggregate monitor therefore equals the local process tree.
|
|
137
|
+
const remoteProcessManagerLayer = RemoteProcessManager.layerNoop.pipe(Layer.provide(baseLayer));
|
|
138
|
+
// Remote ephemeral trace (DX-1125): use the first contributed swarm-backed monitor, else no-op.
|
|
139
|
+
const remoteTraceMonitorLayer =
|
|
140
|
+
remoteTraceMonitors.length > 0
|
|
141
|
+
? Layer.succeed(RemoteTraceMonitor.Service, remoteTraceMonitors[0])
|
|
142
|
+
: RemoteTraceMonitor.layerNoop;
|
|
143
|
+
const processMonitorLayer = ProcessMonitor.layer.pipe(
|
|
144
|
+
Layer.provide(Layer.mergeAll(processManagerLayer, remoteProcessManagerLayer, remoteTraceMonitorLayer, baseLayer)),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const runtimeLayer = Layer.mergeAll(baseLayer, processManagerLayer, operationInvokerLayer, processMonitorLayer);
|
|
125
148
|
|
|
126
149
|
const managedRuntime = ManagedRuntime.make(runtimeLayer as Layer.Layer<any, any, never>);
|
|
127
150
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { ErrorFallback, type FallbackProps } from '@dxos/react-error-boundary';
|
|
8
|
+
import { downloadLogs } from '@dxos/storybook-addon-logger/download';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default `ErrorBoundary` fallback for storybook-hosted apps (`withPluginManager`/`useApp`). Adds
|
|
12
|
+
* a "Download logs" action alongside the theme-independent default `ErrorFallback`, so a crashed
|
|
13
|
+
* story still offers a one-click way to grab the story's buffered logs, matching the manager
|
|
14
|
+
* toolbar's button (see `@dxos/storybook-addon-logger`).
|
|
15
|
+
*
|
|
16
|
+
* Deliberately does NOT depend on `@dxos/react-ui`'s themed components: `useApp`'s `ErrorBoundary`
|
|
17
|
+
* can catch failures that happen before/during an app's own theme setup, so its fallback must keep
|
|
18
|
+
* working even when no theme context is available — the same reason `ErrorFallback` itself uses
|
|
19
|
+
* inline styles rather than Tailwind/theme dependencies.
|
|
20
|
+
*/
|
|
21
|
+
export const StorybookErrorFallback = (props: FallbackProps) => (
|
|
22
|
+
<div>
|
|
23
|
+
<ErrorFallback {...props} />
|
|
24
|
+
<button
|
|
25
|
+
type='button'
|
|
26
|
+
onClick={downloadLogs}
|
|
27
|
+
style={{
|
|
28
|
+
margin: '0 1rem 1rem',
|
|
29
|
+
padding: '0.25rem 0.75rem',
|
|
30
|
+
fontSize: '0.875rem',
|
|
31
|
+
background: 'transparent',
|
|
32
|
+
border: '1px solid #888888',
|
|
33
|
+
borderRadius: '0.375rem',
|
|
34
|
+
cursor: 'pointer',
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
Download logs
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
package/src/testing/harness.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2026 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type Registry } from '@effect-atom/atom
|
|
5
|
+
import { type Registry } from '@effect-atom/atom';
|
|
6
6
|
import * as Duration from 'effect/Duration';
|
|
7
7
|
import * as Effect from 'effect/Effect';
|
|
8
8
|
import * as PubSub from 'effect/PubSub';
|
package/src/testing/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { describe, test } from 'vitest';
|
|
|
9
9
|
import { DXN } from '@dxos/keys';
|
|
10
10
|
|
|
11
11
|
import { ActivationEvents, Capabilities } from '../common';
|
|
12
|
+
import * as Role from '../common/Role';
|
|
12
13
|
import { Capability, Plugin } from '../core';
|
|
13
14
|
import { Surface } from '../ui';
|
|
14
15
|
import { createTestApp } from './harness';
|
|
@@ -16,7 +17,7 @@ import { render, renderSurface } from './react';
|
|
|
16
17
|
|
|
17
18
|
const testMeta = Plugin.makeMeta({ key: DXN.make('org.dxos.plugin.test.reactHarness'), name: 'ReactHarnessTest' });
|
|
18
19
|
|
|
19
|
-
const GreetingRole =
|
|
20
|
+
const GreetingRole = Role.make<{ message: string }>('org.dxos.test.role.greeting');
|
|
20
21
|
|
|
21
22
|
const TestPlugin = Plugin.define(testMeta).pipe(
|
|
22
23
|
Plugin.addModule({
|
package/src/testing/react.tsx
CHANGED
|
@@ -4,16 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
import { RegistryContext } from '@effect-atom/atom-react';
|
|
6
6
|
import { type RenderOptions, type RenderResult, render as rtlRender } from '@testing-library/react';
|
|
7
|
-
import React, { type FC, Fragment, type PropsWithChildren, type ReactNode } from 'react';
|
|
7
|
+
import React, { type FC, Fragment, type PropsWithChildren, type ReactNode, useMemo } from 'react';
|
|
8
8
|
|
|
9
9
|
import { ContextProtocolProvider } from '@dxos/web-context-react';
|
|
10
10
|
|
|
11
11
|
import { Capabilities } from '../common';
|
|
12
|
+
import * as Role from '../common/Role';
|
|
12
13
|
import { PluginManagerContext } from '../context';
|
|
13
14
|
import { topologicalSort } from '../helpers';
|
|
14
15
|
import { PluginManagerProvider } from '../ui/components/PluginManager/PluginManagerProvider';
|
|
15
16
|
import { SurfaceComponent } from '../ui/components/Surface/SurfaceComponent';
|
|
16
|
-
import {
|
|
17
|
+
import { SurfaceManager } from '../ui/components/Surface/SurfaceManager';
|
|
18
|
+
import { SurfaceManagerProvider } from '../ui/components/Surface/SurfaceManagerContext';
|
|
17
19
|
import { type TestHarness } from './harness';
|
|
18
20
|
|
|
19
21
|
export type HarnessRenderOptions = Omit<RenderOptions, 'wrapper'> & {
|
|
@@ -35,9 +37,9 @@ export const render = (harness: TestHarness, ui: ReactNode, options?: HarnessRen
|
|
|
35
37
|
return rtlRender(<>{ui}</>, { ...rest, wrapper: Wrapper });
|
|
36
38
|
};
|
|
37
39
|
|
|
38
|
-
export type RenderSurfaceProps<TToken extends
|
|
40
|
+
export type RenderSurfaceProps<TToken extends Role.Role<any>> = {
|
|
39
41
|
type: TToken;
|
|
40
|
-
data?: TToken extends
|
|
42
|
+
data?: TToken extends Role.Role<infer D> ? D : never;
|
|
41
43
|
limit?: number;
|
|
42
44
|
fallback?: FC<{ error: Error; data?: any }>;
|
|
43
45
|
placeholder?: ReactNode;
|
|
@@ -46,7 +48,7 @@ export type RenderSurfaceProps<TToken extends RoleToken<any>> = {
|
|
|
46
48
|
/**
|
|
47
49
|
* Renders a `Surface` with the given role token/data inside the harness provider tree.
|
|
48
50
|
*/
|
|
49
|
-
export const renderSurface = <TToken extends
|
|
51
|
+
export const renderSurface = <TToken extends Role.Role<any>>(
|
|
50
52
|
harness: TestHarness,
|
|
51
53
|
props: RenderSurfaceProps<TToken>,
|
|
52
54
|
options?: HarnessRenderOptions,
|
|
@@ -68,13 +70,17 @@ const HarnessProviders = ({ harness, extra, children }: HarnessProvidersProps) =
|
|
|
68
70
|
const contributed = harness.getAll(Capabilities.ReactContext);
|
|
69
71
|
const ContributedContext = composeContexts(contributed);
|
|
70
72
|
const ExtraContext = composeExtra(extra);
|
|
73
|
+
// Mirror useApp's provider stack (see SurfaceManager) so the unit-test render path matches the app.
|
|
74
|
+
const surfaces = useMemo(() => new SurfaceManager(harness.manager.capabilities), [harness]);
|
|
71
75
|
return (
|
|
72
76
|
<PluginManagerProvider value={harness.manager}>
|
|
73
77
|
<ContextProtocolProvider value={harness.manager} context={PluginManagerContext}>
|
|
74
78
|
<RegistryContext.Provider value={harness.registry}>
|
|
75
|
-
<
|
|
76
|
-
<
|
|
77
|
-
|
|
79
|
+
<SurfaceManagerProvider value={surfaces}>
|
|
80
|
+
<ContributedContext>
|
|
81
|
+
<ExtraContext>{children}</ExtraContext>
|
|
82
|
+
</ContributedContext>
|
|
83
|
+
</SurfaceManagerProvider>
|
|
78
84
|
</RegistryContext.Provider>
|
|
79
85
|
</ContextProtocolProvider>
|
|
80
86
|
</PluginManagerProvider>
|
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
6
|
import React from 'react';
|
|
7
7
|
|
|
8
|
+
import { ThrowError } from '@dxos/react-ui';
|
|
8
9
|
import { withTheme } from '@dxos/react-ui/testing';
|
|
9
10
|
|
|
10
11
|
import { Capabilities } from '../common';
|
|
12
|
+
import * as Role from '../common/Role';
|
|
11
13
|
import { Capability } from '../core';
|
|
12
14
|
import { Surface } from '../ui';
|
|
13
15
|
import { withPluginManager } from './withPluginManager';
|
|
14
16
|
|
|
15
|
-
const MainRole =
|
|
17
|
+
const MainRole = Role.make<Record<string, never>>('org.dxos.test.role.main');
|
|
16
18
|
|
|
17
19
|
const DefaultStory = () => {
|
|
18
20
|
console.log('Render');
|
|
@@ -49,3 +51,16 @@ export default meta;
|
|
|
49
51
|
type Story = StoryObj<typeof meta>;
|
|
50
52
|
|
|
51
53
|
export const Default: Story = {};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Demonstrates `withPluginManager`'s default fallback (`StorybookErrorFallback`): when the app
|
|
57
|
+
* throws, the theme-independent `ErrorFallback` renders with a "Download logs" button alongside
|
|
58
|
+
* the usual "Copy" action.
|
|
59
|
+
*/
|
|
60
|
+
export const Crashes: Story = {
|
|
61
|
+
render: () => <ThrowError />,
|
|
62
|
+
play: async () => {
|
|
63
|
+
// This story intentionally renders an ErrorBoundary fallback; clear the smoke-test error flag.
|
|
64
|
+
(window as any).__ERROR_BOUNDARY_ERRORS__ = [];
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -15,6 +15,7 @@ import { type MaybeProvider, getProviderValue } from '@dxos/util';
|
|
|
15
15
|
import { ActivationEvents, Capabilities } from '../common';
|
|
16
16
|
import { type ActivationEvent, Capability, type CapabilityManager, Plugin, PluginManager } from '../core';
|
|
17
17
|
import { type UseAppOptions, useApp } from '../ui';
|
|
18
|
+
import { StorybookErrorFallback } from './StorybookErrorFallback';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* @internal
|
|
@@ -50,6 +51,7 @@ export const setupPluginManager = ({
|
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
type ManagedPluginManagerState = {
|
|
54
|
+
fallback?: UseAppOptions['fallback'];
|
|
53
55
|
fireEvents?: (ActivationEvent.ActivationEvent | string)[];
|
|
54
56
|
pluginManager: PluginManager.PluginManager;
|
|
55
57
|
setupEvents?: ActivationEvent.ActivationEvent[];
|
|
@@ -94,6 +96,7 @@ export const withPluginManager = <Args,>(init: WithPluginManagerInitializer<Args
|
|
|
94
96
|
pluginManager,
|
|
95
97
|
setupEvents: options.setupEvents,
|
|
96
98
|
fireEvents: options.fireEvents,
|
|
99
|
+
fallback: options.fallback,
|
|
97
100
|
storyId,
|
|
98
101
|
});
|
|
99
102
|
|
|
@@ -112,13 +115,21 @@ export const withPluginManager = <Args,>(init: WithPluginManagerInitializer<Args
|
|
|
112
115
|
};
|
|
113
116
|
};
|
|
114
117
|
|
|
115
|
-
const WithPluginManagerApp = ({
|
|
118
|
+
const WithPluginManagerApp = ({
|
|
119
|
+
fallback,
|
|
120
|
+
fireEvents,
|
|
121
|
+
pluginManager,
|
|
122
|
+
setupEvents,
|
|
123
|
+
storyId,
|
|
124
|
+
}: ManagedPluginManagerState) => {
|
|
116
125
|
// Fire deprecated events only after the effect-owned manager for this story exists.
|
|
117
126
|
useAsyncEffect(async () => {
|
|
118
127
|
await Promise.all(fireEvents?.map((event) => pluginManager.activate(event)) ?? []);
|
|
119
128
|
}, [fireEvents, pluginManager, storyId]);
|
|
120
129
|
|
|
121
|
-
|
|
130
|
+
// Default to a fallback that offers "Download logs" so a crashed story is still debuggable;
|
|
131
|
+
// callers can override via `withPluginManager({ fallback })`.
|
|
132
|
+
const App = useApp({ pluginManager, setupEvents, fallback: fallback ?? StorybookErrorFallback });
|
|
122
133
|
return <App />;
|
|
123
134
|
};
|
|
124
135
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Decorator } from '@storybook/react';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
import { Surface } from '../ui';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Storybook decorator that enables the surface debug overlays (`<dx-surface>` wrappers + boundary
|
|
12
|
+
* badges) for the wrapped story. The flag is a `window` singleton, so it is (re)applied on every
|
|
13
|
+
* render — enabling it on decorated stories and clearing it otherwise, so the setting never leaks
|
|
14
|
+
* between stories. Prefer this over the `.storybook/preview` flag when the story must also render
|
|
15
|
+
* under the aggregate root Storybook, which does not load a per-package preview.
|
|
16
|
+
*/
|
|
17
|
+
export const withSurfaceDebug =
|
|
18
|
+
(enabled = true): Decorator =>
|
|
19
|
+
(Story) => {
|
|
20
|
+
Surface.setDebug(enabled);
|
|
21
|
+
return <Story />;
|
|
22
|
+
};
|
|
@@ -7,36 +7,9 @@ import React, { type PropsWithChildren, useEffect, useLayoutEffect } from 'react
|
|
|
7
7
|
import { Capabilities } from '../../../common';
|
|
8
8
|
import { topologicalSort } from '../../../helpers';
|
|
9
9
|
import { LoadingState, type StartupProgress, type UseAppOptions, useCapabilities, useLoading } from '../../hooks';
|
|
10
|
+
import { bootLoader } from './loader';
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
interface Window {
|
|
13
|
-
/**
|
|
14
|
-
* Driver injected by `@dxos/app-framework/vite-plugin`'s `bootLoaderPlugin`
|
|
15
|
-
* (a Solid app inlined into `index.html`). Declared here — on a module that
|
|
16
|
-
* is part of the `@dxos/app-framework/ui` export surface — so the React side
|
|
17
|
-
* and host apps (e.g. composer-app) can drive the loader without each
|
|
18
|
-
* re-declaring the type. The canonical definition lives in the plugin's
|
|
19
|
-
* `loader-app/types.ts`; this mirror exists because that source compiles in a
|
|
20
|
-
* separate (Solid) program that doesn't ship its globals to consumers.
|
|
21
|
-
*/
|
|
22
|
-
__bootLoader?: {
|
|
23
|
-
status: (payload: {
|
|
24
|
-
event?: string;
|
|
25
|
-
module?: string;
|
|
26
|
-
humanized: string;
|
|
27
|
-
/**
|
|
28
|
-
* Optional `(index/total)` tick. When present, the loader replaces the
|
|
29
|
-
* current line in place ("Loading plugins (12/80)") instead of appending
|
|
30
|
-
* a new entry — keeps the visible log compact during long counted phases.
|
|
31
|
-
*/
|
|
32
|
-
range?: { index: number; total: number };
|
|
33
|
-
}) => void;
|
|
34
|
-
progress: (fraction?: number) => void;
|
|
35
|
-
ready: () => void;
|
|
36
|
-
dismiss: () => void;
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
12
|
+
const FIRST_INTERACTIVE_MARK = 'app-framework:first-interactive';
|
|
40
13
|
|
|
41
14
|
export type AppProps = Pick<UseAppOptions, 'debounce'> & {
|
|
42
15
|
ready: boolean;
|
|
@@ -44,8 +17,6 @@ export type AppProps = Pick<UseAppOptions, 'debounce'> & {
|
|
|
44
17
|
progress?: StartupProgress;
|
|
45
18
|
};
|
|
46
19
|
|
|
47
|
-
const FIRST_INTERACTIVE_MARK = 'app-framework:first-interactive';
|
|
48
|
-
|
|
49
20
|
export const App = ({ ready, error, debounce, progress }: AppProps) => {
|
|
50
21
|
const reactContexts = useCapabilities(Capabilities.ReactContext);
|
|
51
22
|
const reactRoots = useCapabilities(Capabilities.ReactRoot);
|
|
@@ -62,10 +33,11 @@ export const App = ({ ready, error, debounce, progress }: AppProps) => {
|
|
|
62
33
|
if (stage >= LoadingState.FadeOut) {
|
|
63
34
|
return;
|
|
64
35
|
}
|
|
36
|
+
|
|
65
37
|
const fraction = progress?.progress ?? 0;
|
|
66
|
-
|
|
38
|
+
bootLoader?.progress(0.5 + fraction * 0.5);
|
|
67
39
|
if (progress?.humanizedName) {
|
|
68
|
-
|
|
40
|
+
bootLoader?.status({
|
|
69
41
|
event: progress.event,
|
|
70
42
|
module: progress.module,
|
|
71
43
|
humanized: `Activating ${progress.humanizedName}`,
|
|
@@ -78,7 +50,7 @@ export const App = ({ ready, error, debounce, progress }: AppProps) => {
|
|
|
78
50
|
// same frame the real shell starts rendering beneath it.
|
|
79
51
|
useLayoutEffect(() => {
|
|
80
52
|
if (stage >= LoadingState.FadeOut) {
|
|
81
|
-
|
|
53
|
+
bootLoader?.ready();
|
|
82
54
|
}
|
|
83
55
|
}, [stage]);
|
|
84
56
|
|
|
@@ -97,7 +69,7 @@ export const App = ({ ready, error, debounce, progress }: AppProps) => {
|
|
|
97
69
|
if (performance.getEntriesByName(FIRST_INTERACTIVE_MARK).length === 0) {
|
|
98
70
|
performance.mark(FIRST_INTERACTIVE_MARK);
|
|
99
71
|
}
|
|
100
|
-
|
|
72
|
+
bootLoader?.dismiss();
|
|
101
73
|
}, [placeholderDismissed]);
|
|
102
74
|
|
|
103
75
|
// Used in tests to exercise the error boundary & reset dialog (see
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface BootLoader {
|
|
7
|
+
status: (payload: {
|
|
8
|
+
event?: string;
|
|
9
|
+
module?: string;
|
|
10
|
+
humanized: string;
|
|
11
|
+
/**
|
|
12
|
+
* Optional `(index/total)` tick. When present, the loader replaces the
|
|
13
|
+
* current line in place ("Loading plugins (12/80)") instead of appending
|
|
14
|
+
* a new entry — keeps the visible log compact during long counted phases.
|
|
15
|
+
*/
|
|
16
|
+
range?: { index: number; total: number };
|
|
17
|
+
}) => void;
|
|
18
|
+
progress: (fraction?: number) => void;
|
|
19
|
+
ready: () => void;
|
|
20
|
+
dismiss: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Window {
|
|
24
|
+
/**
|
|
25
|
+
* Driver injected by `@dxos/app-framework/vite-plugin`'s `bootLoaderPlugin`
|
|
26
|
+
* (a Solid app inlined into `index.html`). Declared here — on a module that
|
|
27
|
+
* is part of the `@dxos/app-framework/ui` export surface — so the React side
|
|
28
|
+
* and host apps (e.g. composer-app) can drive the loader without each
|
|
29
|
+
* re-declaring the type. The canonical definition lives in the plugin's
|
|
30
|
+
* `loader-app/types.ts`; this mirror exists because that source compiles in a
|
|
31
|
+
* separate (Solid) program that doesn't ship its globals to consumers.
|
|
32
|
+
*/
|
|
33
|
+
__bootLoader?: BootLoader;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Guarded: `@dxos/app-framework/ui` has a `node` export and this module is re-exported through the
|
|
38
|
+
// UI barrel, so a bare top-level `window` read would throw when imported in SSR / tests / tooling.
|
|
39
|
+
export const bootLoader = typeof window !== 'undefined' ? window.__bootLoader : undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
import { Button } from '@dxos/react-ui';
|
|
9
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
|
|
11
|
+
import { HomeSection } from './HomeSection';
|
|
12
|
+
|
|
13
|
+
const DefaultStory = () => (
|
|
14
|
+
<HomeSection.Root>
|
|
15
|
+
<HomeSection.Header title='Recent' onClose={() => {}} />
|
|
16
|
+
<div className='rounded-sm bg-group-surface p-4 text-description'>Section content.</div>
|
|
17
|
+
</HomeSection.Root>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const WithActionsStory = () => (
|
|
21
|
+
<HomeSection.Root>
|
|
22
|
+
<HomeSection.Header title='Activity' onClose={() => {}}>
|
|
23
|
+
<Button variant='ghost'>All</Button>
|
|
24
|
+
<Button variant='ghost'>30d</Button>
|
|
25
|
+
</HomeSection.Header>
|
|
26
|
+
<div className='rounded-sm bg-group-surface p-4 text-description'>Section content.</div>
|
|
27
|
+
</HomeSection.Root>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const meta = {
|
|
31
|
+
title: 'sdk/app-framework/components/HomeSection',
|
|
32
|
+
render: DefaultStory,
|
|
33
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen', classNames: 'p-4' })],
|
|
34
|
+
parameters: { layout: 'fullscreen' },
|
|
35
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
36
|
+
|
|
37
|
+
export default meta;
|
|
38
|
+
|
|
39
|
+
type Story = StoryObj<typeof meta>;
|
|
40
|
+
|
|
41
|
+
export const Default: Story = {};
|
|
42
|
+
|
|
43
|
+
export const WithActions: Story = { render: WithActionsStory };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type PropsWithChildren, forwardRef } from 'react';
|
|
6
|
+
|
|
7
|
+
import { SystemIconButton, type ThemedClassName } from '@dxos/react-ui';
|
|
8
|
+
import { mx } from '@dxos/ui-theme';
|
|
9
|
+
|
|
10
|
+
//
|
|
11
|
+
// Root
|
|
12
|
+
//
|
|
13
|
+
|
|
14
|
+
type HomeSectionRootProps = ThemedClassName<PropsWithChildren>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Shared container for a Home content section: a centered, max-width column. Home surface
|
|
18
|
+
* contributors (welcome, recent, dashboard, suggestions) render inside one so they share a
|
|
19
|
+
* consistent width and rhythm regardless of which plugin owns them.
|
|
20
|
+
*/
|
|
21
|
+
const HomeSectionRoot = forwardRef<HTMLDivElement, HomeSectionRootProps>(({ classNames, children }, forwardedRef) => (
|
|
22
|
+
<div ref={forwardedRef} className={mx('flex flex-col w-full mx-auto max-w-[40rem]', classNames)}>
|
|
23
|
+
{children}
|
|
24
|
+
</div>
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
HomeSectionRoot.displayName = 'HomeSection.Root';
|
|
28
|
+
|
|
29
|
+
//
|
|
30
|
+
// Header
|
|
31
|
+
//
|
|
32
|
+
|
|
33
|
+
type HomeSectionHeaderProps = ThemedClassName<
|
|
34
|
+
PropsWithChildren<{
|
|
35
|
+
/** Section heading. */
|
|
36
|
+
title?: string;
|
|
37
|
+
/** When provided, renders a close affordance that hides the section. */
|
|
38
|
+
onClose?: () => void;
|
|
39
|
+
}>
|
|
40
|
+
>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Header row for a Home section: a muted title, an optional trailing actions slot (`children`),
|
|
44
|
+
* and an optional close affordance. `children` render before the close button so section-specific
|
|
45
|
+
* controls (e.g. a range selector) sit inline with it.
|
|
46
|
+
*/
|
|
47
|
+
const HomeSectionHeader = forwardRef<HTMLDivElement, HomeSectionHeaderProps>(
|
|
48
|
+
({ title, onClose, classNames, children }, forwardedRef) => (
|
|
49
|
+
<div ref={forwardedRef} className={mx('flex items-center gap-2', classNames)}>
|
|
50
|
+
{title && <h2 className='grow truncate text-sm font-medium text-description'>{title}</h2>}
|
|
51
|
+
{!title && <span className='grow' />}
|
|
52
|
+
{children}
|
|
53
|
+
{onClose && <SystemIconButton.Close variant='ghost' iconOnly onClick={onClose} />}
|
|
54
|
+
</div>
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
HomeSectionHeader.displayName = 'HomeSection.Header';
|
|
59
|
+
|
|
60
|
+
//
|
|
61
|
+
// HomeSection
|
|
62
|
+
//
|
|
63
|
+
|
|
64
|
+
export const HomeSection = {
|
|
65
|
+
Root: HomeSectionRoot,
|
|
66
|
+
Header: HomeSectionHeader,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type { HomeSectionHeaderProps, HomeSectionRootProps };
|