@dxos/app-framework 0.9.0 → 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.
Files changed (208) hide show
  1. package/dist/lib/browser/{capability-BOPAKKWG.mjs → capability-Z4MRP3CP.mjs} +9 -9
  2. package/dist/lib/browser/{chunk-CO3XLNUX.mjs → chunk-37PG7XJC.mjs} +18 -18
  3. package/dist/lib/browser/chunk-37PG7XJC.mjs.map +7 -0
  4. package/dist/lib/browser/{chunk-66IXTIVK.mjs → chunk-3NQLTQCP.mjs} +6 -7
  5. package/dist/lib/browser/chunk-3NQLTQCP.mjs.map +7 -0
  6. package/dist/lib/browser/{chunk-KZUDO43J.mjs → chunk-3UTDRCKX.mjs} +31 -23
  7. package/dist/lib/browser/chunk-3UTDRCKX.mjs.map +7 -0
  8. package/dist/lib/browser/chunk-53HJHMS5.mjs +7 -0
  9. package/dist/lib/browser/chunk-53HJHMS5.mjs.map +7 -0
  10. package/dist/lib/browser/{chunk-IW44C7UL.mjs → chunk-CMINFVA5.mjs} +1 -1
  11. package/dist/lib/browser/chunk-CMINFVA5.mjs.map +7 -0
  12. package/dist/lib/browser/{chunk-Y66ELD4U.mjs → chunk-DFVSOBV2.mjs} +17 -7
  13. package/dist/lib/browser/chunk-DFVSOBV2.mjs.map +7 -0
  14. package/dist/lib/browser/{chunk-DC3WRPBV.mjs → chunk-S2ZBYQPI.mjs} +45 -12
  15. package/dist/lib/browser/chunk-S2ZBYQPI.mjs.map +7 -0
  16. package/dist/lib/browser/{chunk-HFLKXMT7.mjs → chunk-SWECIICH.mjs} +57 -41
  17. package/dist/lib/browser/chunk-SWECIICH.mjs.map +7 -0
  18. package/dist/lib/browser/{chunk-5GY3YOEL.mjs → chunk-VF37YTXJ.mjs} +5 -5
  19. package/dist/lib/browser/chunk-VF37YTXJ.mjs.map +7 -0
  20. package/dist/lib/browser/{chunk-XHS5KDNX.mjs → chunk-W4BHZING.mjs} +25 -25
  21. package/dist/lib/browser/chunk-W4BHZING.mjs.map +7 -0
  22. package/dist/lib/browser/{chunk-FO3IYSLV.mjs → chunk-YUVQIOTB.mjs} +29 -37
  23. package/dist/lib/browser/chunk-YUVQIOTB.mjs.map +7 -0
  24. package/dist/lib/browser/cli/index.mjs +9 -9
  25. package/dist/lib/browser/cli/index.mjs.map +2 -2
  26. package/dist/lib/browser/common/activation-events.mjs +7 -7
  27. package/dist/lib/browser/common/capabilities.mjs +7 -7
  28. package/dist/lib/browser/config/index.mjs +8 -0
  29. package/dist/lib/browser/config/index.mjs.map +7 -0
  30. package/dist/lib/browser/core/activation-event.mjs +1 -1
  31. package/dist/lib/browser/core/capability.mjs +1 -1
  32. package/dist/lib/browser/core/plugin-manager.mjs +4 -4
  33. package/dist/lib/browser/core/plugin.mjs +12 -2
  34. package/dist/lib/browser/core/url-loader.mjs +3 -3
  35. package/dist/lib/browser/index.mjs +14 -10
  36. package/dist/lib/browser/index.mjs.map +1 -1
  37. package/dist/lib/browser/meta.json +1 -1
  38. package/dist/lib/browser/{process-manager-capability-GXNUO7AR.mjs → process-manager-capability-RDROCCQD.mjs} +9 -9
  39. package/dist/lib/browser/testing/index.mjs +15 -15
  40. package/dist/lib/browser/testing/index.mjs.map +3 -3
  41. package/dist/lib/browser/testing/react.mjs +11 -11
  42. package/dist/lib/browser/testing/react.mjs.map +3 -3
  43. package/dist/lib/browser/ui/index.mjs +16 -10
  44. package/dist/lib/node-esm/{capability-KZWRTX7R.mjs → capability-34Z6NPVR.mjs} +9 -9
  45. package/dist/lib/node-esm/chunk-2UPUZXGU.mjs +9 -0
  46. package/dist/lib/node-esm/chunk-2UPUZXGU.mjs.map +7 -0
  47. package/dist/lib/node-esm/{chunk-FSVGKTF6.mjs → chunk-A4O42ZO3.mjs} +17 -7
  48. package/dist/lib/node-esm/chunk-A4O42ZO3.mjs.map +7 -0
  49. package/dist/lib/node-esm/{chunk-CTKEZHKF.mjs → chunk-AIDZMJ3U.mjs} +1 -1
  50. package/dist/lib/node-esm/chunk-AIDZMJ3U.mjs.map +7 -0
  51. package/dist/lib/node-esm/{chunk-XOCUANHO.mjs → chunk-BMW6P6QA.mjs} +6 -7
  52. package/dist/lib/node-esm/chunk-BMW6P6QA.mjs.map +7 -0
  53. package/dist/lib/node-esm/{chunk-A5CHGAZE.mjs → chunk-H7T7XXLS.mjs} +57 -41
  54. package/dist/lib/node-esm/chunk-H7T7XXLS.mjs.map +7 -0
  55. package/dist/lib/node-esm/{chunk-NULBHQYS.mjs → chunk-LUK3FCTH.mjs} +18 -18
  56. package/dist/lib/node-esm/chunk-LUK3FCTH.mjs.map +7 -0
  57. package/dist/lib/node-esm/{chunk-XGBUKRCK.mjs → chunk-TEQSMP2O.mjs} +31 -23
  58. package/dist/lib/node-esm/chunk-TEQSMP2O.mjs.map +7 -0
  59. package/dist/lib/node-esm/{chunk-KFZEB6BV.mjs → chunk-TLKYZKPW.mjs} +5 -5
  60. package/dist/lib/node-esm/chunk-TLKYZKPW.mjs.map +7 -0
  61. package/dist/lib/node-esm/{chunk-WK7OIQKI.mjs → chunk-VCPGX7WN.mjs} +29 -37
  62. package/dist/lib/node-esm/chunk-VCPGX7WN.mjs.map +7 -0
  63. package/dist/lib/node-esm/{chunk-XIGPWXCQ.mjs → chunk-WERVO5EK.mjs} +45 -12
  64. package/dist/lib/node-esm/chunk-WERVO5EK.mjs.map +7 -0
  65. package/dist/lib/node-esm/{chunk-SX5NVETP.mjs → chunk-ZRTG4TSS.mjs} +25 -25
  66. package/dist/lib/node-esm/chunk-ZRTG4TSS.mjs.map +7 -0
  67. package/dist/lib/node-esm/cli/index.mjs +9 -9
  68. package/dist/lib/node-esm/cli/index.mjs.map +2 -2
  69. package/dist/lib/node-esm/common/activation-events.mjs +7 -7
  70. package/dist/lib/node-esm/common/capabilities.mjs +7 -7
  71. package/dist/lib/node-esm/config/index.mjs +9 -0
  72. package/dist/lib/node-esm/config/index.mjs.map +7 -0
  73. package/dist/lib/node-esm/core/activation-event.mjs +1 -1
  74. package/dist/lib/node-esm/core/capability.mjs +1 -1
  75. package/dist/lib/node-esm/core/plugin-manager.mjs +4 -4
  76. package/dist/lib/node-esm/core/plugin.mjs +12 -2
  77. package/dist/lib/node-esm/core/url-loader.mjs +3 -3
  78. package/dist/lib/node-esm/index.mjs +14 -10
  79. package/dist/lib/node-esm/index.mjs.map +1 -1
  80. package/dist/lib/node-esm/meta.json +1 -1
  81. package/dist/lib/node-esm/{process-manager-capability-TX5TA6YN.mjs → process-manager-capability-6OCXSKUW.mjs} +9 -9
  82. package/dist/lib/node-esm/testing/index.mjs +15 -15
  83. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  84. package/dist/lib/node-esm/testing/react.mjs +11 -11
  85. package/dist/lib/node-esm/testing/react.mjs.map +3 -3
  86. package/dist/lib/node-esm/ui/index.mjs +16 -10
  87. package/dist/plugin/node-esm/index.mjs +406 -71
  88. package/dist/plugin/node-esm/index.mjs.map +4 -4
  89. package/dist/plugin/node-esm/meta.json +1 -1
  90. package/dist/types/src/cli/cli.d.ts +1 -1
  91. package/dist/types/src/common/activation-events.d.ts.map +1 -1
  92. package/dist/types/src/common/capabilities.d.ts.map +1 -1
  93. package/dist/types/src/config/index.d.ts +2 -0
  94. package/dist/types/src/config/index.d.ts.map +1 -0
  95. package/dist/types/src/core/activation-event.d.ts +9 -6
  96. package/dist/types/src/core/activation-event.d.ts.map +1 -1
  97. package/dist/types/src/core/capability.d.ts +6 -2
  98. package/dist/types/src/core/capability.d.ts.map +1 -1
  99. package/dist/types/src/core/edge-registry-plugin-provider.d.ts +6 -11
  100. package/dist/types/src/core/edge-registry-plugin-provider.d.ts.map +1 -1
  101. package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
  102. package/dist/types/src/core/plugin-manifest.d.ts +25 -7
  103. package/dist/types/src/core/plugin-manifest.d.ts.map +1 -1
  104. package/dist/types/src/core/plugin.d.ts +81 -91
  105. package/dist/types/src/core/plugin.d.ts.map +1 -1
  106. package/dist/types/src/core/registry.d.ts +17 -57
  107. package/dist/types/src/core/registry.d.ts.map +1 -1
  108. package/dist/types/src/core/url-loader.d.ts +2 -2
  109. package/dist/types/src/core/url-loader.d.ts.map +1 -1
  110. package/dist/types/src/index.d.ts +1 -0
  111. package/dist/types/src/index.d.ts.map +1 -1
  112. package/dist/types/src/plugin-process-manager/meta.d.ts +28 -2
  113. package/dist/types/src/plugin-process-manager/meta.d.ts.map +1 -1
  114. package/dist/types/src/testing/harness.d.ts +1 -1
  115. package/dist/types/src/testing/harness.d.ts.map +1 -1
  116. package/dist/types/src/testing/react.d.ts +6 -5
  117. package/dist/types/src/testing/react.d.ts.map +1 -1
  118. package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
  119. package/dist/types/src/testing/withPluginManager.stories.d.ts.map +1 -1
  120. package/dist/types/src/ui/components/PluginManager/PluginManagerProvider.d.ts +6 -0
  121. package/dist/types/src/ui/components/PluginManager/PluginManagerProvider.d.ts.map +1 -1
  122. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts +6 -10
  123. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts.map +1 -1
  124. package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts.map +1 -1
  125. package/dist/types/src/ui/components/Surface/context.d.ts +6 -2
  126. package/dist/types/src/ui/components/Surface/context.d.ts.map +1 -1
  127. package/dist/types/src/ui/components/Surface/index.d.ts +10 -3
  128. package/dist/types/src/ui/components/Surface/index.d.ts.map +1 -1
  129. package/dist/types/src/ui/components/Surface/types.d.ts +50 -70
  130. package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
  131. package/dist/types/src/ui/hooks/useApp.d.ts +1 -1
  132. package/dist/types/src/ui/hooks/useCapabilities.d.ts +11 -1
  133. package/dist/types/src/ui/hooks/useCapabilities.d.ts.map +1 -1
  134. package/dist/types/src/vite-plugin/composer/index.d.ts +11 -4
  135. package/dist/types/src/vite-plugin/composer/index.d.ts.map +1 -1
  136. package/dist/types/src/vite-plugin/index.d.ts +1 -0
  137. package/dist/types/src/vite-plugin/index.d.ts.map +1 -1
  138. package/dist/types/src/vite-plugin/load.d.ts +16 -0
  139. package/dist/types/src/vite-plugin/load.d.ts.map +1 -0
  140. package/dist/types/src/vite-plugin/manifest.d.ts +16 -4
  141. package/dist/types/src/vite-plugin/manifest.d.ts.map +1 -1
  142. package/dist/types/src/vite-plugin/packages.d.ts.map +1 -1
  143. package/dist/types/tsconfig.tsbuildinfo +1 -1
  144. package/docs/registry-spec.md +331 -0
  145. package/moon.yml +1 -0
  146. package/package.json +32 -27
  147. package/src/cli/cli.ts +2 -2
  148. package/src/common/activation-events.ts +3 -3
  149. package/src/common/capabilities.ts +16 -16
  150. package/src/config/index.ts +5 -0
  151. package/src/core/activation-event.ts +15 -8
  152. package/src/core/capability.ts +15 -4
  153. package/src/core/edge-registry-plugin-provider.ts +41 -42
  154. package/src/core/plugin-manager.test.ts +133 -137
  155. package/src/core/plugin-manager.ts +25 -19
  156. package/src/core/plugin-manifest.test.ts +5 -5
  157. package/src/core/plugin-manifest.ts +13 -10
  158. package/src/core/plugin.ts +58 -114
  159. package/src/core/registry.ts +20 -60
  160. package/src/core/url-loader.test.ts +2 -2
  161. package/src/core/url-loader.ts +23 -19
  162. package/src/index.ts +1 -0
  163. package/src/testing/harness.ts +5 -3
  164. package/src/testing/react.test.tsx +4 -2
  165. package/src/testing/react.tsx +9 -8
  166. package/src/testing/service.ts +2 -2
  167. package/src/testing/withPluginManager.stories.tsx +4 -2
  168. package/src/testing/withPluginManager.tsx +3 -1
  169. package/src/ui/components/PluginManager/PluginManagerProvider.ts +7 -0
  170. package/src/ui/components/Surface/SurfaceComponent.stories.tsx +11 -14
  171. package/src/ui/components/Surface/SurfaceComponent.tsx +84 -74
  172. package/src/ui/components/Surface/context.ts +6 -3
  173. package/src/ui/components/Surface/index.ts +12 -1
  174. package/src/ui/components/Surface/types.test.ts +39 -49
  175. package/src/ui/components/Surface/types.ts +95 -60
  176. package/src/ui/hooks/useApp.test.tsx +1 -1
  177. package/src/ui/hooks/useApp.tsx +2 -2
  178. package/src/ui/hooks/useCapabilities.ts +24 -2
  179. package/src/vite-plugin/composer/index.ts +134 -61
  180. package/src/vite-plugin/index.ts +1 -0
  181. package/src/vite-plugin/load.ts +57 -0
  182. package/src/vite-plugin/manifest.test.ts +3 -3
  183. package/src/vite-plugin/manifest.ts +21 -10
  184. package/src/vite-plugin/packages.ts +0 -1
  185. package/dist/lib/browser/chunk-5GY3YOEL.mjs.map +0 -7
  186. package/dist/lib/browser/chunk-66IXTIVK.mjs.map +0 -7
  187. package/dist/lib/browser/chunk-CO3XLNUX.mjs.map +0 -7
  188. package/dist/lib/browser/chunk-DC3WRPBV.mjs.map +0 -7
  189. package/dist/lib/browser/chunk-FO3IYSLV.mjs.map +0 -7
  190. package/dist/lib/browser/chunk-HFLKXMT7.mjs.map +0 -7
  191. package/dist/lib/browser/chunk-IW44C7UL.mjs.map +0 -7
  192. package/dist/lib/browser/chunk-KZUDO43J.mjs.map +0 -7
  193. package/dist/lib/browser/chunk-XHS5KDNX.mjs.map +0 -7
  194. package/dist/lib/browser/chunk-Y66ELD4U.mjs.map +0 -7
  195. package/dist/lib/node-esm/chunk-A5CHGAZE.mjs.map +0 -7
  196. package/dist/lib/node-esm/chunk-CTKEZHKF.mjs.map +0 -7
  197. package/dist/lib/node-esm/chunk-FSVGKTF6.mjs.map +0 -7
  198. package/dist/lib/node-esm/chunk-KFZEB6BV.mjs.map +0 -7
  199. package/dist/lib/node-esm/chunk-NULBHQYS.mjs.map +0 -7
  200. package/dist/lib/node-esm/chunk-SX5NVETP.mjs.map +0 -7
  201. package/dist/lib/node-esm/chunk-WK7OIQKI.mjs.map +0 -7
  202. package/dist/lib/node-esm/chunk-XGBUKRCK.mjs.map +0 -7
  203. package/dist/lib/node-esm/chunk-XIGPWXCQ.mjs.map +0 -7
  204. package/dist/lib/node-esm/chunk-XOCUANHO.mjs.map +0 -7
  205. /package/dist/lib/browser/{capability-BOPAKKWG.mjs.map → capability-Z4MRP3CP.mjs.map} +0 -0
  206. /package/dist/lib/browser/{process-manager-capability-GXNUO7AR.mjs.map → process-manager-capability-RDROCCQD.mjs.map} +0 -0
  207. /package/dist/lib/node-esm/{capability-KZWRTX7R.mjs.map → capability-34Z6NPVR.mjs.map} +0 -0
  208. /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
