@object-ui/core 3.3.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +6 -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/utils/filter-converter.js +25 -5
  7. package/package.json +32 -8
  8. package/.turbo/turbo-build.log +0 -4
  9. package/src/__benchmarks__/core.bench.ts +0 -64
  10. package/src/__tests__/protocols/DndProtocol.test.ts +0 -186
  11. package/src/__tests__/protocols/KeyboardProtocol.test.ts +0 -177
  12. package/src/__tests__/protocols/NotificationProtocol.test.ts +0 -142
  13. package/src/__tests__/protocols/ResponsiveProtocol.test.ts +0 -176
  14. package/src/__tests__/protocols/SharingProtocol.test.ts +0 -188
  15. package/src/actions/ActionEngine.ts +0 -268
  16. package/src/actions/ActionRunner.ts +0 -717
  17. package/src/actions/TransactionManager.ts +0 -521
  18. package/src/actions/UndoManager.ts +0 -215
  19. package/src/actions/__tests__/ActionEngine.test.ts +0 -206
  20. package/src/actions/__tests__/ActionRunner.params.test.ts +0 -134
  21. package/src/actions/__tests__/ActionRunner.test.ts +0 -711
  22. package/src/actions/__tests__/TransactionManager.test.ts +0 -447
  23. package/src/actions/__tests__/UndoManager.test.ts +0 -320
  24. package/src/actions/index.ts +0 -12
  25. package/src/adapters/ApiDataSource.ts +0 -376
  26. package/src/adapters/README.md +0 -180
  27. package/src/adapters/ValueDataSource.ts +0 -459
  28. package/src/adapters/__tests__/ApiDataSource.test.ts +0 -418
  29. package/src/adapters/__tests__/ValueDataSource.test.ts +0 -571
  30. package/src/adapters/__tests__/resolveDataSource.test.ts +0 -144
  31. package/src/adapters/index.ts +0 -15
  32. package/src/adapters/resolveDataSource.ts +0 -79
  33. package/src/builder/__tests__/schema-builder.test.ts +0 -235
  34. package/src/builder/schema-builder.ts +0 -584
  35. package/src/data-scope/DataScopeManager.ts +0 -269
  36. package/src/data-scope/ViewDataProvider.ts +0 -282
  37. package/src/data-scope/__tests__/DataScopeManager.test.ts +0 -211
  38. package/src/data-scope/__tests__/ViewDataProvider.test.ts +0 -270
  39. package/src/data-scope/index.ts +0 -24
  40. package/src/errors/__tests__/errors.test.ts +0 -292
  41. package/src/errors/index.ts +0 -269
  42. package/src/evaluator/ExpressionCache.ts +0 -206
  43. package/src/evaluator/ExpressionContext.ts +0 -118
  44. package/src/evaluator/ExpressionEvaluator.ts +0 -315
  45. package/src/evaluator/FormulaFunctions.ts +0 -398
  46. package/src/evaluator/SafeExpressionParser.ts +0 -893
  47. package/src/evaluator/__tests__/ExpressionCache.test.ts +0 -135
  48. package/src/evaluator/__tests__/ExpressionContext.test.ts +0 -110
  49. package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +0 -558
  50. package/src/evaluator/__tests__/FormulaFunctions.test.ts +0 -447
  51. package/src/evaluator/index.ts +0 -13
  52. package/src/index.ts +0 -38
  53. package/src/protocols/DndProtocol.ts +0 -168
  54. package/src/protocols/KeyboardProtocol.ts +0 -181
  55. package/src/protocols/NotificationProtocol.ts +0 -150
  56. package/src/protocols/ResponsiveProtocol.ts +0 -210
  57. package/src/protocols/SharingProtocol.ts +0 -185
  58. package/src/protocols/index.ts +0 -13
  59. package/src/query/__tests__/query-ast.test.ts +0 -211
  60. package/src/query/__tests__/window-functions.test.ts +0 -275
  61. package/src/query/index.ts +0 -7
  62. package/src/query/query-ast.ts +0 -341
  63. package/src/registry/PluginScopeImpl.ts +0 -259
  64. package/src/registry/PluginSystem.ts +0 -206
  65. package/src/registry/Registry.ts +0 -219
  66. package/src/registry/WidgetRegistry.ts +0 -316
  67. package/src/registry/__tests__/PluginSystem.test.ts +0 -309
  68. package/src/registry/__tests__/Registry.test.ts +0 -293
  69. package/src/registry/__tests__/WidgetRegistry.test.ts +0 -321
  70. package/src/registry/__tests__/plugin-scope-integration.test.ts +0 -283
  71. package/src/theme/ThemeEngine.ts +0 -530
  72. package/src/theme/__tests__/ThemeEngine.test.ts +0 -668
  73. package/src/theme/index.ts +0 -24
  74. package/src/types/index.ts +0 -21
  75. package/src/utils/__tests__/debug-collector.test.ts +0 -102
  76. package/src/utils/__tests__/debug.test.ts +0 -134
  77. package/src/utils/__tests__/expand-fields.test.ts +0 -120
  78. package/src/utils/__tests__/extract-records.test.ts +0 -50
  79. package/src/utils/__tests__/filter-converter.test.ts +0 -118
  80. package/src/utils/__tests__/merge-views-into-objects.test.ts +0 -110
  81. package/src/utils/__tests__/normalize-quick-filter.test.ts +0 -123
  82. package/src/utils/debug-collector.ts +0 -100
  83. package/src/utils/debug.ts +0 -148
  84. package/src/utils/expand-fields.ts +0 -76
  85. package/src/utils/extract-records.ts +0 -33
  86. package/src/utils/filter-converter.ts +0 -133
  87. package/src/utils/merge-views-into-objects.ts +0 -36
  88. package/src/utils/normalize-quick-filter.ts +0 -78
  89. package/src/validation/__tests__/object-validation-engine.test.ts +0 -567
  90. package/src/validation/__tests__/schema-validator.test.ts +0 -118
  91. package/src/validation/__tests__/validation-engine.test.ts +0 -102
  92. package/src/validation/index.ts +0 -10
  93. package/src/validation/schema-validator.ts +0 -344
  94. package/src/validation/validation-engine.ts +0 -528
  95. package/src/validation/validators/index.ts +0 -25
  96. package/src/validation/validators/object-validation-engine.ts +0 -722
  97. package/tsconfig.json +0 -15
  98. package/tsconfig.tsbuildinfo +0 -1
  99. 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
- });