@dxos/app-framework 0.8.4-staging.60fe92afc8 → 0.9.1-main.c7dcc2e112
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-BOPAKKWG.mjs → capability-Z4MRP3CP.mjs} +9 -9
- package/dist/lib/browser/{chunk-CO3XLNUX.mjs → chunk-37PG7XJC.mjs} +18 -18
- package/dist/lib/browser/chunk-37PG7XJC.mjs.map +7 -0
- package/dist/lib/browser/{chunk-66IXTIVK.mjs → chunk-3NQLTQCP.mjs} +6 -7
- package/dist/lib/browser/chunk-3NQLTQCP.mjs.map +7 -0
- package/dist/lib/browser/{chunk-KZUDO43J.mjs → chunk-3UTDRCKX.mjs} +31 -23
- package/dist/lib/browser/chunk-3UTDRCKX.mjs.map +7 -0
- package/dist/lib/browser/chunk-53HJHMS5.mjs +7 -0
- package/dist/lib/browser/chunk-53HJHMS5.mjs.map +7 -0
- package/dist/lib/browser/{chunk-IW44C7UL.mjs → chunk-CMINFVA5.mjs} +1 -1
- package/dist/lib/browser/chunk-CMINFVA5.mjs.map +7 -0
- package/dist/lib/browser/{chunk-Y66ELD4U.mjs → chunk-DFVSOBV2.mjs} +17 -7
- package/dist/lib/browser/chunk-DFVSOBV2.mjs.map +7 -0
- package/dist/lib/browser/{chunk-DC3WRPBV.mjs → chunk-S2ZBYQPI.mjs} +45 -12
- package/dist/lib/browser/chunk-S2ZBYQPI.mjs.map +7 -0
- package/dist/lib/browser/{chunk-HFLKXMT7.mjs → chunk-SWECIICH.mjs} +57 -41
- package/dist/lib/browser/chunk-SWECIICH.mjs.map +7 -0
- package/dist/lib/browser/{chunk-5GY3YOEL.mjs → chunk-VF37YTXJ.mjs} +5 -5
- package/dist/lib/browser/chunk-VF37YTXJ.mjs.map +7 -0
- package/dist/lib/browser/{chunk-XHS5KDNX.mjs → chunk-W4BHZING.mjs} +25 -25
- package/dist/lib/browser/chunk-W4BHZING.mjs.map +7 -0
- package/dist/lib/browser/{chunk-FO3IYSLV.mjs → chunk-YUVQIOTB.mjs} +29 -37
- package/dist/lib/browser/chunk-YUVQIOTB.mjs.map +7 -0
- package/dist/lib/browser/cli/index.mjs +9 -9
- 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/config/index.mjs +8 -0
- package/dist/lib/browser/config/index.mjs.map +7 -0
- 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 +4 -4
- package/dist/lib/browser/core/plugin.mjs +12 -2
- package/dist/lib/browser/core/url-loader.mjs +3 -3
- package/dist/lib/browser/index.mjs +14 -10
- package/dist/lib/browser/index.mjs.map +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/{process-manager-capability-GXNUO7AR.mjs → process-manager-capability-RDROCCQD.mjs} +9 -9
- package/dist/lib/browser/testing/index.mjs +15 -15
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/testing/react.mjs +11 -11
- package/dist/lib/browser/testing/react.mjs.map +3 -3
- package/dist/lib/browser/ui/index.mjs +16 -10
- package/dist/lib/node-esm/{capability-KZWRTX7R.mjs → capability-34Z6NPVR.mjs} +9 -9
- package/dist/lib/node-esm/chunk-2UPUZXGU.mjs +9 -0
- package/dist/lib/node-esm/chunk-2UPUZXGU.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-FSVGKTF6.mjs → chunk-A4O42ZO3.mjs} +17 -7
- package/dist/lib/node-esm/chunk-A4O42ZO3.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-CTKEZHKF.mjs → chunk-AIDZMJ3U.mjs} +1 -1
- package/dist/lib/node-esm/chunk-AIDZMJ3U.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-XOCUANHO.mjs → chunk-BMW6P6QA.mjs} +6 -7
- package/dist/lib/node-esm/chunk-BMW6P6QA.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-A5CHGAZE.mjs → chunk-H7T7XXLS.mjs} +57 -41
- package/dist/lib/node-esm/chunk-H7T7XXLS.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-NULBHQYS.mjs → chunk-LUK3FCTH.mjs} +18 -18
- package/dist/lib/node-esm/chunk-LUK3FCTH.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-XGBUKRCK.mjs → chunk-TEQSMP2O.mjs} +31 -23
- package/dist/lib/node-esm/chunk-TEQSMP2O.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-KFZEB6BV.mjs → chunk-TLKYZKPW.mjs} +5 -5
- package/dist/lib/node-esm/chunk-TLKYZKPW.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-WK7OIQKI.mjs → chunk-VCPGX7WN.mjs} +29 -37
- package/dist/lib/node-esm/chunk-VCPGX7WN.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-XIGPWXCQ.mjs → chunk-WERVO5EK.mjs} +45 -12
- package/dist/lib/node-esm/chunk-WERVO5EK.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-SX5NVETP.mjs → chunk-ZRTG4TSS.mjs} +25 -25
- package/dist/lib/node-esm/chunk-ZRTG4TSS.mjs.map +7 -0
- package/dist/lib/node-esm/cli/index.mjs +9 -9
- 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/config/index.mjs +9 -0
- package/dist/lib/node-esm/config/index.mjs.map +7 -0
- 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 +4 -4
- package/dist/lib/node-esm/core/plugin.mjs +12 -2
- package/dist/lib/node-esm/core/url-loader.mjs +3 -3
- package/dist/lib/node-esm/index.mjs +14 -10
- package/dist/lib/node-esm/index.mjs.map +1 -1
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/{process-manager-capability-TX5TA6YN.mjs → process-manager-capability-6OCXSKUW.mjs} +9 -9
- package/dist/lib/node-esm/testing/index.mjs +15 -15
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/testing/react.mjs +11 -11
- package/dist/lib/node-esm/testing/react.mjs.map +3 -3
- package/dist/lib/node-esm/ui/index.mjs +16 -10
- package/dist/plugin/node-esm/index.mjs +406 -71
- package/dist/plugin/node-esm/index.mjs.map +4 -4
- package/dist/plugin/node-esm/meta.json +1 -1
- package/dist/types/src/cli/cli.d.ts +1 -1
- package/dist/types/src/common/activation-events.d.ts.map +1 -1
- package/dist/types/src/common/capabilities.d.ts.map +1 -1
- package/dist/types/src/config/index.d.ts +2 -0
- package/dist/types/src/config/index.d.ts.map +1 -0
- package/dist/types/src/core/activation-event.d.ts +9 -6
- package/dist/types/src/core/activation-event.d.ts.map +1 -1
- package/dist/types/src/core/capability.d.ts +6 -2
- package/dist/types/src/core/capability.d.ts.map +1 -1
- package/dist/types/src/core/edge-registry-plugin-provider.d.ts +6 -11
- package/dist/types/src/core/edge-registry-plugin-provider.d.ts.map +1 -1
- package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
- package/dist/types/src/core/plugin-manifest.d.ts +25 -7
- package/dist/types/src/core/plugin-manifest.d.ts.map +1 -1
- package/dist/types/src/core/plugin.d.ts +81 -91
- package/dist/types/src/core/plugin.d.ts.map +1 -1
- package/dist/types/src/core/registry.d.ts +17 -57
- package/dist/types/src/core/registry.d.ts.map +1 -1
- package/dist/types/src/core/url-loader.d.ts +2 -2
- package/dist/types/src/core/url-loader.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/plugin-process-manager/meta.d.ts +28 -2
- package/dist/types/src/plugin-process-manager/meta.d.ts.map +1 -1
- 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/react.d.ts +6 -5
- 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.map +1 -1
- package/dist/types/src/ui/components/PluginManager/PluginManagerProvider.d.ts +6 -0
- package/dist/types/src/ui/components/PluginManager/PluginManagerProvider.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts +6 -10
- 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/context.d.ts +6 -2
- package/dist/types/src/ui/components/Surface/context.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/index.d.ts +10 -3
- package/dist/types/src/ui/components/Surface/index.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/types.d.ts +50 -70
- package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
- package/dist/types/src/ui/hooks/useApp.d.ts +1 -1
- package/dist/types/src/ui/hooks/useCapabilities.d.ts +11 -1
- package/dist/types/src/ui/hooks/useCapabilities.d.ts.map +1 -1
- package/dist/types/src/vite-plugin/composer/index.d.ts +11 -4
- package/dist/types/src/vite-plugin/composer/index.d.ts.map +1 -1
- package/dist/types/src/vite-plugin/index.d.ts +1 -0
- package/dist/types/src/vite-plugin/index.d.ts.map +1 -1
- package/dist/types/src/vite-plugin/load.d.ts +16 -0
- package/dist/types/src/vite-plugin/load.d.ts.map +1 -0
- package/dist/types/src/vite-plugin/manifest.d.ts +16 -4
- package/dist/types/src/vite-plugin/manifest.d.ts.map +1 -1
- package/dist/types/src/vite-plugin/packages.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/docs/registry-spec.md +331 -0
- package/moon.yml +1 -0
- package/package.json +32 -27
- package/src/cli/cli.ts +2 -2
- package/src/common/activation-events.ts +3 -3
- package/src/common/capabilities.ts +16 -16
- package/src/config/index.ts +5 -0
- package/src/core/activation-event.ts +15 -8
- package/src/core/capability.ts +15 -4
- package/src/core/edge-registry-plugin-provider.ts +41 -42
- package/src/core/plugin-manager.test.ts +133 -137
- package/src/core/plugin-manager.ts +25 -19
- package/src/core/plugin-manifest.test.ts +5 -5
- package/src/core/plugin-manifest.ts +13 -10
- package/src/core/plugin.ts +58 -114
- package/src/core/registry.ts +20 -60
- package/src/core/url-loader.test.ts +2 -2
- package/src/core/url-loader.ts +23 -19
- package/src/index.ts +1 -0
- package/src/testing/harness.ts +5 -3
- package/src/testing/react.test.tsx +4 -2
- package/src/testing/react.tsx +9 -8
- package/src/testing/service.ts +2 -2
- package/src/testing/withPluginManager.stories.tsx +4 -2
- package/src/testing/withPluginManager.tsx +3 -1
- package/src/ui/components/PluginManager/PluginManagerProvider.ts +7 -0
- package/src/ui/components/Surface/SurfaceComponent.stories.tsx +11 -14
- package/src/ui/components/Surface/SurfaceComponent.tsx +84 -74
- package/src/ui/components/Surface/context.ts +6 -3
- package/src/ui/components/Surface/index.ts +12 -1
- package/src/ui/components/Surface/types.test.ts +39 -49
- package/src/ui/components/Surface/types.ts +95 -60
- package/src/ui/hooks/useApp.test.tsx +1 -1
- package/src/ui/hooks/useApp.tsx +2 -2
- package/src/ui/hooks/useCapabilities.ts +24 -2
- package/src/vite-plugin/composer/index.ts +134 -61
- package/src/vite-plugin/index.ts +1 -0
- package/src/vite-plugin/load.ts +57 -0
- package/src/vite-plugin/manifest.test.ts +3 -3
- package/src/vite-plugin/manifest.ts +21 -10
- package/src/vite-plugin/packages.ts +0 -1
- package/dist/lib/browser/chunk-5GY3YOEL.mjs.map +0 -7
- package/dist/lib/browser/chunk-66IXTIVK.mjs.map +0 -7
- package/dist/lib/browser/chunk-CO3XLNUX.mjs.map +0 -7
- package/dist/lib/browser/chunk-DC3WRPBV.mjs.map +0 -7
- package/dist/lib/browser/chunk-FO3IYSLV.mjs.map +0 -7
- package/dist/lib/browser/chunk-HFLKXMT7.mjs.map +0 -7
- package/dist/lib/browser/chunk-IW44C7UL.mjs.map +0 -7
- package/dist/lib/browser/chunk-KZUDO43J.mjs.map +0 -7
- package/dist/lib/browser/chunk-XHS5KDNX.mjs.map +0 -7
- package/dist/lib/browser/chunk-Y66ELD4U.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-A5CHGAZE.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-CTKEZHKF.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-FSVGKTF6.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-KFZEB6BV.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-NULBHQYS.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-SX5NVETP.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-WK7OIQKI.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-XGBUKRCK.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-XIGPWXCQ.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-XOCUANHO.mjs.map +0 -7
- /package/dist/lib/browser/{capability-BOPAKKWG.mjs.map → capability-Z4MRP3CP.mjs.map} +0 -0
- /package/dist/lib/browser/{process-manager-capability-GXNUO7AR.mjs.map → process-manager-capability-RDROCCQD.mjs.map} +0 -0
- /package/dist/lib/node-esm/{capability-KZWRTX7R.mjs.map → capability-34Z6NPVR.mjs.map} +0 -0
- /package/dist/lib/node-esm/{process-manager-capability-TX5TA6YN.mjs.map → process-manager-capability-6OCXSKUW.mjs.map} +0 -0
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import type { DXN } from '@dxos/keys';
|
|
6
|
+
|
|
5
7
|
// TODO(wittjosiah): Cleanup to avoid re-naming.
|
|
6
8
|
import { SurfaceContext } from './context';
|
|
7
9
|
import { SurfaceComponent, isSurfaceAvailable } from './SurfaceComponent';
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
create as createSurface,
|
|
23
25
|
createWeb as createWebSurface,
|
|
24
26
|
isSurfaceFilter as isSurfaceFilterFn,
|
|
27
|
+
makeFilter as makeFilterFn,
|
|
25
28
|
makeType as makeTypeFn,
|
|
26
29
|
} from './types';
|
|
27
30
|
|
|
@@ -41,7 +44,15 @@ export namespace Surface {
|
|
|
41
44
|
export type Filter<TData> = SurfaceFilterType<TData>;
|
|
42
45
|
export type TokenData<T> = SurfaceTokenData<T>;
|
|
43
46
|
export type TypedProps<TToken extends SurfaceRoleToken<any>> = SurfaceTypedProps<TToken>;
|
|
44
|
-
|
|
47
|
+
|
|
48
|
+
/** Mints a typed role token. The NSID is validated at compile time via {@link DXN.Name}. */
|
|
49
|
+
export const makeType: {
|
|
50
|
+
<TData = unknown, T extends string = string>(
|
|
51
|
+
nsid: [DXN.Name<T>] extends [never] ? `Invalid NSID "${T}": final segment must be camelCase (no hyphens)` : T,
|
|
52
|
+
): SurfaceRoleToken<TData>;
|
|
53
|
+
} = makeTypeFn;
|
|
54
|
+
|
|
55
|
+
export const makeFilter = makeFilterFn;
|
|
45
56
|
export const isFilter = isSurfaceFilterFn;
|
|
46
57
|
|
|
47
58
|
export const ProfilerProvider = SurfaceProfilerProvider;
|
|
@@ -6,25 +6,35 @@ import { describe, test } from 'vitest';
|
|
|
6
6
|
|
|
7
7
|
import { type CapabilityManager } from '../../../core';
|
|
8
8
|
import { isSurfaceAvailable } from './SurfaceComponent';
|
|
9
|
-
import { type RoleToken, type SurfaceFilter, create, isSurfaceFilter, makeType } from './types';
|
|
9
|
+
import { type RoleToken, type SurfaceFilter, create, isSurfaceFilter, makeFilter, makeType } from './types';
|
|
10
10
|
|
|
11
11
|
describe('Surface.makeType', () => {
|
|
12
|
-
test('creates a role token
|
|
13
|
-
const token = makeType<{ subject: string }>('test
|
|
14
|
-
expect(token.role).toBe('test
|
|
12
|
+
test('creates a role token with the given NSID', ({ expect }) => {
|
|
13
|
+
const token = makeType<{ subject: string }>('org.dxos.test.role.testRole');
|
|
14
|
+
expect(token.role).toBe('org.dxos.test.role.testRole');
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
test('tokens with the same role are independent objects (identity-by-role)', ({ expect }) => {
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
expect(
|
|
21
|
-
expect(
|
|
18
|
+
const tokenOne = makeType<{ x: number }>('org.dxos.test.role.shared');
|
|
19
|
+
const tokenTwo = makeType<{ x: number }>('org.dxos.test.role.shared');
|
|
20
|
+
expect(tokenOne).not.toBe(tokenTwo);
|
|
21
|
+
expect(tokenOne.role).toBe(tokenTwo.role);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('throws on invalid NSID at runtime (hyphenated final segment)', ({ expect }) => {
|
|
25
|
+
expect(() => makeType('org.dxos.test.role.bad-name' as any)).toThrow();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('throws on single-segment NSID', ({ expect }) => {
|
|
29
|
+
expect(() => makeType('article' as any)).toThrow();
|
|
22
30
|
});
|
|
23
31
|
});
|
|
24
32
|
|
|
25
33
|
describe('isSurfaceFilter', () => {
|
|
26
34
|
test('distinguishes filter objects from predicate functions', ({ expect }) => {
|
|
27
|
-
const filter: SurfaceFilter<Record<string, any>> = {
|
|
35
|
+
const filter: SurfaceFilter<Record<string, any>> = {
|
|
36
|
+
bindings: [{ role: 'org.dxos.test.role.r', guard: () => true }],
|
|
37
|
+
};
|
|
28
38
|
expect(isSurfaceFilter(filter)).toBe(true);
|
|
29
39
|
expect(isSurfaceFilter(() => true)).toBe(false);
|
|
30
40
|
expect(isSurfaceFilter({})).toBe(false);
|
|
@@ -33,50 +43,39 @@ describe('isSurfaceFilter', () => {
|
|
|
33
43
|
});
|
|
34
44
|
|
|
35
45
|
describe('create', () => {
|
|
36
|
-
test('accepts the legacy { role, filter } shape', ({ expect }) => {
|
|
37
|
-
const def = create({
|
|
38
|
-
id: 'legacy',
|
|
39
|
-
role: 'article',
|
|
40
|
-
filter: (data): data is { x: number } => typeof (data as any).x === 'number',
|
|
41
|
-
component: () => null,
|
|
42
|
-
});
|
|
43
|
-
expect(def.kind).toBe('react');
|
|
44
|
-
expect(def.role).toBe('article');
|
|
45
|
-
expect(def.filter!({ x: 1 })).toBe(true);
|
|
46
|
-
expect(def.filter!({})).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
46
|
test('expands a single-binding SurfaceFilter into a role string', ({ expect }) => {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
};
|
|
47
|
+
const token = makeType<Record<string, any>>('org.dxos.test.role.article');
|
|
48
|
+
const filter = makeFilter(token, (data) => data.subject === 'ok');
|
|
53
49
|
const def = create({ id: 'typedSingle', filter, component: () => null });
|
|
54
|
-
expect(def.role).toBe('article');
|
|
55
|
-
expect(def.filter!({ subject: 'ok' }, 'article')).toBe(true);
|
|
56
|
-
expect(def.filter!({ subject: 'no' }, 'article')).toBe(false);
|
|
50
|
+
expect(def.role).toBe('org.dxos.test.role.article');
|
|
51
|
+
expect(def.filter!({ subject: 'ok' }, 'org.dxos.test.role.article')).toBe(true);
|
|
52
|
+
expect(def.filter!({ subject: 'no' }, 'org.dxos.test.role.article')).toBe(false);
|
|
57
53
|
});
|
|
58
54
|
|
|
59
55
|
test('expands a multi-binding SurfaceFilter into a role array with role-scoped guards', ({ expect }) => {
|
|
56
|
+
const tokenA = makeType<Record<string, any>>('org.dxos.test.role.article');
|
|
57
|
+
const tokenB = makeType<Record<string, any>>('org.dxos.test.role.section');
|
|
60
58
|
const filter: SurfaceFilter<Record<string, any>> = {
|
|
61
59
|
bindings: [
|
|
62
|
-
{ role:
|
|
63
|
-
{ role:
|
|
60
|
+
{ role: tokenA.role, guard: (data) => (data as any).subject === 'a' },
|
|
61
|
+
{ role: tokenB.role, guard: (data) => (data as any).subject === 's' },
|
|
64
62
|
],
|
|
65
63
|
};
|
|
66
64
|
const def = create({ id: 'typedMulti', filter, component: () => null });
|
|
67
|
-
expect(def.role).toEqual([
|
|
65
|
+
expect(def.role).toEqual([tokenA.role, tokenB.role]);
|
|
68
66
|
// Role-specific guard.
|
|
69
|
-
expect(def.filter!({ subject: 'a' },
|
|
70
|
-
expect(def.filter!({ subject: 'a' },
|
|
71
|
-
expect(def.filter!({ subject: 's' },
|
|
72
|
-
expect(def.filter!({ subject: 's' },
|
|
67
|
+
expect(def.filter!({ subject: 'a' }, tokenA.role)).toBe(true);
|
|
68
|
+
expect(def.filter!({ subject: 'a' }, tokenB.role)).toBe(false);
|
|
69
|
+
expect(def.filter!({ subject: 's' }, tokenB.role)).toBe(true);
|
|
70
|
+
expect(def.filter!({ subject: 's' }, tokenA.role)).toBe(false);
|
|
73
71
|
// Without role, any binding may match.
|
|
74
72
|
expect(def.filter!({ subject: 'a' })).toBe(true);
|
|
75
73
|
expect(def.filter!({ subject: 'unknown' })).toBe(false);
|
|
76
74
|
});
|
|
77
75
|
|
|
78
76
|
test('passes position through untouched', ({ expect }) => {
|
|
79
|
-
const
|
|
77
|
+
const token = makeType<Record<string, any>>('org.dxos.test.role.r');
|
|
78
|
+
const filter: SurfaceFilter<Record<string, any>> = { bindings: [{ role: token.role, guard: () => true }] };
|
|
80
79
|
const def = create({ id: 'pos', filter, component: () => null, position: 'last' });
|
|
81
80
|
expect(def.position).toBe('last');
|
|
82
81
|
});
|
|
@@ -84,10 +83,9 @@ describe('create', () => {
|
|
|
84
83
|
|
|
85
84
|
describe('role token typing', () => {
|
|
86
85
|
test('makeType preserves TData through token use-sites', ({ expect }) => {
|
|
87
|
-
// Type-level smoke test: if TS compiles, we're fine.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
expect(token.role).toBe('numeric');
|
|
86
|
+
// Type-level smoke test: if TS compiles, we're fine.
|
|
87
|
+
const token: RoleToken<{ subject: number }> = makeType('org.dxos.test.role.numeric');
|
|
88
|
+
expect(token.role).toBe('org.dxos.test.role.numeric');
|
|
91
89
|
});
|
|
92
90
|
});
|
|
93
91
|
|
|
@@ -95,7 +93,7 @@ describe('isSurfaceAvailable typing', () => {
|
|
|
95
93
|
// These tests double as static assertions: the `@ts-expect-error` comments
|
|
96
94
|
// fail to compile if the surrounding expression typechecks, so they verify
|
|
97
95
|
// the typed overload narrows `data` to the token's declared contract.
|
|
98
|
-
const sectionToken = makeType<{ attendableId: string; subject: string }>('section');
|
|
96
|
+
const sectionToken = makeType<{ attendableId: string; subject: string }>('org.dxos.test.role.section');
|
|
99
97
|
const capabilityManager = { getAll: () => [] } as unknown as CapabilityManager.CapabilityManager;
|
|
100
98
|
|
|
101
99
|
test('typed overload accepts data matching the token contract', () => {
|
|
@@ -115,12 +113,4 @@ describe('isSurfaceAvailable typing', () => {
|
|
|
115
113
|
// @ts-expect-error — `attendableId` must be a string, not a number.
|
|
116
114
|
isSurfaceAvailable(capabilityManager, { type: sectionToken, data: { attendableId: 123, subject: 'x' } });
|
|
117
115
|
});
|
|
118
|
-
|
|
119
|
-
test('legacy overload accepts loose data when `role` is a string', () => {
|
|
120
|
-
// No error — legacy overload's `data` is untyped (`Record<string, unknown>`).
|
|
121
|
-
isSurfaceAvailable(capabilityManager, {
|
|
122
|
-
role: 'article',
|
|
123
|
-
data: { anything: 'goes' },
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
116
|
});
|
|
@@ -2,16 +2,20 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import type { FC, PropsWithChildren, ReactNode,
|
|
5
|
+
import type { FC, PropsWithChildren, ReactNode, Ref } from 'react';
|
|
6
6
|
|
|
7
|
+
import type { DXN } from '@dxos/keys';
|
|
8
|
+
import { log } from '@dxos/log';
|
|
7
9
|
import type { MakeOptional, Position } from '@dxos/util';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
|
-
* A typed role token. Carries a role
|
|
12
|
+
* A typed role token. Carries a role NSID for runtime dispatch plus a phantom
|
|
11
13
|
* data type so consumers using `type={SomeToken}` and providers using
|
|
12
14
|
* `AppSurface.object(SomeToken, ...)` share a single type-level contract.
|
|
13
15
|
*
|
|
14
|
-
* Mint via {@link makeType}.
|
|
16
|
+
* Mint via {@link makeType}. The `role` field holds the bare NSID (e.g.
|
|
17
|
+
* `org.dxos.role.article`) — the dispatch key, `data-role` attribute value,
|
|
18
|
+
* and what `.role` comparisons inside matched components see.
|
|
15
19
|
*/
|
|
16
20
|
export type RoleToken<TData> = {
|
|
17
21
|
readonly role: string;
|
|
@@ -20,8 +24,8 @@ export type RoleToken<TData> = {
|
|
|
20
24
|
};
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
|
-
* One entry in a {@link SurfaceFilter} — a role plus the guard that
|
|
24
|
-
* the data shape when the Surface dispatcher is matching that role.
|
|
27
|
+
* One entry in a {@link SurfaceFilter} — a role NSID plus the guard that
|
|
28
|
+
* validates the data shape when the Surface dispatcher is matching that role.
|
|
25
29
|
*/
|
|
26
30
|
export type SurfaceBinding = {
|
|
27
31
|
readonly role: string;
|
|
@@ -52,17 +56,61 @@ export const isSurfaceFilter = (value: unknown): value is SurfaceFilter<any> =>
|
|
|
52
56
|
typeof value === 'object' && value !== null && Array.isArray((value as { bindings?: unknown }).bindings);
|
|
53
57
|
|
|
54
58
|
/**
|
|
55
|
-
* Mints a typed role token.
|
|
56
|
-
*
|
|
59
|
+
* Mints a typed role token. The NSID is validated at compile time via
|
|
60
|
+
* {@link DXN.Name}: the final segment must be camelCase (no hyphens).
|
|
61
|
+
* Dynamic NSIDs (template-literal / runtime strings) are validated at runtime.
|
|
62
|
+
*
|
|
63
|
+
* Identity is structural; all tokens with the same `role` string are
|
|
64
|
+
* interchangeable at runtime.
|
|
57
65
|
*/
|
|
58
|
-
export const makeType
|
|
66
|
+
export const makeType: {
|
|
67
|
+
<TData = unknown, T extends string = string>(
|
|
68
|
+
nsid: [DXN.Name<T>] extends [never] ? `Invalid NSID "${T}": final segment must be camelCase (no hyphens)` : T,
|
|
69
|
+
): RoleToken<TData>;
|
|
70
|
+
} = <TData>(nsid: string): RoleToken<TData> => {
|
|
71
|
+
// Runtime guard for dynamic NSIDs that bypass the compile-time DXN.Name<T> check.
|
|
72
|
+
if (
|
|
73
|
+
typeof nsid !== 'string' ||
|
|
74
|
+
!/^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z][a-zA-Z0-9]{0,62})$/.test(
|
|
75
|
+
nsid,
|
|
76
|
+
)
|
|
77
|
+
) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Invalid role NSID: "${nsid}". Must be a dotted name with a camelCase final segment (no hyphens in the last segment).`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return { role: nsid };
|
|
83
|
+
};
|
|
59
84
|
|
|
60
85
|
/**
|
|
61
|
-
*
|
|
86
|
+
* Creates a {@link SurfaceFilter} from a role token and an optional guard.
|
|
87
|
+
*
|
|
88
|
+
* When `guard` is omitted the filter matches any data at the token's role
|
|
89
|
+
* (role-only dispatch). Pass a guard to add runtime data-shape validation on
|
|
90
|
+
* top of the role match.
|
|
62
91
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
|
|
92
|
+
* This is the framework-level primitive; `@dxos/app-toolkit` builds richer
|
|
93
|
+
* domain-aware helpers (ECHO schema checks, literal matching, etc.) on top of it.
|
|
94
|
+
*/
|
|
95
|
+
export const makeFilter = <TData>(token: RoleToken<TData>, guard?: (data: TData) => boolean): SurfaceFilter<TData> => {
|
|
96
|
+
const boundGuard =
|
|
97
|
+
guard == null
|
|
98
|
+
? () => true
|
|
99
|
+
: (data: unknown): boolean => {
|
|
100
|
+
try {
|
|
101
|
+
return guard(data as TData);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
log.catch(err);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
return { bindings: [{ role: token.role, guard: boundGuard }] };
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Typed Surface consumer props — carries the role/data-shape contract via a
|
|
112
|
+
* {@link RoleToken}. The `type` prop is required; the string `role` prop has
|
|
113
|
+
* been removed from the consumer-facing API.
|
|
66
114
|
*/
|
|
67
115
|
export type Props<T extends Record<string, any> = Record<string, unknown>> = {
|
|
68
116
|
/**
|
|
@@ -76,22 +124,7 @@ export type Props<T extends Record<string, any> = Record<string, unknown>> = {
|
|
|
76
124
|
* The placeholder component will be rendered while the surface component is loading.
|
|
77
125
|
*/
|
|
78
126
|
placeholder?: ReactNode;
|
|
79
|
-
} & MakeOptional<CoreProps<T>, 'id' | 'data'
|
|
80
|
-
/**
|
|
81
|
-
* Explicitly disallow `type` on the untyped Props overload — if the caller
|
|
82
|
-
* provides `type={AppSurface.X}`, TypeScript must route to the typed
|
|
83
|
-
* overload (where `data` is narrowed by the token). Without this, the
|
|
84
|
-
* catch-all index signature below would accept any `type` value and mask
|
|
85
|
-
* the typed overload entirely.
|
|
86
|
-
*/
|
|
87
|
-
type?: undefined;
|
|
88
|
-
} /**
|
|
89
|
-
* Additional props to pass to the component.
|
|
90
|
-
* These props are not used by Surface itself but may be used by components which resolve the surface.
|
|
91
|
-
* Exclude known prop names to prevent overriding well-defined props.
|
|
92
|
-
*/ & {
|
|
93
|
-
[K in keyof Record<string, any>]: K extends keyof CoreProps<T> | 'fallback' | 'placeholder' | 'type' ? never : any;
|
|
94
|
-
};
|
|
127
|
+
} & MakeOptional<CoreProps<T>, 'id' | 'data'>;
|
|
95
128
|
|
|
96
129
|
/**
|
|
97
130
|
* Typed Surface consumer props — carries the role/data-shape contract via a
|
|
@@ -118,6 +151,8 @@ export type TypedProps<TToken extends RoleToken<any>> = {
|
|
|
118
151
|
|
|
119
152
|
/**
|
|
120
153
|
* NOTE: If `[key: string]: unknown` is included in shared types, when re-used other fields become unknown as well.
|
|
154
|
+
* The `type` prop is the only way to set the role; it is required here and
|
|
155
|
+
* optional in {@link Props} via MakeOptional.
|
|
121
156
|
*/
|
|
122
157
|
export type CoreProps<T extends Record<string, any> = Record<string, unknown>> = PropsWithChildren<{
|
|
123
158
|
/**
|
|
@@ -126,12 +161,9 @@ export type CoreProps<T extends Record<string, any> = Record<string, unknown>> =
|
|
|
126
161
|
id: string;
|
|
127
162
|
|
|
128
163
|
/**
|
|
129
|
-
*
|
|
130
|
-
* typed overload of the Surface component which takes a {@link RoleToken}
|
|
131
|
-
* via the `type` prop instead — it enforces a role/data contract at compile
|
|
132
|
-
* time.
|
|
164
|
+
* The role token defining how the data should be rendered.
|
|
133
165
|
*/
|
|
134
|
-
|
|
166
|
+
type: RoleToken<T>;
|
|
135
167
|
|
|
136
168
|
/**
|
|
137
169
|
* The data to be rendered by the surface.
|
|
@@ -146,9 +178,20 @@ export type CoreProps<T extends Record<string, any> = Record<string, unknown>> =
|
|
|
146
178
|
}>;
|
|
147
179
|
|
|
148
180
|
// TODO(burdon): Remove ref since relying on this would be error prone.
|
|
149
|
-
export type ComponentProps<T extends Record<string, any> = Record<string, any>> =
|
|
150
|
-
|
|
151
|
-
|
|
181
|
+
export type ComponentProps<T extends Record<string, any> = Record<string, any>> = {
|
|
182
|
+
/**
|
|
183
|
+
* The resolved role NSID string (e.g. `org.dxos.role.article`). Present on
|
|
184
|
+
* matched components only — multi-role containers can branch on this value
|
|
185
|
+
* by comparing against a token's `.role` (e.g. `role === AppSurface.Section.role`).
|
|
186
|
+
*/
|
|
187
|
+
role: string;
|
|
188
|
+
ref?: Ref<HTMLElement>;
|
|
189
|
+
} & PropsWithChildren<{
|
|
190
|
+
id: string;
|
|
191
|
+
data: T;
|
|
192
|
+
limit?: number | undefined;
|
|
193
|
+
}> &
|
|
194
|
+
Record<string, any>;
|
|
152
195
|
|
|
153
196
|
/**
|
|
154
197
|
* React component used to render a surface once is has matched.
|
|
@@ -163,6 +206,9 @@ export type ComponentFunction<T extends Record<string, any> = Record<string, any
|
|
|
163
206
|
* The optional `filter` receives the consumer-supplied `role` as its second
|
|
164
207
|
* argument so a single definition can encode role-specific guards (used by the
|
|
165
208
|
* {@link SurfaceFilter}-based `create` overload).
|
|
209
|
+
*
|
|
210
|
+
* @internal The `role` field is populated by the framework from the filter
|
|
211
|
+
* bindings and is not authored directly.
|
|
166
212
|
*/
|
|
167
213
|
export type ReactDefinition<T extends Record<string, any> = any> = Readonly<{
|
|
168
214
|
kind: 'react';
|
|
@@ -175,6 +221,9 @@ export type ReactDefinition<T extends Record<string, any> = any> = Readonly<{
|
|
|
175
221
|
|
|
176
222
|
/**
|
|
177
223
|
* Definition of when a Web Component surface should be rendered.
|
|
224
|
+
*
|
|
225
|
+
* @internal The `role` field is populated by the framework from the filter
|
|
226
|
+
* bindings and is not authored directly.
|
|
178
227
|
*/
|
|
179
228
|
export type WebComponentDefinition<T extends Record<string, any> = any> = Readonly<{
|
|
180
229
|
kind: 'web-component';
|
|
@@ -250,41 +299,27 @@ const validateLocalId = (id: string): void => {
|
|
|
250
299
|
};
|
|
251
300
|
|
|
252
301
|
/**
|
|
253
|
-
* Creates a React surface definition.
|
|
302
|
+
* Creates a React surface definition from a typed filter.
|
|
254
303
|
*/
|
|
255
304
|
export function create<T extends Record<string, any> = any>(definition: TypedReactDefinition<T>): ReactDefinition<T>;
|
|
256
|
-
export function create<T extends Record<string, any> = any>(
|
|
257
|
-
definition: Omit<ReactDefinition<T>, 'kind'>,
|
|
258
|
-
): ReactDefinition<T>;
|
|
259
|
-
export function create<T extends Record<string, any> = any>(
|
|
260
|
-
definition: TypedReactDefinition<T> | Omit<ReactDefinition<T>, 'kind'>,
|
|
261
|
-
): ReactDefinition<T> {
|
|
305
|
+
export function create<T extends Record<string, any> = any>(definition: TypedReactDefinition<T>): ReactDefinition<T> {
|
|
262
306
|
validateLocalId(definition.id);
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return { kind: 'react', id, role, position, component, filter: guard };
|
|
267
|
-
}
|
|
268
|
-
return { ...(definition as Omit<ReactDefinition<T>, 'kind'>), kind: 'react' };
|
|
307
|
+
const { id, filter, component, position } = definition;
|
|
308
|
+
const { role, guard } = expandBindings(filter);
|
|
309
|
+
return { kind: 'react', id, role, position, component, filter: guard };
|
|
269
310
|
}
|
|
270
311
|
|
|
271
312
|
/**
|
|
272
|
-
* Creates a Web Component surface definition.
|
|
313
|
+
* Creates a Web Component surface definition from a typed filter.
|
|
273
314
|
*/
|
|
274
315
|
export function createWeb<T extends Record<string, any> = any>(
|
|
275
316
|
definition: TypedWebComponentDefinition<T>,
|
|
276
317
|
): WebComponentDefinition<T>;
|
|
277
318
|
export function createWeb<T extends Record<string, any> = any>(
|
|
278
|
-
definition:
|
|
279
|
-
): WebComponentDefinition<T>;
|
|
280
|
-
export function createWeb<T extends Record<string, any> = any>(
|
|
281
|
-
definition: TypedWebComponentDefinition<T> | Omit<WebComponentDefinition<T>, 'kind'>,
|
|
319
|
+
definition: TypedWebComponentDefinition<T>,
|
|
282
320
|
): WebComponentDefinition<T> {
|
|
283
321
|
validateLocalId(definition.id);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
return { kind: 'web-component', id, role, position, tagName, filter: guard };
|
|
288
|
-
}
|
|
289
|
-
return { ...(definition as Omit<WebComponentDefinition<T>, 'kind'>), kind: 'web-component' };
|
|
322
|
+
const { id, filter, tagName, position } = definition;
|
|
323
|
+
const { role, guard } = expandBindings(filter);
|
|
324
|
+
return { kind: 'web-component', id, role, position, tagName, filter: guard };
|
|
290
325
|
}
|
|
@@ -18,7 +18,7 @@ const testMeta = Plugin.makeMeta({ key: DXN.make('org.dxos.plugin.test'), name:
|
|
|
18
18
|
|
|
19
19
|
const pluginLoader = (plugins: Plugin.Plugin[]) =>
|
|
20
20
|
Effect.fn(function* (id: string) {
|
|
21
|
-
const plugin = plugins.find((plugin) => plugin.meta.
|
|
21
|
+
const plugin = plugins.find((plugin) => plugin.meta.profile.key === id);
|
|
22
22
|
if (!plugin) {
|
|
23
23
|
return yield* Effect.fail(new Error(`Plugin not found: ${id}`));
|
|
24
24
|
}
|
package/src/ui/hooks/useApp.tsx
CHANGED
|
@@ -94,7 +94,7 @@ export type UseAppOptions = {
|
|
|
94
94
|
* );
|
|
95
95
|
*
|
|
96
96
|
* @param params.pluginLoader A function which loads new plugins.
|
|
97
|
-
* @param params.plugins All plugins available to the application. Plugins whose `meta.tags` includes `'system'` are treated as core (force-enabled, not user-toggleable).
|
|
97
|
+
* @param params.plugins All plugins available to the application. Plugins whose `meta.profile.tags` includes `'system'` are treated as core (force-enabled, not user-toggleable).
|
|
98
98
|
* @param params.defaults Default plugins are enabled by default but can be disabled by the user.
|
|
99
99
|
* @param params.cacheEnabled Whether to cache enabled plugins in localStorage.
|
|
100
100
|
* @param params.safeMode Whether to enable safe mode, which disables optional plugins.
|
|
@@ -123,7 +123,7 @@ export const useApp = ({
|
|
|
123
123
|
pluginLoaderProp ??
|
|
124
124
|
((id: string) =>
|
|
125
125
|
Effect.sync(() => {
|
|
126
|
-
const plugin = plugins.find((plugin) => plugin.meta.
|
|
126
|
+
const plugin = plugins.find((plugin) => plugin.meta.profile.key === id);
|
|
127
127
|
invariant(plugin, `Plugin not found: ${id}`);
|
|
128
128
|
return { plugin };
|
|
129
129
|
})),
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Atom } from '@effect-atom/atom';
|
|
6
|
+
import { useAtomValue } from '@effect-atom/atom-react';
|
|
6
7
|
import { useCallback } from 'react';
|
|
7
8
|
|
|
8
9
|
import { invariant } from '@dxos/invariant';
|
|
9
10
|
|
|
10
11
|
import { Capabilities } from '../../common';
|
|
11
12
|
import { type Capability } from '../../core';
|
|
12
|
-
import { usePluginManager } from '../components';
|
|
13
|
+
import { useOptionalPluginManager, usePluginManager } from '../components';
|
|
14
|
+
|
|
15
|
+
/** Stable empty result for capability lookups made outside a plugin manager. */
|
|
16
|
+
const emptyCapabilities = Atom.make(() => [] as const);
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* Hook to request capabilities from the plugin context.
|
|
@@ -32,6 +36,24 @@ export const useCapability = <T>(interfaceDef: Capability.InterfaceDef<T>) => {
|
|
|
32
36
|
return capabilities[0];
|
|
33
37
|
};
|
|
34
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Hook to request capabilities without requiring a plugin manager.
|
|
41
|
+
* @returns An array of capabilities, or an empty array when rendered outside a {@link PluginManagerProvider}.
|
|
42
|
+
*/
|
|
43
|
+
export const useOptionalCapabilities = <T>(interfaceDef: Capability.InterfaceDef<T>): readonly T[] => {
|
|
44
|
+
const manager = useOptionalPluginManager();
|
|
45
|
+
return useAtomValue(
|
|
46
|
+
manager ? manager.capabilities.atom(interfaceDef) : (emptyCapabilities as Atom.Atom<readonly T[]>),
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Hook to request a single capability without requiring a plugin manager.
|
|
52
|
+
* @returns The first matching capability, or `undefined` when none is registered (or there is no plugin manager).
|
|
53
|
+
*/
|
|
54
|
+
export const useOptionalCapability = <T>(interfaceDef: Capability.InterfaceDef<T>): T | undefined =>
|
|
55
|
+
useOptionalCapabilities(interfaceDef)[0];
|
|
56
|
+
|
|
35
57
|
/**
|
|
36
58
|
* Hook to get the current value of an atom capability.
|
|
37
59
|
* Automatically subscribes to changes.
|