@object-ui/core 0.3.0 → 0.5.0

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 (90) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +8 -0
  3. package/dist/actions/ActionRunner.d.ts +40 -0
  4. package/dist/actions/ActionRunner.js +160 -0
  5. package/dist/actions/index.d.ts +8 -0
  6. package/dist/actions/index.js +8 -0
  7. package/dist/adapters/index.d.ts +7 -0
  8. package/dist/adapters/index.js +10 -0
  9. package/dist/builder/schema-builder.d.ts +7 -0
  10. package/dist/builder/schema-builder.js +4 -6
  11. package/dist/evaluator/ExpressionCache.d.ts +101 -0
  12. package/dist/evaluator/ExpressionCache.js +135 -0
  13. package/dist/evaluator/ExpressionContext.d.ts +51 -0
  14. package/dist/evaluator/ExpressionContext.js +110 -0
  15. package/dist/evaluator/ExpressionEvaluator.d.ts +117 -0
  16. package/dist/evaluator/ExpressionEvaluator.js +220 -0
  17. package/dist/evaluator/index.d.ts +10 -0
  18. package/dist/evaluator/index.js +10 -0
  19. package/dist/index.d.ts +17 -4
  20. package/dist/index.js +16 -5
  21. package/dist/query/index.d.ts +6 -0
  22. package/dist/query/index.js +6 -0
  23. package/dist/query/query-ast.d.ts +32 -0
  24. package/dist/query/query-ast.js +268 -0
  25. package/dist/registry/PluginScopeImpl.d.ts +80 -0
  26. package/dist/registry/PluginScopeImpl.js +243 -0
  27. package/dist/registry/PluginSystem.d.ts +66 -0
  28. package/dist/registry/PluginSystem.js +142 -0
  29. package/dist/registry/Registry.d.ts +80 -4
  30. package/dist/registry/Registry.js +119 -7
  31. package/dist/types/index.d.ts +7 -0
  32. package/dist/types/index.js +7 -0
  33. package/dist/utils/filter-converter.d.ts +57 -0
  34. package/dist/utils/filter-converter.js +100 -0
  35. package/dist/validation/index.d.ts +9 -0
  36. package/dist/validation/index.js +9 -0
  37. package/dist/validation/schema-validator.d.ts +7 -0
  38. package/dist/validation/schema-validator.js +4 -6
  39. package/dist/validation/validation-engine.d.ts +70 -0
  40. package/dist/validation/validation-engine.js +363 -0
  41. package/dist/validation/validators/index.d.ts +16 -0
  42. package/dist/validation/validators/index.js +16 -0
  43. package/dist/validation/validators/object-validation-engine.d.ts +118 -0
  44. package/dist/validation/validators/object-validation-engine.js +538 -0
  45. package/package.json +26 -7
  46. package/src/actions/ActionRunner.ts +195 -0
  47. package/src/actions/index.ts +9 -0
  48. package/src/adapters/README.md +180 -0
  49. package/src/adapters/index.ts +10 -0
  50. package/src/builder/schema-builder.ts +8 -0
  51. package/src/evaluator/ExpressionCache.ts +192 -0
  52. package/src/evaluator/ExpressionContext.ts +118 -0
  53. package/src/evaluator/ExpressionEvaluator.ts +267 -0
  54. package/src/evaluator/__tests__/ExpressionCache.test.ts +135 -0
  55. package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +101 -0
  56. package/src/evaluator/index.ts +11 -0
  57. package/src/index.test.ts +8 -0
  58. package/src/index.ts +18 -5
  59. package/src/query/__tests__/query-ast.test.ts +211 -0
  60. package/src/query/__tests__/window-functions.test.ts +275 -0
  61. package/src/query/index.ts +7 -0
  62. package/src/query/query-ast.ts +341 -0
  63. package/src/registry/PluginScopeImpl.ts +259 -0
  64. package/src/registry/PluginSystem.ts +161 -0
  65. package/src/registry/Registry.ts +133 -8
  66. package/src/registry/__tests__/PluginSystem.test.ts +226 -0
  67. package/src/registry/__tests__/Registry.test.ts +293 -0
  68. package/src/registry/__tests__/plugin-scope-integration.test.ts +283 -0
  69. package/src/types/index.ts +8 -0
  70. package/src/utils/__tests__/filter-converter.test.ts +118 -0
  71. package/src/utils/filter-converter.ts +133 -0
  72. package/src/validation/__tests__/object-validation-engine.test.ts +567 -0
  73. package/src/validation/__tests__/validation-engine.test.ts +102 -0
  74. package/src/validation/index.ts +10 -0
  75. package/src/validation/schema-validator.ts +8 -0
  76. package/src/validation/validation-engine.ts +461 -0
  77. package/src/validation/validators/index.ts +25 -0
  78. package/src/validation/validators/object-validation-engine.ts +722 -0
  79. package/tsconfig.tsbuildinfo +1 -1
  80. package/vitest.config.ts +2 -0
  81. package/src/builder/schema-builder.d.ts +0 -287
  82. package/src/builder/schema-builder.js +0 -505
  83. package/src/index.d.ts +0 -4
  84. package/src/index.js +0 -7
  85. package/src/registry/Registry.d.ts +0 -49
  86. package/src/registry/Registry.js +0 -36
  87. package/src/types/index.d.ts +0 -12
  88. package/src/types/index.js +0 -1
  89. package/src/validation/schema-validator.d.ts +0 -87
  90. package/src/validation/schema-validator.js +0 -280
