@object-ui/core 3.3.0 → 3.3.2

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 (101) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +20 -1
  3. package/dist/actions/ActionRunner.d.ts +9 -0
  4. package/dist/actions/ActionRunner.js +41 -4
  5. package/dist/adapters/ValueDataSource.js +3 -1
  6. package/dist/registry/Registry.d.ts +47 -0
  7. package/dist/registry/Registry.js +92 -0
  8. package/dist/utils/filter-converter.js +25 -5
  9. package/package.json +32 -8
  10. package/.turbo/turbo-build.log +0 -4
  11. package/src/__benchmarks__/core.bench.ts +0 -64
  12. package/src/__tests__/protocols/DndProtocol.test.ts +0 -186
  13. package/src/__tests__/protocols/KeyboardProtocol.test.ts +0 -177
  14. package/src/__tests__/protocols/NotificationProtocol.test.ts +0 -142
  15. package/src/__tests__/protocols/ResponsiveProtocol.test.ts +0 -176
  16. package/src/__tests__/protocols/SharingProtocol.test.ts +0 -188
  17. package/src/actions/ActionEngine.ts +0 -268
  18. package/src/actions/ActionRunner.ts +0 -717
  19. package/src/actions/TransactionManager.ts +0 -521
  20. package/src/actions/UndoManager.ts +0 -215
  21. package/src/actions/__tests__/ActionEngine.test.ts +0 -206
  22. package/src/actions/__tests__/ActionRunner.params.test.ts +0 -134
  23. package/src/actions/__tests__/ActionRunner.test.ts +0 -711
  24. package/src/actions/__tests__/TransactionManager.test.ts +0 -447
  25. package/src/actions/__tests__/UndoManager.test.ts +0 -320
  26. package/src/actions/index.ts +0 -12
  27. package/src/adapters/ApiDataSource.ts +0 -376
  28. package/src/adapters/README.md +0 -180
  29. package/src/adapters/ValueDataSource.ts +0 -459
  30. package/src/adapters/__tests__/ApiDataSource.test.ts +0 -418
  31. package/src/adapters/__tests__/ValueDataSource.test.ts +0 -571
  32. package/src/adapters/__tests__/resolveDataSource.test.ts +0 -144
  33. package/src/adapters/index.ts +0 -15
  34. package/src/adapters/resolveDataSource.ts +0 -79
  35. package/src/builder/__tests__/schema-builder.test.ts +0 -235
  36. package/src/builder/schema-builder.ts +0 -584
  37. package/src/data-scope/DataScopeManager.ts +0 -269
  38. package/src/data-scope/ViewDataProvider.ts +0 -282
  39. package/src/data-scope/__tests__/DataScopeManager.test.ts +0 -211
  40. package/src/data-scope/__tests__/ViewDataProvider.test.ts +0 -270
  41. package/src/data-scope/index.ts +0 -24
  42. package/src/errors/__tests__/errors.test.ts +0 -292
  43. package/src/errors/index.ts +0 -269
  44. package/src/evaluator/ExpressionCache.ts +0 -206
  45. package/src/evaluator/ExpressionContext.ts +0 -118
  46. package/src/evaluator/ExpressionEvaluator.ts +0 -315
  47. package/src/evaluator/FormulaFunctions.ts +0 -398
  48. package/src/evaluator/SafeExpressionParser.ts +0 -893
  49. package/src/evaluator/__tests__/ExpressionCache.test.ts +0 -135
  50. package/src/evaluator/__tests__/ExpressionContext.test.ts +0 -110
  51. package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +0 -558
  52. package/src/evaluator/__tests__/FormulaFunctions.test.ts +0 -447
  53. package/src/evaluator/index.ts +0 -13
  54. package/src/index.ts +0 -38
  55. package/src/protocols/DndProtocol.ts +0 -168
  56. package/src/protocols/KeyboardProtocol.ts +0 -181
  57. package/src/protocols/NotificationProtocol.ts +0 -150
  58. package/src/protocols/ResponsiveProtocol.ts +0 -210
  59. package/src/protocols/SharingProtocol.ts +0 -185
  60. package/src/protocols/index.ts +0 -13
  61. package/src/query/__tests__/query-ast.test.ts +0 -211
  62. package/src/query/__tests__/window-functions.test.ts +0 -275
  63. package/src/query/index.ts +0 -7
  64. package/src/query/query-ast.ts +0 -341
  65. package/src/registry/PluginScopeImpl.ts +0 -259
  66. package/src/registry/PluginSystem.ts +0 -206
  67. package/src/registry/Registry.ts +0 -219
  68. package/src/registry/WidgetRegistry.ts +0 -316
  69. package/src/registry/__tests__/PluginSystem.test.ts +0 -309
  70. package/src/registry/__tests__/Registry.test.ts +0 -293
  71. package/src/registry/__tests__/WidgetRegistry.test.ts +0 -321
  72. package/src/registry/__tests__/plugin-scope-integration.test.ts +0 -283
  73. package/src/theme/ThemeEngine.ts +0 -530
  74. package/src/theme/__tests__/ThemeEngine.test.ts +0 -668
  75. package/src/theme/index.ts +0 -24
  76. package/src/types/index.ts +0 -21
  77. package/src/utils/__tests__/debug-collector.test.ts +0 -102
  78. package/src/utils/__tests__/debug.test.ts +0 -134
  79. package/src/utils/__tests__/expand-fields.test.ts +0 -120
  80. package/src/utils/__tests__/extract-records.test.ts +0 -50
  81. package/src/utils/__tests__/filter-converter.test.ts +0 -118
  82. package/src/utils/__tests__/merge-views-into-objects.test.ts +0 -110
  83. package/src/utils/__tests__/normalize-quick-filter.test.ts +0 -123
  84. package/src/utils/debug-collector.ts +0 -100
  85. package/src/utils/debug.ts +0 -148
  86. package/src/utils/expand-fields.ts +0 -76
  87. package/src/utils/extract-records.ts +0 -33
  88. package/src/utils/filter-converter.ts +0 -133
  89. package/src/utils/merge-views-into-objects.ts +0 -36
  90. package/src/utils/normalize-quick-filter.ts +0 -78
  91. package/src/validation/__tests__/object-validation-engine.test.ts +0 -567
  92. package/src/validation/__tests__/schema-validator.test.ts +0 -118
  93. package/src/validation/__tests__/validation-engine.test.ts +0 -102
  94. package/src/validation/index.ts +0 -10
  95. package/src/validation/schema-validator.ts +0 -344
  96. package/src/validation/validation-engine.ts +0 -528
  97. package/src/validation/validators/index.ts +0 -25
  98. package/src/validation/validators/object-validation-engine.ts +0 -722
  99. package/tsconfig.json +0 -15
  100. package/tsconfig.tsbuildinfo +0 -1
  101. package/vitest.config.ts +0 -2