- export const makeType = makeTypeFn;
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 carrying the role string', ({ expect }) => {
13
- const token = makeType<{ subject: string }>('test-role');
14
- expect(token.role).toBe('test-role');
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 a = makeType<{ x: number }>('shared');
19
- const b = makeType<{ x: number }>('shared');
20
- expect(a).not.toBe(b);
21
- expect(a.role).toBe(b.role);
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>> = { bindings: [{ role: 'r', guard: () => true }] };
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 filter: SurfaceFilter<Record<string, any>> = {
51
- bindings: [{ role: 'article', guard: (d) => (d as any).subject === 'ok' }],
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: 'article', guard: (d) => (d as any).subject === 'a' },
63
- { role: 'section', guard: (d) => (d as any).subject === 's' },
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(['article', 'section']);
65
+ expect(def.role).toEqual([tokenA.role, tokenB.role]);
68
66
  // Role-specific guard.
69
- expect(def.filter!({ subject: 'a' }, 'article')).toBe(true);
70
- expect(def.filter!({ subject: 'a' }, 'section')).toBe(false);
71
- expect(def.filter!({ subject: 's' }, 'section')).toBe(true);
72
- expect(def.filter!({ subject: 's' }, 'article')).toBe(false);
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 filter: SurfaceFilter<Record<string, any>> = { bindings: [{ role: 'r', guard: () => true }] };
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. The `RoleToken<T>`
88
- // phantom should not impose a runtime constraint.
89
- const token: RoleToken<{ subject: number }> = makeType('numeric');
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, RefCallback } from 'react';
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 string for runtime dispatch plus a phantom
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 validates
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. Identity is structural; all tokens with the same
56
- * `role` string are interchangeable at runtime.
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 = <TData>(role: string): RoleToken<TData> => ({ role });
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
- * Props that are passed to the Surface component.
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
- * The role can be provided either as a string via `role` or as a typed
64
- * {@link RoleToken} via `type`. The typed form narrows `data` to the token's
65
- * declared shape.
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' | 'role'> & {
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
- * Role defines how the data should be rendered. For new code, prefer the
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
- role: string;
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>> = CoreProps<T> & {
150
- ref?: RefCallback<HTMLElement>;
151
- } & Record<string, any>;
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
- if (isSurfaceFilter(definition.filter)) {
264
- const { id, filter, component, position } = definition as TypedReactDefinition<T>;
265
- const { role, guard } = expandBindings(filter);
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: Omit<WebComponentDefinition<T>, 'kind'>,
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
- if (isSurfaceFilter(definition.filter)) {
285
- const { id, filter, tagName, position } = definition as TypedWebComponentDefinition<T>;
286
- const { role, guard } = expandBindings(filter);
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.id === id);
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
  }
@@ -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.id === id);
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 { type Atom, useAtomValue } from '@effect-atom/atom-react';
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.