@@ -0,0 +1,161 @@
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 type { Registry } from './Registry.js';
10
+ import type { PluginScope, PluginScopeConfig } from '@object-ui/types';
11
+ import { PluginScopeImpl } from './PluginScopeImpl.js';
12
+
13
+ export interface PluginDefinition {
14
+ name: string;
15
+ version: string;
16
+ dependencies?: string[]; // Dependencies on other plugins
17
+ peerDependencies?: string[]; // Peer dependencies
18
+ register: (registry: Registry | PluginScope) => void; // Support both legacy and scoped registration
19
+ onLoad?: () => void | Promise<void>; // Lifecycle hook: called after registration
20
+ onUnload?: () => void | Promise<void>; // Lifecycle hook: called before unload
21
+ scopeConfig?: PluginScopeConfig; // Optional scope configuration
22
+ }
23
+
24
+ export class PluginSystem {
25
+ private plugins = new Map<string, PluginDefinition>();
26
+ private loaded = new Set<string>();
27
+ private scopes = new Map<string, PluginScopeImpl>();
28
+
29
+ /**
30
+ * Load a plugin into the system with optional scope isolation
31
+ * @param plugin The plugin definition to load
32
+ * @param registry The component registry to use for registration
33
+ * @param useScope Whether to use scoped loading (default: true for better isolation)
34
+ * @throws Error if dependencies are missing
35
+ */
36
+ async loadPlugin(plugin: PluginDefinition, registry: Registry, useScope: boolean = true): Promise<void> {
37
+ // Check if already loaded
38
+ if (this.loaded.has(plugin.name)) {
39
+ console.warn(`Plugin "${plugin.name}" is already loaded. Skipping.`);
40
+ return;
41
+ }
42
+
43
+ // Check dependencies
44
+ for (const dep of plugin.dependencies || []) {
45
+ if (!this.loaded.has(dep)) {
46
+ throw new Error(`Missing dependency: ${dep} required by ${plugin.name}`);
47
+ }
48
+ }
49
+
50
+ try {
51
+ if (useScope) {
52
+ // Create scoped environment for plugin
53
+ const scope = new PluginScopeImpl(
54
+ plugin.name,
55
+ plugin.version,
56
+ registry,
57
+ plugin.scopeConfig
58
+ );
59
+
60
+ // Store scope for cleanup
61
+ this.scopes.set(plugin.name, scope);
62
+
63
+ // Execute registration with scope
64
+ plugin.register(scope);
65
+ } else {
66
+ // Legacy mode: direct registry access
67
+ plugin.register(registry);
68
+ }
69
+
70
+ // Store plugin definition
71
+ this.plugins.set(plugin.name, plugin);
72
+
73
+ // Execute lifecycle hook
74
+ await plugin.onLoad?.();
75
+
76
+ // Mark as loaded
77
+ this.loaded.add(plugin.name);
78
+ } catch (error) {
79
+ // Clean up on failure
80
+ this.plugins.delete(plugin.name);
81
+ this.scopes.delete(plugin.name);
82
+ throw error;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Unload a plugin from the system
88
+ * @param name The name of the plugin to unload
89
+ * @throws Error if other plugins depend on this plugin
90
+ */
91
+ async unloadPlugin(name: string): Promise<void> {
92
+ const plugin = this.plugins.get(name);
93
+ if (!plugin) {
94
+ throw new Error(`Plugin "${name}" is not loaded`);
95
+ }
96
+
97
+ // Check if any loaded plugins depend on this one
98
+ for (const [pluginName, pluginDef] of this.plugins.entries()) {
99
+ if (this.loaded.has(pluginName) && pluginDef.dependencies?.includes(name)) {
100
+ throw new Error(`Cannot unload plugin "${name}" - plugin "${pluginName}" depends on it`);
101
+ }
102
+ }
103
+
104
+ // Execute lifecycle hook
105
+ await plugin.onUnload?.();
106
+
107
+ // Clean up scope if exists
108
+ const scope = this.scopes.get(name);
109
+ if (scope) {
110
+ scope.cleanup();
111
+ this.scopes.delete(name);
112
+ }
113
+
114
+ // Remove from loaded set
115
+ this.loaded.delete(name);
116
+ this.plugins.delete(name);
117
+ }
118
+
119
+ /**
120
+ * Get the scope for a loaded plugin
121
+ * @param name The name of the plugin
122
+ * @returns The plugin scope or undefined
123
+ */
124
+ getScope(name: string): PluginScope | undefined {
125
+ return this.scopes.get(name);
126
+ }
127
+
128
+ /**
129
+ * Check if a plugin is loaded
130
+ * @param name The name of the plugin
131
+ * @returns true if the plugin is loaded
132
+ */
133
+ isLoaded(name: string): boolean {
134
+ return this.loaded.has(name);
135
+ }
136
+
137
+ /**
138
+ * Get a loaded plugin definition
139
+ * @param name The name of the plugin
140
+ * @returns The plugin definition or undefined
141
+ */
142
+ getPlugin(name: string): PluginDefinition | undefined {
143
+ return this.plugins.get(name);
144
+ }
145
+
146
+ /**
147
+ * Get all loaded plugin names
148
+ * @returns Array of loaded plugin names
149
+ */
150
+ getLoadedPlugins(): string[] {
151
+ return Array.from(this.loaded);
152
+ }
153
+
154
+ /**
155
+ * Get all plugin definitions
156
+ * @returns Array of all plugin definitions
157
+ */
158
+ getAllPlugins(): PluginDefinition[] {
159
+ return Array.from(this.plugins.values());
160
+ }
161
+ }
@@ -1,4 +1,12 @@
1
- import type { SchemaNode } from '../types';
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 type { SchemaNode } from '../types/index.js';
2
10
 
3
11
  export type ComponentRenderer<T = any> = T;
4
12
 
@@ -18,6 +26,7 @@ export type ComponentMeta = {
18
26
  label?: string; // Display name in designer
19
27
  icon?: string; // Icon name or svg string
20
28
  category?: string; // Grouping category
29
+ namespace?: string; // Component namespace (e.g., 'ui', 'plugin-grid', 'field')
21
30
  inputs?: ComponentInput[];
22
31
  defaultProps?: Record<string, any>; // Default props when dropped
23
32
  defaultChildren?: SchemaNode[]; // Default children when dropped
@@ -42,36 +51,152 @@ export type ComponentConfig<T = any> = ComponentMeta & {
42
51
  export class Registry<T = any> {
43
52
  private components = new Map<string, ComponentConfig<T>>();
44
53
 
54
+ /**
55
+ * Register a component with optional namespace support.
56
+ * If namespace is provided in meta, the component will be registered as "namespace:type".
57
+ *
58
+ * @param type - Component type identifier
59
+ * @param component - Component renderer
60
+ * @param meta - Component metadata (including optional namespace)
61
+ *
62
+ * @example
63
+ * // Register with namespace
64
+ * registry.register('button', ButtonComponent, { namespace: 'ui' });
65
+ * // Accessible as 'ui:button' or 'button' (fallback)
66
+ *
67
+ * @example
68
+ * // Register without namespace (backward compatible)
69
+ * registry.register('button', ButtonComponent);
70
+ * // Accessible as 'button'
71
+ */
45
72
  register(type: string, component: ComponentRenderer<T>, meta?: ComponentMeta) {
46
- if (this.components.has(type)) {
47
- console.warn(`Component type "${type}" is already registered. Overwriting.`);
73
+ const fullType = meta?.namespace ? `${meta.namespace}:${type}` : type;
74
+
75
+ // Warn if registering without namespace (deprecated pattern)
76
+ if (!meta?.namespace) {
77
+ console.warn(
78
+ `Registering component "${type}" without a namespace is deprecated. ` +
79
+ `Please provide a namespace in the meta parameter.`
80
+ );
81
+ }
82
+
83
+ if (this.components.has(fullType)) {
84
+ // console.warn(`Component type "${fullType}" is already registered. Overwriting.`);
48
85
  }
49
- this.components.set(type, {
50
- type,
86
+
87
+ this.components.set(fullType, {
88
+ type: fullType,
51
89
  component,
52
90
  ...meta
53
91
  });
92
+
93
+ // Also register without namespace for backward compatibility
94
+ // This allows "button" to work even when registered as "ui:button"
95
+ // Note: If multiple namespaced components share the same short name,
96
+ // the last registration wins for non-namespaced lookups
97
+ if (meta?.namespace) {
98
+ this.components.set(type, {
99
+ type: fullType, // Keep reference to namespaced type
100
+ component,
101
+ ...meta
102
+ });
103
+ }
54
104
  }
55
105
 
56
- get(type: string): ComponentRenderer<T> | undefined {
106
+ /**
107
+ * Get a component by type. Supports both namespaced and non-namespaced lookups.
108
+ *
109
+ * @param type - Component type (e.g., 'button' or 'ui:button')
110
+ * @param namespace - Optional namespace for lookup priority
111
+ * @returns Component renderer or undefined
112
+ *
113
+ * @example
114
+ * // Direct lookup
115
+ * registry.get('ui:button') // Gets ui:button
116
+ *
117
+ * @example
118
+ * // Fallback lookup
119
+ * registry.get('button') // Gets first registered button
120
+ *
121
+ * @example
122
+ * // Namespaced lookup with priority
123
+ * registry.get('button', 'ui') // Tries 'ui:button' first, then 'button'
124
+ */
125
+ get(type: string, namespace?: string): ComponentRenderer<T> | undefined {
126
+ // If namespace is explicitly provided, ONLY look in that namespace (no fallback)
127
+ if (namespace) {
128
+ const namespacedType = `${namespace}:${type}`;
129
+ return this.components.get(namespacedType)?.component;
130
+ }
131
+
132
+ // When no namespace provided, use backward compatibility lookup
57
133
  return this.components.get(type)?.component;
58
134
  }
59
135
 
60
- getConfig(type: string): ComponentConfig<T> | undefined {
136
+ /**
137
+ * Get component configuration by type with namespace support.
138
+ *
139
+ * @param type - Component type (e.g., 'button' or 'ui:button')
140
+ * @param namespace - Optional namespace for lookup priority
141
+ * @returns Component configuration or undefined
142
+ */
143
+ getConfig(type: string, namespace?: string): ComponentConfig<T> | undefined {
144
+ // If namespace is explicitly provided, ONLY look in that namespace (no fallback)
145
+ if (namespace) {
146
+ const namespacedType = `${namespace}:${type}`;
147
+ return this.components.get(namespacedType);
148
+ }
149
+
150
+ // When no namespace provided, use backward compatibility lookup
61
151
  return this.components.get(type);
62
152
  }
63
153
 
64
- has(type: string): boolean {
154
+ /**
155
+ * Check if a component type is registered.
156
+ *
157
+ * @param type - Component type (e.g., 'button' or 'ui:button')
158
+ * @param namespace - Optional namespace for lookup
159
+ * @returns True if component is registered
160
+ */
161
+ has(type: string, namespace?: string): boolean {
162
+ // If namespace is explicitly provided, ONLY look in that namespace (no fallback)
163
+ if (namespace) {
164
+ const namespacedType = `${namespace}:${type}`;
165
+ return this.components.has(namespacedType);
166
+ }
167
+ // When no namespace provided, use backward compatibility lookup
65
168
  return this.components.has(type);
66
169
  }
67
170
 
171
+ /**
172
+ * Get all registered component types.
173
+ *
174
+ * @returns Array of all component type identifiers
175
+ */
68
176
  getAllTypes(): string[] {
69
177
  return Array.from(this.components.keys());
70
178
  }
71
179
 
180
+ /**
181
+ * Get all registered component configurations.
182
+ *
183
+ * @returns Array of all component configurations
184
+ */
72
185
  getAllConfigs(): ComponentConfig<T>[] {
73
186
  return Array.from(this.components.values());
74
187
  }
188
+
189
+ /**
190
+ * Get all components in a specific namespace.
191
+ *
192
+ * @param namespace - Namespace to filter by
193
+ * @returns Array of component configurations in the namespace
194
+ */
195
+ getNamespaceComponents(namespace: string): ComponentConfig<T>[] {
196
+ return Array.from(this.components.values()).filter(
197
+ config => config.namespace === namespace
198
+ );
199
+ }
75
200
  }
76
201
 
77
202
  export const ComponentRegistry = new Registry<any>();
@@ -0,0 +1,226 @@
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
+ });