@@ -1,316 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * WidgetRegistry - Runtime widget management with auto-discovery.
11
- *
12
- * Provides registration, loading, and lookup of runtime widgets
13
- * described by WidgetManifest objects. Widgets can be loaded from
14
- * ES module URLs, provided inline, or resolved from the component registry.
15
- *
16
- * @module
17
- */
18
-
19
- import type {
20
- WidgetManifest,
21
- ResolvedWidget,
22
- WidgetRegistryEvent,
23
- WidgetRegistryListener,
24
- } from '@object-ui/types';
25
- import type { Registry } from './Registry.js';
26
-
27
- /**
28
- * Options for creating a WidgetRegistry instance.
29
- */
30
- export interface WidgetRegistryOptions {
31
- /**
32
- * Component registry to sync loaded widgets into.
33
- * When a widget is loaded, its component is also registered here.
34
- */
35
- componentRegistry?: Registry;
36
- }
37
-
38
- /**
39
- * WidgetRegistry manages runtime-loadable widgets described by manifests.
40
- *
41
- * @example
42
- * ```ts
43
- * const widgets = new WidgetRegistry({ componentRegistry: registry });
44
- *
45
- * widgets.register({
46
- * name: 'custom-chart',
47
- * version: '1.0.0',
48
- * type: 'chart',
49
- * label: 'Custom Chart',
50
- * source: { type: 'module', url: '/widgets/chart.js' },
51
- * });
52
- *
53
- * const resolved = await widgets.load('custom-chart');
54
- * ```
55
- */
56
- export class WidgetRegistry {
57
- private manifests = new Map<string, WidgetManifest>();
58
- private resolved = new Map<string, ResolvedWidget>();
59
- private listeners = new Set<WidgetRegistryListener>();
60
- private componentRegistry?: Registry;
61
-
62
- constructor(options: WidgetRegistryOptions = {}) {
63
- this.componentRegistry = options.componentRegistry;
64
- }
65
-
66
- /**
67
- * Register a widget manifest.
68
- * Does not load the widget; call `load()` to resolve it.
69
- */
70
- register(manifest: WidgetManifest): void {
71
- this.manifests.set(manifest.name, manifest);
72
- this.emit({ type: 'widget:registered', widget: manifest });
73
- }
74
-
75
- /**
76
- * Register multiple widget manifests at once.
77
- */
78
- registerAll(manifests: WidgetManifest[]): void {
79
- for (const manifest of manifests) {
80
- this.register(manifest);
81
- }
82
- }
83
-
84
- /**
85
- * Unregister a widget by name.
86
- */
87
- unregister(name: string): boolean {
88
- const existed = this.manifests.delete(name);
89
- this.resolved.delete(name);
90
- if (existed) {
91
- this.emit({ type: 'widget:unregistered', name });
92
- }
93
- return existed;
94
- }
95
-
96
- /**
97
- * Get a widget manifest by name.
98
- */
99
- getManifest(name: string): WidgetManifest | undefined {
100
- return this.manifests.get(name);
101
- }
102
-
103
- /**
104
- * Get all registered widget manifests.
105
- */
106
- getAllManifests(): WidgetManifest[] {
107
- return Array.from(this.manifests.values());
108
- }
109
-
110
- /**
111
- * Get manifests filtered by category.
112
- */
113
- getByCategory(category: string): WidgetManifest[] {
114
- return this.getAllManifests().filter((m) => m.category === category);
115
- }
116
-
117
- /**
118
- * Check if a widget is registered.
119
- */
120
- has(name: string): boolean {
121
- return this.manifests.has(name);
122
- }
123
-
124
- /**
125
- * Check if a widget has been loaded (resolved).
126
- */
127
- isLoaded(name: string): boolean {
128
- return this.resolved.has(name);
129
- }
130
-
131
- /**
132
- * Load (resolve) a widget by name.
133
- * If already loaded, returns the cached resolved widget.
134
- *
135
- * @throws Error if the widget is not registered or fails to load.
136
- */
137
- async load(name: string): Promise<ResolvedWidget> {
138
- // Return cached if already loaded
139
- const cached = this.resolved.get(name);
140
- if (cached) return cached;
141
-
142
- const manifest = this.manifests.get(name);
143
- if (!manifest) {
144
- const error = new Error(`Widget "${name}" is not registered`);
145
- this.emit({ type: 'widget:error', name, error });
146
- throw error;
147
- }
148
-
149
- // Resolve dependencies first
150
- if (manifest.dependencies) {
151
- for (const dep of manifest.dependencies) {
152
- if (!this.isLoaded(dep)) {
153
- await this.load(dep);
154
- }
155
- }
156
- }
157
-
158
- try {
159
- const component = await this.resolveComponent(manifest);
160
-
161
- const resolved: ResolvedWidget = {
162
- manifest,
163
- component,
164
- loadedAt: Date.now(),
165
- };
166
-
167
- this.resolved.set(name, resolved);
168
-
169
- // Sync to component registry if available
170
- if (this.componentRegistry) {
171
- this.componentRegistry.register(manifest.type, component as any, {
172
- label: manifest.label,
173
- icon: manifest.icon,
174
- category: manifest.category,
175
- inputs: manifest.inputs?.map((input) => ({
176
- name: input.name,
177
- type: input.type,
178
- label: input.label,
179
- defaultValue: input.defaultValue,
180
- required: input.required,
181
- enum: input.options,
182
- description: input.description,
183
- advanced: input.advanced,
184
- })),
185
- defaultProps: manifest.defaultProps as Record<string, any>,
186
- isContainer: manifest.isContainer,
187
- });
188
- }
189
-
190
- this.emit({ type: 'widget:loaded', widget: resolved });
191
- return resolved;
192
- } catch (err) {
193
- const error = err instanceof Error ? err : new Error(String(err));
194
- this.emit({ type: 'widget:error', name, error });
195
- throw error;
196
- }
197
- }
198
-
199
- /**
200
- * Load all registered widgets.
201
- * Returns an array of results (settled promises).
202
- */
203
- async loadAll(): Promise<Array<{ name: string; result: ResolvedWidget | Error }>> {
204
- const results: Array<{ name: string; result: ResolvedWidget | Error }> = [];
205
-
206
- for (const [name] of this.manifests) {
207
- try {
208
- const resolved = await this.load(name);
209
- results.push({ name, result: resolved });
210
- } catch (err) {
211
- results.push({ name, result: err instanceof Error ? err : new Error(String(err)) });
212
- }
213
- }
214
-
215
- return results;
216
- }
217
-
218
- /**
219
- * Subscribe to widget registry events.
220
- * @returns Unsubscribe function.
221
- */
222
- on(listener: WidgetRegistryListener): () => void {
223
- this.listeners.add(listener);
224
- return () => {
225
- this.listeners.delete(listener);
226
- };
227
- }
228
-
229
- /**
230
- * Clear all registered and loaded widgets.
231
- */
232
- clear(): void {
233
- this.manifests.clear();
234
- this.resolved.clear();
235
- }
236
-
237
- /**
238
- * Get registry statistics.
239
- */
240
- getStats(): { registered: number; loaded: number; categories: string[] } {
241
- const categories = new Set<string>();
242
- for (const m of this.manifests.values()) {
243
- if (m.category) categories.add(m.category);
244
- }
245
- return {
246
- registered: this.manifests.size,
247
- loaded: this.resolved.size,
248
- categories: Array.from(categories),
249
- };
250
- }
251
-
252
- // -------------------------------------------------------------------------
253
- // Private helpers
254
- // -------------------------------------------------------------------------
255
-
256
- private async resolveComponent(manifest: WidgetManifest): Promise<unknown> {
257
- const { source } = manifest;
258
-
259
- switch (source.type) {
260
- case 'inline':
261
- return source.component;
262
-
263
- case 'registry': {
264
- if (!this.componentRegistry) {
265
- throw new Error(
266
- `Widget "${manifest.name}" uses registry source but no component registry is configured`,
267
- );
268
- }
269
- const component = this.componentRegistry.get(source.registryKey);
270
- if (!component) {
271
- throw new Error(
272
- `Widget "${manifest.name}" references registry key "${source.registryKey}" which is not registered`,
273
- );
274
- }
275
- return component;
276
- }
277
-
278
- case 'module': {
279
- // Runtime-only dynamic import for loading widgets from external URLs
280
- // This uses Function constructor to prevent bundlers (Webpack/Turbopack/Vite)
281
- // from attempting static analysis at build time, which would fail since
282
- // source.url is only known at runtime.
283
- //
284
- // Security: Widget URLs must be from trusted sources only. Never pass
285
- // user-supplied URLs directly to WidgetManifest. URLs should be validated
286
- // and controlled by the application developer.
287
- //
288
- // CSP Consideration: If your application uses strict Content Security Policy,
289
- // ensure dynamic imports are allowed or use 'inline' or 'registry' source types.
290
- const dynamicImport = new Function('url', 'return import(url)');
291
- const mod = await dynamicImport(source.url);
292
- const exportName = source.exportName ?? 'default';
293
- const component = mod[exportName];
294
- if (!component) {
295
- throw new Error(
296
- `Widget "${manifest.name}" module at "${source.url}" does not export "${exportName}"`,
297
- );
298
- }
299
- return component;
300
- }
301
-
302
- default:
303
- throw new Error(`Unknown widget source type for "${manifest.name}"`);
304
- }
305
- }
306
-
307
- private emit(event: WidgetRegistryEvent): void {
308
- for (const listener of this.listeners) {
309
- try {
310
- listener(event);
311
- } catch {
312
- // Swallow listener errors to prevent cascading failures
313
- }
314
- }
315
- }
316
- }
@@ -1,309 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import { describe, it, expect, vi, beforeEach } from 'vitest';
10
- import { PluginSystem, type PluginDefinition } from '../PluginSystem';
11
- import { Registry } from '../Registry';
12
-
13
- describe('PluginSystem', () => {
14
- let pluginSystem: PluginSystem;
15
- let registry: Registry;
16
-
17
- beforeEach(() => {
18
- pluginSystem = new PluginSystem();
19
- registry = new Registry();
20
- });
21
-
22
- it('should load a simple plugin', async () => {
23
- const plugin: PluginDefinition = {
24
- name: 'test-plugin',
25
- version: '1.0.0',
26
- register: (reg) => {
27
- reg.register('test', () => 'test');
28
- }
29
- };
30
-
31
- // Use legacy mode (useScope: false) to test direct registry access
32
- await pluginSystem.loadPlugin(plugin, registry, false);
33
-
34
- expect(pluginSystem.isLoaded('test-plugin')).toBe(true);
35
- expect(pluginSystem.getLoadedPlugins()).toContain('test-plugin');
36
- expect(registry.has('test')).toBe(true);
37
- });
38
-
39
- it('should execute onLoad lifecycle hook', async () => {
40
- const onLoad = vi.fn();
41
- const plugin: PluginDefinition = {
42
- name: 'test-plugin',
43
- version: '1.0.0',
44
- register: () => {},
45
- onLoad
46
- };
47
-
48
- await pluginSystem.loadPlugin(plugin, registry);
49
-
50
- expect(onLoad).toHaveBeenCalledTimes(1);
51
- });
52
-
53
- it('should execute async onLoad lifecycle hook', async () => {
54
- const onLoad = vi.fn().mockResolvedValue(undefined);
55
- const plugin: PluginDefinition = {
56
- name: 'test-plugin',
57
- version: '1.0.0',
58
- register: () => {},
59
- onLoad
60
- };
61
-
62
- await pluginSystem.loadPlugin(plugin, registry);
63
-
64
- expect(onLoad).toHaveBeenCalledTimes(1);
65
- });
66
-
67
- it('should not load plugin twice', async () => {
68
- const onLoad = vi.fn();
69
- const plugin: PluginDefinition = {
70
- name: 'test-plugin',
71
- version: '1.0.0',
72
- register: () => {},
73
- onLoad
74
- };
75
-
76
- await pluginSystem.loadPlugin(plugin, registry);
77
- await pluginSystem.loadPlugin(plugin, registry);
78
-
79
- expect(onLoad).toHaveBeenCalledTimes(1);
80
- });
81
-
82
- it('should check dependencies before loading', async () => {
83
- const plugin: PluginDefinition = {
84
- name: 'dependent-plugin',
85
- version: '1.0.0',
86
- dependencies: ['base-plugin'],
87
- register: () => {}
88
- };
89
-
90
- await expect(pluginSystem.loadPlugin(plugin, registry)).rejects.toThrow(
91
- 'Missing dependency: base-plugin required by dependent-plugin'
92
- );
93
- });
94
-
95
- it('should load plugins with dependencies in correct order', async () => {
96
- const basePlugin: PluginDefinition = {
97
- name: 'base-plugin',
98
- version: '1.0.0',
99
- register: () => {}
100
- };
101
-
102
- const dependentPlugin: PluginDefinition = {
103
- name: 'dependent-plugin',
104
- version: '1.0.0',
105
- dependencies: ['base-plugin'],
106
- register: () => {}
107
- };
108
-
109
- await pluginSystem.loadPlugin(basePlugin, registry);
110
- await pluginSystem.loadPlugin(dependentPlugin, registry);
111
-
112
- expect(pluginSystem.isLoaded('base-plugin')).toBe(true);
113
- expect(pluginSystem.isLoaded('dependent-plugin')).toBe(true);
114
- });
115
-
116
- it('should unload a plugin', async () => {
117
- const onUnload = vi.fn();
118
- const plugin: PluginDefinition = {
119
- name: 'test-plugin',
120
- version: '1.0.0',
121
- register: () => {},
122
- onUnload
123
- };
124
-
125
- await pluginSystem.loadPlugin(plugin, registry);
126
- expect(pluginSystem.isLoaded('test-plugin')).toBe(true);
127
-
128
- await pluginSystem.unloadPlugin('test-plugin');
129
-
130
- expect(pluginSystem.isLoaded('test-plugin')).toBe(false);
131
- expect(onUnload).toHaveBeenCalledTimes(1);
132
- });
133
-
134
- it('should prevent unloading plugin with dependents', async () => {
135
- const basePlugin: PluginDefinition = {
136
- name: 'base-plugin',
137
- version: '1.0.0',
138
- register: () => {}
139
- };
140
-
141
- const dependentPlugin: PluginDefinition = {
142
- name: 'dependent-plugin',
143
- version: '1.0.0',
144
- dependencies: ['base-plugin'],
145
- register: () => {}
146
- };
147
-
148
- await pluginSystem.loadPlugin(basePlugin, registry);
149
- await pluginSystem.loadPlugin(dependentPlugin, registry);
150
-
151
- await expect(pluginSystem.unloadPlugin('base-plugin')).rejects.toThrow(
152
- 'Cannot unload plugin "base-plugin" - plugin "dependent-plugin" depends on it'
153
- );
154
- });
155
-
156
- it('should throw error when unloading non-existent plugin', async () => {
157
- await expect(pluginSystem.unloadPlugin('non-existent')).rejects.toThrow(
158
- 'Plugin "non-existent" is not loaded'
159
- );
160
- });
161
-
162
- it('should get plugin definition', async () => {
163
- const plugin: PluginDefinition = {
164
- name: 'test-plugin',
165
- version: '1.0.0',
166
- register: () => {}
167
- };
168
-
169
- await pluginSystem.loadPlugin(plugin, registry);
170
-
171
- const retrieved = pluginSystem.getPlugin('test-plugin');
172
- expect(retrieved).toBe(plugin);
173
- });
174
-
175
- it('should get all plugins', async () => {
176
- const plugin1: PluginDefinition = {
177
- name: 'plugin-1',
178
- version: '1.0.0',
179
- register: () => {}
180
- };
181
-
182
- const plugin2: PluginDefinition = {
183
- name: 'plugin-2',
184
- version: '1.0.0',
185
- register: () => {}
186
- };
187
-
188
- await pluginSystem.loadPlugin(plugin1, registry);
189
- await pluginSystem.loadPlugin(plugin2, registry);
190
-
191
- const allPlugins = pluginSystem.getAllPlugins();
192
- expect(allPlugins).toHaveLength(2);
193
- expect(allPlugins).toContain(plugin1);
194
- expect(allPlugins).toContain(plugin2);
195
- });
196
-
197
- it('should call register function with registry', async () => {
198
- const registerFn = vi.fn();
199
- const plugin: PluginDefinition = {
200
- name: 'test-plugin',
201
- version: '1.0.0',
202
- register: registerFn
203
- };
204
-
205
- // Use legacy mode (useScope: false) to verify the raw Registry is passed
206
- await pluginSystem.loadPlugin(plugin, registry, false);
207
-
208
- expect(registerFn).toHaveBeenCalledWith(registry);
209
- expect(registerFn).toHaveBeenCalledTimes(1);
210
- });
211
-
212
- it('should cleanup on registration failure', async () => {
213
- const plugin: PluginDefinition = {
214
- name: 'failing-plugin',
215
- version: '1.0.0',
216
- register: () => {
217
- throw new Error('Registration failed');
218
- }
219
- };
220
-
221
- await expect(pluginSystem.loadPlugin(plugin, registry)).rejects.toThrow('Registration failed');
222
-
223
- expect(pluginSystem.isLoaded('failing-plugin')).toBe(false);
224
- expect(pluginSystem.getPlugin('failing-plugin')).toBeUndefined();
225
- });
226
-
227
- describe('install / uninstall (AppMetadataPlugin)', () => {
228
- it('should install an AppMetadataPlugin and call init + start', async () => {
229
- const initFn = vi.fn();
230
- const startFn = vi.fn();
231
- const stopFn = vi.fn();
232
-
233
- const appPlugin = {
234
- name: '@test/my-plugin',
235
- version: '1.0.0',
236
- type: 'app-metadata' as const,
237
- description: 'Test metadata plugin',
238
- init: initFn,
239
- start: startFn,
240
- stop: stopFn,
241
- getConfig: () => ({ objects: [] }),
242
- };
243
-
244
- await pluginSystem.install(appPlugin, registry, { logger: console });
245
-
246
- expect(initFn).toHaveBeenCalledTimes(1);
247
- expect(startFn).toHaveBeenCalledTimes(1);
248
- expect(startFn).toHaveBeenCalledWith({ logger: console });
249
- expect(pluginSystem.isLoaded('@test/my-plugin')).toBe(true);
250
- });
251
-
252
- it('should not install the same plugin twice', async () => {
253
- const appPlugin = {
254
- name: '@test/dup-plugin',
255
- version: '1.0.0',
256
- type: 'app-metadata' as const,
257
- init: vi.fn(),
258
- start: vi.fn(),
259
- stop: vi.fn(),
260
- };
261
-
262
- await pluginSystem.install(appPlugin, registry);
263
- await pluginSystem.install(appPlugin, registry);
264
-
265
- expect(appPlugin.init).toHaveBeenCalledTimes(1);
266
- });
267
-
268
- it('should uninstall a plugin and call stop', async () => {
269
- const stopFn = vi.fn();
270
- const appPlugin = {
271
- name: '@test/removable',
272
- version: '1.0.0',
273
- type: 'app-metadata' as const,
274
- init: vi.fn(),
275
- start: vi.fn(),
276
- stop: stopFn,
277
- };
278
-
279
- await pluginSystem.install(appPlugin, registry);
280
- expect(pluginSystem.isLoaded('@test/removable')).toBe(true);
281
-
282
- await pluginSystem.uninstall('@test/removable');
283
- expect(pluginSystem.isLoaded('@test/removable')).toBe(false);
284
- expect(stopFn).toHaveBeenCalledTimes(1);
285
- });
286
-
287
- it('should throw when uninstalling a plugin that is not installed', async () => {
288
- await expect(pluginSystem.uninstall('@test/nonexistent')).rejects.toThrow(
289
- 'Plugin "@test/nonexistent" is not loaded'
290
- );
291
- });
292
-
293
- it('should provide default context when none is given', async () => {
294
- const startFn = vi.fn();
295
- const appPlugin = {
296
- name: '@test/default-ctx',
297
- version: '1.0.0',
298
- type: 'app-metadata' as const,
299
- init: vi.fn(),
300
- start: startFn,
301
- stop: vi.fn(),
302
- };
303
-
304
- await pluginSystem.install(appPlugin, registry);
305
-
306
- expect(startFn).toHaveBeenCalledWith({ logger: console });
307
- });
308
- });
309
- });