@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
@@ -59,7 +59,7 @@ export type PreloadOptions = Options & {
59
59
  *
60
60
  * `url` is the URL of the plugin manifest (`plugin.json`), not the entry module.
61
61
  */
62
- export type RemotePluginEntry = {
62
+ export type RemotePluginView = {
63
63
  id: string;
64
64
  url: string;
65
65
  /** Installed version string, e.g. `v1.0.0`. Populated after install from a community catalog entry. */
@@ -71,22 +71,26 @@ const defaultStorage = (): Storage => ({
71
71
  set: (key, value) => localStorage.setItem(key, value),
72
72
  });
73
73
 
74
- const getPersistedRemotePlugins = (storage: Storage, key: string): RemotePluginEntry[] => {
74
+ const getPersistedRemotePlugins = (storage: Storage, key: string): RemotePluginView[] => {
75
75
  try {
76
76
  const parsed: unknown = JSON.parse(storage.get(key) ?? '[]');
77
77
  if (!Array.isArray(parsed)) {
78
78
  return [];
79
79
  }
80
80
  return parsed.filter(
81
- (entry): entry is RemotePluginEntry =>
82
- typeof entry === 'object' && entry !== null && typeof entry.id === 'string' && typeof entry.url === 'string',
81
+ (entry): entry is RemotePluginView =>
82
+ typeof entry === 'object' &&
83
+ entry !== null &&
84
+ typeof entry.id === 'string' &&
85
+ typeof entry.url === 'string' &&
86
+ (entry.version === undefined || typeof entry.version === 'string'),
83
87
  );
84
88
  } catch {
85
89
  return [];
86
90
  }
87
91
  };
88
92
 
89
- const persistRemotePlugin = (storage: Storage, key: string, entry: RemotePluginEntry): void => {
93
+ const persistRemotePlugin = (storage: Storage, key: string, entry: RemotePluginView): void => {
90
94
  try {
91
95
  const entries = getPersistedRemotePlugins(storage, key).filter((existing) => existing.id !== entry.id);
92
96
  entries.push(entry);
@@ -132,7 +136,7 @@ export const isLocalUrl = (locator: string): boolean => {
132
136
  * Useful for UI code that needs to know which loaded plugins were installed from a URL
133
137
  * (e.g. to surface a tag on remote or localhost-hosted plugins).
134
138
  */
135
- export const getRemoteEntries = (options: Options = {}): readonly RemotePluginEntry[] => {
139
+ export const getRemoteEntries = (options: Options = {}): readonly RemotePluginView[] => {
136
140
  const storage = options.storage ?? defaultStorage();
137
141
  const key = options.key ?? DEFAULT_KEY;
138
142
  return getPersistedRemotePlugins(storage, key);
@@ -196,14 +200,14 @@ const loadStylesheets = (
196
200
  }
197
201
  const cssUrls = manifest.assetUrls.filter((url) => url.endsWith('.css'));
198
202
  for (const url of cssUrls) {
199
- const resolved = yield* cache.resolve(manifest.id, url);
200
- if (document.querySelector(`link[data-dxos-plugin-id="${manifest.id}"][href="${resolved}"]`)) {
203
+ const resolved = yield* cache.resolve(manifest.key, url);
204
+ if (document.querySelector(`link[data-dxos-plugin-id="${manifest.key}"][href="${resolved}"]`)) {
201
205
  continue;
202
206
  }
203
207
  const link = document.createElement('link');
204
208
  link.rel = 'stylesheet';
205
209
  link.href = resolved;
206
- link.dataset.dxosPluginId = manifest.id;
210
+ link.dataset.dxosPluginId = manifest.key;
207
211
  document.head.appendChild(link);
208
212
  }
209
213
  });
@@ -233,30 +237,30 @@ const loadFromManifest = (
233
237
  // plugin's host is offline, dropping the plugin from the runtime.
234
238
  const cachedUrls =
235
239
  manifest.assetUrls.indexOf(manifestUrl) === -1 ? [manifestUrl, ...manifest.assetUrls] : manifest.assetUrls;
236
- yield* cache.cache(manifest.id, cachedUrls).pipe(wrapCacheError);
240
+ yield* cache.cache(manifest.key, cachedUrls).pipe(wrapCacheError);
237
241
  }
238
242
  const entryUrl = manifest.dev
239
243
  ? manifest.entryUrl
240
- : yield* cache.resolve(manifest.id, manifest.entryUrl).pipe(wrapCacheError);
244
+ : yield* cache.resolve(manifest.key, manifest.entryUrl).pipe(wrapCacheError);
241
245
  const mod = yield* Effect.tryPromise({
242
246
  try: () => import(/* @vite-ignore */ entryUrl),
243
247
  catch: (cause) =>
244
248
  new RemotePluginLoadError({ context: { locator: manifestUrl, reason: 'import-failed' }, cause }),
245
249
  });
246
250
  const plugin = normalizePluginExport(mod);
247
- if (!plugin.meta.id || !plugin.meta.name) {
251
+ if (!plugin.meta.profile.key || !plugin.meta.profile.name) {
248
252
  return yield* Effect.fail(
249
253
  new RemotePluginLoadError({ context: { locator: manifestUrl, reason: 'meta-missing' } }),
250
254
  );
251
255
  }
252
- if (plugin.meta.id !== manifest.id) {
256
+ if (plugin.meta.profile.key !== manifest.key) {
253
257
  return yield* Effect.fail(
254
258
  new RemotePluginLoadError({
255
259
  context: {
256
260
  locator: manifestUrl,
257
261
  reason: 'meta-mismatch',
258
- metaId: plugin.meta.id,
259
- manifestId: manifest.id,
262
+ metaId: plugin.meta.profile.key,
263
+ manifestId: manifest.key,
260
264
  },
261
265
  }),
262
266
  );
@@ -337,7 +341,7 @@ export const make = (builtinPlugins: Plugin.Plugin[], options: Options = {}) =>
337
341
 
338
342
  return (locator: string): Effect.Effect<{ plugin: Plugin.Plugin; dev?: boolean }, RemotePluginLoadError> =>
339
343
  Effect.gen(function* () {
340
- const builtin = builtinPlugins.find((plugin) => plugin.meta.id === locator);
344
+ const builtin = builtinPlugins.find((plugin) => plugin.meta.profile.key === locator);
341
345
  if (builtin) {
342
346
  return { plugin: builtin };
343
347
  }
@@ -351,13 +355,13 @@ export const make = (builtinPlugins: Plugin.Plugin[], options: Options = {}) =>
351
355
  // overlap (dev plugin takes the id slot for the session, original is
352
356
  // restored on uninstall or page reload).
353
357
  if (!manifest.dev) {
354
- const duplicate = builtinPlugins.find((existing) => existing.meta.id === plugin.meta.id);
358
+ const duplicate = builtinPlugins.find((existing) => existing.meta.profile.key === plugin.meta.profile.key);
355
359
  if (duplicate) {
356
360
  return yield* Effect.fail(
357
- new RemotePluginLoadError({ context: { locator, reason: 'duplicate-id', id: plugin.meta.id } }),
361
+ new RemotePluginLoadError({ context: { locator, reason: 'duplicate-id', id: plugin.meta.profile.key } }),
358
362
  );
359
363
  }
360
- persistRemotePlugin(storage, key, { id: plugin.meta.id, url: locator });
364
+ persistRemotePlugin(storage, key, { id: plugin.meta.profile.key, url: locator });
361
365
  }
362
366
  return { plugin, dev: manifest.dev };
363
367
  });
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  export * from './common';
6
+ export * from './config';
6
7
  export * from './context';
7
8
  export * from './core';
8
9
  export * from './plugin-process-manager';
@@ -17,7 +17,7 @@ import { ActivationEvent, type Capability, type CapabilityManager, type Plugin,
17
17
 
18
18
  export type TestAppOptions = {
19
19
  /**
20
- * Plugins to register. Plugins whose `meta.tags` includes `'system'` are treated as core
20
+ * Plugins to register. Plugins whose `meta.profile.tags` includes `'system'` are treated as core
21
21
  * (force-enabled). For test convenience, plugins without a `'system'` tag are enabled by
22
22
  * default unless `enabled` is provided.
23
23
  */
@@ -97,7 +97,9 @@ const DEFAULT_TIMEOUT_MS = 5_000;
97
97
  export const createTestApp = async (opts: TestAppOptions): Promise<TestHarness> => {
98
98
  const {
99
99
  plugins,
100
- enabled = plugins.filter(({ meta }) => !meta.tags?.includes('system')).map((plugin) => plugin.meta.id),
100
+ enabled = plugins
101
+ .filter(({ meta }) => !meta.profile.tags?.includes('system'))
102
+ .map((plugin) => plugin.meta.profile.key),
101
103
  setupEvents = [],
102
104
  autoStart = true,
103
105
  registerFrameworkCapabilities = true,
@@ -105,7 +107,7 @@ export const createTestApp = async (opts: TestAppOptions): Promise<TestHarness>
105
107
 
106
108
  const pluginLoader = (id: string) =>
107
109
  Effect.sync(() => {
108
- const plugin = plugins.find((plugin) => plugin.meta.id === id);
110
+ const plugin = plugins.find((plugin) => plugin.meta.profile.key === id);
109
111
  invariant(plugin, `Plugin not found: ${id}`);
110
112
  return { plugin };
111
113
  });
@@ -16,6 +16,8 @@ import { render, renderSurface } from './react';
16
16
 
17
17
  const testMeta = Plugin.makeMeta({ key: DXN.make('org.dxos.plugin.test.reactHarness'), name: 'ReactHarnessTest' });
18
18
 
19
+ const GreetingRole = Surface.makeType<{ message: string }>('org.dxos.test.role.greeting');
20
+
19
21
  const TestPlugin = Plugin.define(testMeta).pipe(
20
22
  Plugin.addModule({
21
23
  id: 'surfaces',
@@ -25,7 +27,7 @@ const TestPlugin = Plugin.define(testMeta).pipe(
25
27
  Capability.contributes(Capabilities.ReactSurface, [
26
28
  Surface.create<{ message: string }>({
27
29
  id: 'greeting',
28
- role: 'greeting',
30
+ filter: Surface.makeFilter(GreetingRole),
29
31
  component: ({ data }) => <span data-testid='greeting'>hello {data.message}</span>,
30
32
  }),
31
33
  ]),
@@ -43,7 +45,7 @@ describe('testing/react', () => {
43
45
 
44
46
  test('renderSurface mounts a surface registered by a plugin', async ({ expect }) => {
45
47
  await using harness = await createTestApp({ plugins: [TestPlugin()] });
46
- const view = renderSurface(harness, { role: 'greeting', data: { message: 'plugins' } });
48
+ const view = renderSurface(harness, { type: GreetingRole, data: { message: 'plugins' } });
47
49
  const node = await view.findByTestId('greeting');
48
50
  expect(node.textContent).toBe('hello plugins');
49
51
  });
@@ -13,6 +13,7 @@ import { PluginManagerContext } from '../context';
13
13
  import { topologicalSort } from '../helpers';
14
14
  import { PluginManagerProvider } from '../ui/components/PluginManager/PluginManagerProvider';
15
15
  import { SurfaceComponent } from '../ui/components/Surface/SurfaceComponent';
16
+ import { type RoleToken } from '../ui/components/Surface/types';
16
17
  import { type TestHarness } from './harness';
17
18
 
18
19
  export type HarnessRenderOptions = Omit<RenderOptions, 'wrapper'> & {
@@ -34,26 +35,26 @@ export const render = (harness: TestHarness, ui: ReactNode, options?: HarnessRen
34
35
  return rtlRender(<>{ui}</>, { ...rest, wrapper: Wrapper });
35
36
  };
36
37
 
37
- export type RenderSurfaceProps = {
38
- role: string;
39
- data?: unknown;
38
+ export type RenderSurfaceProps<TToken extends RoleToken<any>> = {
39
+ type: TToken;
40
+ data?: TToken extends RoleToken<infer D> ? D : never;
40
41
  limit?: number;
41
42
  fallback?: FC<{ error: Error; data?: any }>;
42
43
  placeholder?: ReactNode;
43
44
  };
44
45
 
45
46
  /**
46
- * Renders a `Surface` with the given role/data inside the harness provider tree.
47
+ * Renders a `Surface` with the given role token/data inside the harness provider tree.
47
48
  */
48
- export const renderSurface = (
49
+ export const renderSurface = <TToken extends RoleToken<any>>(
49
50
  harness: TestHarness,
50
- props: RenderSurfaceProps,
51
+ props: RenderSurfaceProps<TToken>,
51
52
  options?: HarnessRenderOptions,
52
53
  ): RenderResult => {
53
- const { role, data, limit, fallback, placeholder } = props;
54
+ const { type, data, limit, fallback, placeholder } = props;
54
55
  return render(
55
56
  harness,
56
- <SurfaceComponent role={role} data={data as any} limit={limit} fallback={fallback} placeholder={placeholder} />,
57
+ <SurfaceComponent type={type} data={data as any} limit={limit} fallback={fallback} placeholder={placeholder} />,
57
58
  options,
58
59
  );
59
60
  };
@@ -22,7 +22,7 @@ export const fromPlugins = (plugins: Plugin.Plugin[]) =>
22
22
 
23
23
  const pluginLoader = (id: string) =>
24
24
  Effect.sync(() => {
25
- const plugin = plugins.find((plugin) => plugin.meta.id === id);
25
+ const plugin = plugins.find((plugin) => plugin.meta.profile.key === id);
26
26
  invariant(plugin, `Plugin not found: ${id}`);
27
27
  return { plugin };
28
28
  });
@@ -30,7 +30,7 @@ export const fromPlugins = (plugins: Plugin.Plugin[]) =>
30
30
  const manager = PluginManager.make({
31
31
  pluginLoader,
32
32
  plugins,
33
- enabled: plugins.map((plugin) => plugin.meta.id),
33
+ enabled: plugins.map((plugin) => plugin.meta.profile.key),
34
34
  });
35
35
 
36
36
  manager.capabilities.contribute({
@@ -12,12 +12,14 @@ import { Capability } from '../core';
12
12
  import { Surface } from '../ui';
13
13
  import { withPluginManager } from './withPluginManager';
14
14
 
15
+ const MainRole = Surface.makeType<Record<string, never>>('org.dxos.test.role.main');
16
+
15
17
  const DefaultStory = () => {
16
18
  console.log('Render');
17
19
  return (
18
20
  <div>
19
21
  <div>Hello</div>
20
- <Surface.Surface role='main' />
22
+ <Surface.Surface type={MainRole} />
21
23
  </div>
22
24
  );
23
25
  };
@@ -33,7 +35,7 @@ const meta = {
33
35
  Capabilities.ReactSurface,
34
36
  Surface.create({
35
37
  id: 'test',
36
- role: 'main',
38
+ filter: Surface.makeFilter(MainRole),
37
39
  component: ({ role }) => <span>{JSON.stringify({ role })}</span>,
38
40
  }),
39
41
  ),
@@ -26,7 +26,9 @@ export const setupPluginManager = ({
26
26
  }: UseAppOptions & Pick<WithPluginManagerOptions, 'capabilities'> = {}) => {
27
27
  // Auto-enable every non-system plugin so stories don't have to spell out
28
28
  // enablement. System-tagged plugins are force-enabled by the manager.
29
- const enabled = plugins.filter(({ meta }) => !meta.tags?.includes('system')).map(({ meta }) => meta.id);
29
+ const enabled = plugins
30
+ .filter(({ meta }) => !meta.profile.tags?.includes('system'))
31
+ .map(({ meta }) => meta.profile.key);
30
32
  const pluginManager = PluginManager.make({
31
33
  pluginLoader: () => raise(new Error('Not implemented')),
32
34
  plugins: [StoryPlugin, ...plugins],
@@ -16,6 +16,13 @@ const PluginManagerContext = createContext<PluginManager.PluginManager | undefin
16
16
  export const usePluginManager = (): PluginManager.PluginManager =>
17
17
  useContext(PluginManagerContext) ?? raise(new Error('Missing PluginManagerContext'));
18
18
 
19
+ /**
20
+ * Get the plugin manager if present, or `undefined` when rendered outside a {@link PluginManagerProvider}
21
+ * (e.g. a standalone component story). Lets components treat plugin capabilities as a progressive
22
+ * enhancement rather than a hard requirement.
23
+ */
24
+ export const useOptionalPluginManager = (): PluginManager.PluginManager | undefined => useContext(PluginManagerContext);
25
+
19
26
  /**
20
27
  * Context provider for a plugin manager.
21
28
  */
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
- import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
6
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
7
7
 
8
8
  import { random } from '@dxos/random';
9
9
  import { List, ListItem, Panel, Toolbar } from '@dxos/react-ui';
@@ -14,23 +14,22 @@ import { Capabilities } from '../../../common';
14
14
  import { withPluginManager } from '../../../testing';
15
15
  import { usePluginManager } from '../PluginManager';
16
16
  import { SurfaceComponent, useSurfaces } from './SurfaceComponent';
17
- import { create } from './types';
17
+ import { create, makeFilter, makeType } from './types';
18
+
19
+ const ItemRole = makeType<{ id: string }>('org.dxos.test.role.item');
18
20
 
19
21
  type TestComponentProps = {
20
22
  id: string;
21
23
  styles: ColorStyles;
22
24
  };
23
25
 
24
- const TestComponent = forwardRef<HTMLDivElement, TestComponentProps>(({ styles, id }, forwardedRef) => {
26
+ const TestComponent = ({ styles, id }: TestComponentProps) => {
25
27
  return (
26
- <div
27
- className={mx('flex justify-center items-center border rounded-sm', styles.surface, styles.border)}
28
- ref={forwardedRef}
29
- >
28
+ <div className={mx('flex justify-center items-center border rounded-sm', styles.surface, styles.border)}>
30
29
  <span className={mx('dx-tag font-mono text-lg', styles.text)}>{id}</span>
31
30
  </div>
32
31
  );
33
- });
32
+ };
34
33
 
35
34
  const DefaultStory = () => {
36
35
  const manager = usePluginManager();
@@ -46,9 +45,8 @@ const DefaultStory = () => {
46
45
  interface: Capabilities.ReactSurface,
47
46
  implementation: create({
48
47
  id,
49
- role: 'item',
50
- filter: (data): data is any => (data as any)?.id === id,
51
- component: ({ ref }) => <TestComponent id={id} styles={styles} ref={ref} />,
48
+ filter: makeFilter(ItemRole, (data) => data.id === id),
49
+ component: () => <TestComponent id={id} styles={styles} />,
52
50
  }),
53
51
  });
54
52
 
@@ -65,8 +63,7 @@ const DefaultStory = () => {
65
63
  interface: Capabilities.ReactSurface,
66
64
  implementation: create({
67
65
  id: 'error',
68
- role: 'item',
69
- filter: (data): data is any => (data as any)?.id === 'error',
66
+ filter: makeFilter(ItemRole, (data) => data.id === 'error'),
70
67
  component: () => {
71
68
  const [count, setCount] = useState(3);
72
69
  useEffect(() => {
@@ -113,7 +110,7 @@ const DefaultStory = () => {
113
110
  </Toolbar.Root>
114
111
  </Panel.Toolbar>
115
112
  <Panel.Content className='grid grid-cols-2 h-full gap-4 overflow-hidden'>
116
- <SurfaceComponent role='item' data={selected ? { id: selected } : undefined} limit={1} ref={ref} />
113
+ <SurfaceComponent type={ItemRole} data={selected ? { id: selected } : undefined} limit={1} ref={ref} />
117
114
  <div className='overflow-y-auto h-full'>
118
115
  <List>
119
116
  {surfaces.map((surface) => (
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { useAtomValue } from '@effect-atom/atom-react';
6
6
  import React, {
7
+ type FC,
7
8
  Fragment,
8
9
  type NamedExoticComponent,
9
10
  Profiler,
@@ -44,78 +45,95 @@ const DEFAULT_PLACEHOLDER = <Fragment />;
44
45
  * Wrapper component for rendering Web Component surfaces.
45
46
  * Handles creation, prop setting, and cleanup of Web Components.
46
47
  */
48
+ type WebComponentWrapperProps = {
49
+ id?: string;
50
+ role: string;
51
+ data?: Record<string, any>;
52
+ limit?: number;
53
+ definition: WebComponentDefinition;
54
+ [key: string]: any;
55
+ };
56
+
47
57
  const WebComponentWrapper = memo(
48
- forwardRef<HTMLElement, Omit<Props, 'role'> & { role: string; definition: WebComponentDefinition }>(
49
- ({ id, role, data, limit, definition, ...rest }, forwardedRef) => {
50
- const containerRef = useRef<HTMLDivElement>(null);
51
- const elementRef = useRef<HTMLElement | null>(null);
52
- const propsRef = useRef({ id, role, data, limit, ...rest });
53
-
54
- // Update props ref on every render
55
- propsRef.current = { id, role, data, limit, ...rest };
56
-
57
- // Create element only once
58
- useEffect(() => {
59
- if (!containerRef.current || elementRef.current) {
60
- return;
61
- }
58
+ forwardRef<HTMLElement, WebComponentWrapperProps>(({ id, role, data, limit, definition, ...rest }, forwardedRef) => {
59
+ const containerRef = useRef<HTMLDivElement>(null);
60
+ const elementRef = useRef<HTMLElement | null>(null);
61
+ const propsRef = useRef({ id, role, data, limit, ...rest });
62
+
63
+ // Update props ref on every render
64
+ propsRef.current = { id, role, data, limit, ...rest };
65
+
66
+ // Create element only once
67
+ useEffect(() => {
68
+ if (!containerRef.current || elementRef.current) {
69
+ return;
70
+ }
62
71
 
63
- // Create the Web Component
64
- const element = document.createElement(definition.tagName);
65
- elementRef.current = element;
72
+ // Create the Web Component
73
+ const element = document.createElement(definition.tagName);
74
+ elementRef.current = element;
66
75
 
67
- // Set initial properties on the Web Component
68
- Object.assign(element, propsRef.current);
76
+ // Set initial properties on the Web Component
77
+ Object.assign(element, propsRef.current);
69
78
 
70
- // Append to container
71
- containerRef.current.appendChild(element);
79
+ // Append to container
80
+ containerRef.current.appendChild(element);
72
81
 
73
- // Setup ref forwarding if provided
82
+ // Setup ref forwarding if provided
83
+ if (typeof forwardedRef === 'function') {
84
+ forwardedRef(element);
85
+ } else if (forwardedRef) {
86
+ forwardedRef.current = element;
87
+ }
88
+
89
+ // Cleanup on unmount to prevent memory leaks
90
+ return () => {
91
+ if (elementRef.current && containerRef.current?.contains(elementRef.current)) {
92
+ containerRef.current.removeChild(elementRef.current);
93
+ }
74
94
  if (typeof forwardedRef === 'function') {
75
- forwardedRef(element);
95
+ forwardedRef(null);
76
96
  } else if (forwardedRef) {
77
- forwardedRef.current = element;
78
- }
79
-
80
- // Cleanup on unmount to prevent memory leaks
81
- return () => {
82
- if (elementRef.current && containerRef.current?.contains(elementRef.current)) {
83
- containerRef.current.removeChild(elementRef.current);
84
- }
85
- if (typeof forwardedRef === 'function') {
86
- forwardedRef(null);
87
- } else if (forwardedRef) {
88
- forwardedRef.current = null;
89
- }
90
- elementRef.current = null;
91
- };
92
- }, [definition.tagName, forwardedRef]);
93
-
94
- // Update props on existing element without recreating it
95
- // This runs on every render to ensure all props (including those in `rest`) are kept up to date
96
- useEffect(() => {
97
- const element = elementRef.current;
98
- if (!element) {
99
- return;
97
+ forwardedRef.current = null;
100
98
  }
99
+ elementRef.current = null;
100
+ };
101
+ }, [definition.tagName, forwardedRef]);
102
+
103
+ // Update props on existing element without recreating it
104
+ // This runs on every render to ensure all props (including those in `rest`) are kept up to date
105
+ useEffect(() => {
106
+ const element = elementRef.current;
107
+ if (!element) {
108
+ return;
109
+ }
101
110
 
102
- // Update properties on the existing Web Component
103
- Object.assign(element, propsRef.current);
104
- });
111
+ // Update properties on the existing Web Component
112
+ Object.assign(element, propsRef.current);
113
+ });
105
114
 
106
- return <div ref={containerRef} />;
107
- },
108
- ),
115
+ return <div ref={containerRef} />;
116
+ }),
109
117
  );
110
118
 
111
119
  WebComponentWrapper.displayName = 'WebComponentWrapper';
112
120
 
121
+ type SurfaceContextProviderProps = {
122
+ id: string;
123
+ role: string;
124
+ data?: Record<string, any>;
125
+ limit?: number;
126
+ fallback?: FC<{ error: Error; data?: any }>;
127
+ definition: Definition;
128
+ [key: string]: any;
129
+ };
130
+
113
131
  /**
114
132
  * Wrapper component that provides context for a surface.
115
133
  */
116
134
  // TODO(burdon): Allow DebugPlugin to provide different fallback using react-ui ErrorFallback.
117
135
  const SurfaceContextProvider = memo(
118
- forwardRef<HTMLElement, Omit<Props, 'role'> & { role: string; definition: Definition }>(
136
+ forwardRef<HTMLElement, SurfaceContextProviderProps>(
119
137
  ({ id, role, data, limit, fallback = ErrorFallback, definition, ...rest }, forwardedRef) => {
120
138
  const contextValue = useMemo(() => ({ id, role, data }), [id, role, data]);
121
139
  const onProfilerRender = useSurfaceProfilerCallback();
@@ -182,11 +200,13 @@ SurfaceContextProvider.displayName = 'SurfaceContextProvider';
182
200
 
183
201
  /**
184
202
  * A surface is a named region of the screen that can be populated by plugins.
203
+ * The `type` prop is a {@link RoleToken} that defines which region and its
204
+ * associated data contract.
185
205
  */
186
206
  // TODO(burdon): Remove `ref` since relying on this would be error prone.
187
207
  export const SurfaceComponent = memo(
188
- forwardRef<HTMLElement, Props & { type?: RoleToken<any> }>(
189
- ({ id: _id, role, type, data: dataProp, limit, placeholder = DEFAULT_PLACEHOLDER, ...rest }, forwardedRef) => {
208
+ forwardRef<HTMLElement, TypedProps<RoleToken<any>>>(
209
+ ({ id: _id, type, data: dataProp, limit, placeholder = DEFAULT_PLACEHOLDER, ...rest }, forwardedRef) => {
190
210
  const data = useDefaultValue(dataProp, () => ({}));
191
211
  // TODO(wittjosiah): This will make all surfaces depend on a single signal.
192
212
  // This isn't ideal because it means that any change to the data will cause all surfaces to re-render.
@@ -195,10 +215,10 @@ export const SurfaceComponent = memo(
195
215
  // In the future, it would be nice to be able to bucket the surface contributions by role.
196
216
  const surfaces = useSurfaces();
197
217
 
198
- const effectiveRole = role ?? type?.role;
218
+ const effectiveRole = type?.role;
199
219
  if (effectiveRole == null) {
200
220
  if (DEBUG) {
201
- log.warn('Surface has neither `role` nor `type` prop', { id: _id });
221
+ log.warn('Surface is missing required `type` prop', { id: _id });
202
222
  }
203
223
  return null;
204
224
  }
@@ -230,12 +250,12 @@ export const SurfaceComponent = memo(
230
250
  },
231
251
  ),
232
252
  ) as (<TToken extends RoleToken<any>>(props: TypedProps<TToken> & RefAttributes<HTMLElement>) => React.ReactNode) &
233
- NamedExoticComponent<Props & RefAttributes<HTMLElement>>;
253
+ NamedExoticComponent<TypedProps<RoleToken<any>> & RefAttributes<HTMLElement>>;
234
254
 
235
255
  SurfaceComponent.displayName = 'Surface';
236
256
 
237
257
  // TODO(burdon): Make user facing, with telemetry.
238
- const ErrorFallback = ({ error }: Props) => {
258
+ const ErrorFallback = ({ error }: { error: Error }) => {
239
259
  const { message } = error instanceof Error ? error : { message: String(error) };
240
260
  return (
241
261
  <div role='alert' data-testid='error-boundary-fallback'>
@@ -273,31 +293,21 @@ export const useSurfaces = () => {
273
293
  /**
274
294
  * @returns `true` if there is a contributed surface which matches the specified role & data, `false` otherwise.
275
295
  *
276
- * Two overloads:
277
- * - Typed: pass a `type` role token and `data` is constrained to the token's
278
- * declared contract (e.g. `AppSurface.Section` requires `attendableId`).
279
- * - Legacy: pass a string `role` and `data` is untyped.
296
+ * Typed: pass a `type` role token and `data` is constrained to the token's
297
+ * declared contract (e.g. `AppSurface.Section` requires `attendableId`).
280
298
  */
281
299
  export function isSurfaceAvailable<TToken extends RoleToken<any>>(
282
300
  capabilityManager: CapabilityManager.CapabilityManager,
283
- args: { type: TToken; data?: TokenData<TToken>; role?: never },
284
- ): boolean;
285
- export function isSurfaceAvailable(
286
- capabilityManager: CapabilityManager.CapabilityManager,
287
- args: Pick<Props, 'role' | 'data'> & { type?: undefined },
288
- ): boolean;
289
- export function isSurfaceAvailable(
290
- capabilityManager: CapabilityManager.CapabilityManager,
291
- { role, type, data }: { role?: string; type?: RoleToken<any>; data?: unknown },
301
+ args: { type: TToken; data?: TokenData<TToken> },
292
302
  ): boolean {
293
- const effectiveRole = role ?? type?.role;
303
+ const effectiveRole = args.type?.role;
294
304
  if (effectiveRole == null) {
295
305
  return false;
296
306
  }
297
307
  const surfaces = capabilityManager.getAll(Capabilities.ReactSurface);
298
308
  const candidates = findCandidates(surfaces.flat(), {
299
309
  role: effectiveRole,
300
- data: data as Props['data'],
310
+ data: args.data as Props['data'],
301
311
  });
302
312
  return candidates.length > 0;
303
313
  }
@@ -4,9 +4,12 @@
4
4
 
5
5
  import { type Context, createContext } from 'react';
6
6
 
7
- import { type Props } from './types';
8
-
9
- export type SurfaceContext = Pick<Props, 'id' | 'role' | 'data'>;
7
+ export type SurfaceContext = {
8
+ id?: string;
9
+ /** The resolved role NSID (e.g. `org.dxos.role.article`). */
10
+ role: string;
11
+ data?: Record<string, any>;
12
+ };
10
13
 
11
14
  // TODO(burdon): Use @radix-ui/react-context
12
15
  export const SurfaceContext: Context<SurfaceContext | undefined> = createContext<SurfaceContext | undefined>(undefined);