@object-ui/core 0.3.1 → 2.0.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.
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +11 -0
- package/dist/actions/ActionRunner.d.ts +228 -4
- package/dist/actions/ActionRunner.js +397 -45
- package/dist/actions/TransactionManager.d.ts +193 -0
- package/dist/actions/TransactionManager.js +410 -0
- package/dist/actions/index.d.ts +2 -1
- package/dist/actions/index.js +2 -1
- package/dist/adapters/ApiDataSource.d.ts +69 -0
- package/dist/adapters/ApiDataSource.js +293 -0
- package/dist/adapters/ValueDataSource.d.ts +55 -0
- package/dist/adapters/ValueDataSource.js +287 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.js +5 -2
- package/dist/adapters/resolveDataSource.d.ts +40 -0
- package/dist/adapters/resolveDataSource.js +59 -0
- package/dist/data-scope/DataScopeManager.d.ts +127 -0
- package/dist/data-scope/DataScopeManager.js +229 -0
- package/dist/data-scope/index.d.ts +10 -0
- package/dist/data-scope/index.js +10 -0
- package/dist/evaluator/ExpressionCache.d.ts +101 -0
- package/dist/evaluator/ExpressionCache.js +135 -0
- package/dist/evaluator/ExpressionEvaluator.d.ts +30 -2
- package/dist/evaluator/ExpressionEvaluator.js +60 -16
- package/dist/evaluator/FormulaFunctions.d.ts +58 -0
- package/dist/evaluator/FormulaFunctions.js +350 -0
- package/dist/evaluator/index.d.ts +4 -2
- package/dist/evaluator/index.js +4 -2
- package/dist/index.d.ts +14 -7
- package/dist/index.js +13 -9
- package/dist/query/index.d.ts +6 -0
- package/dist/query/index.js +6 -0
- package/dist/query/query-ast.d.ts +32 -0
- package/dist/query/query-ast.js +268 -0
- package/dist/registry/PluginScopeImpl.d.ts +80 -0
- package/dist/registry/PluginScopeImpl.js +243 -0
- package/dist/registry/PluginSystem.d.ts +66 -0
- package/dist/registry/PluginSystem.js +142 -0
- package/dist/registry/Registry.d.ts +83 -4
- package/dist/registry/Registry.js +113 -7
- package/dist/registry/WidgetRegistry.d.ts +120 -0
- package/dist/registry/WidgetRegistry.js +275 -0
- package/dist/theme/ThemeEngine.d.ts +82 -0
- package/dist/theme/ThemeEngine.js +400 -0
- package/dist/theme/index.d.ts +8 -0
- package/dist/theme/index.js +8 -0
- package/dist/validation/index.d.ts +9 -0
- package/dist/validation/index.js +9 -0
- package/dist/validation/validation-engine.d.ts +88 -0
- package/dist/validation/validation-engine.js +428 -0
- package/dist/validation/validators/index.d.ts +16 -0
- package/dist/validation/validators/index.js +16 -0
- package/dist/validation/validators/object-validation-engine.d.ts +118 -0
- package/dist/validation/validators/object-validation-engine.js +538 -0
- package/package.json +14 -5
- package/src/actions/ActionRunner.ts +577 -55
- package/src/actions/TransactionManager.ts +521 -0
- package/src/actions/__tests__/ActionRunner.params.test.ts +134 -0
- package/src/actions/__tests__/ActionRunner.test.ts +711 -0
- package/src/actions/__tests__/TransactionManager.test.ts +447 -0
- package/src/actions/index.ts +2 -1
- package/src/adapters/ApiDataSource.ts +349 -0
- package/src/adapters/ValueDataSource.ts +332 -0
- package/src/adapters/__tests__/ApiDataSource.test.ts +418 -0
- package/src/adapters/__tests__/ValueDataSource.test.ts +325 -0
- package/src/adapters/__tests__/resolveDataSource.test.ts +144 -0
- package/src/adapters/index.ts +6 -1
- package/src/adapters/resolveDataSource.ts +79 -0
- package/src/builder/__tests__/schema-builder.test.ts +235 -0
- package/src/data-scope/DataScopeManager.ts +269 -0
- package/src/data-scope/__tests__/DataScopeManager.test.ts +211 -0
- package/src/data-scope/index.ts +16 -0
- package/src/evaluator/ExpressionCache.ts +192 -0
- package/src/evaluator/ExpressionEvaluator.ts +61 -16
- package/src/evaluator/FormulaFunctions.ts +398 -0
- package/src/evaluator/__tests__/ExpressionCache.test.ts +135 -0
- package/src/evaluator/__tests__/ExpressionContext.test.ts +110 -0
- package/src/evaluator/__tests__/FormulaFunctions.test.ts +447 -0
- package/src/evaluator/index.ts +4 -2
- package/src/index.ts +14 -10
- package/src/query/__tests__/query-ast.test.ts +211 -0
- package/src/query/__tests__/window-functions.test.ts +275 -0
- package/src/query/index.ts +7 -0
- package/src/query/query-ast.ts +341 -0
- package/src/registry/PluginScopeImpl.ts +259 -0
- package/src/registry/PluginSystem.ts +161 -0
- package/src/registry/Registry.ts +136 -8
- package/src/registry/WidgetRegistry.ts +316 -0
- package/src/registry/__tests__/PluginSystem.test.ts +226 -0
- package/src/registry/__tests__/Registry.test.ts +293 -0
- package/src/registry/__tests__/WidgetRegistry.test.ts +321 -0
- package/src/registry/__tests__/plugin-scope-integration.test.ts +283 -0
- package/src/theme/ThemeEngine.ts +452 -0
- package/src/theme/__tests__/ThemeEngine.test.ts +606 -0
- package/src/theme/index.ts +22 -0
- package/src/validation/__tests__/object-validation-engine.test.ts +567 -0
- package/src/validation/__tests__/schema-validator.test.ts +118 -0
- package/src/validation/__tests__/validation-engine.test.ts +102 -0
- package/src/validation/index.ts +10 -0
- package/src/validation/validation-engine.ts +520 -0
- package/src/validation/validators/index.ts +25 -0
- package/src/validation/validators/object-validation-engine.ts +722 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +2 -0
- package/src/adapters/index.d.ts +0 -8
- package/src/adapters/index.js +0 -10
- package/src/builder/schema-builder.d.ts +0 -294
- package/src/builder/schema-builder.js +0 -503
- package/src/index.d.ts +0 -13
- package/src/index.js +0 -16
- package/src/registry/Registry.d.ts +0 -56
- package/src/registry/Registry.js +0 -43
- package/src/types/index.d.ts +0 -19
- package/src/types/index.js +0 -8
- package/src/utils/filter-converter.d.ts +0 -57
- package/src/utils/filter-converter.js +0 -100
- package/src/validation/schema-validator.d.ts +0 -94
- package/src/validation/schema-validator.js +0 -278
|
@@ -0,0 +1,316 @@
|
|
|
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
|
+
}
|
|
@@ -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
|
+
});
|