@objectstack/core 0.9.0 → 0.9.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.
- package/{ENHANCED_FEATURES.md → ADVANCED_FEATURES.md} +13 -13
- package/CHANGELOG.md +15 -0
- package/PHASE2_IMPLEMENTATION.md +388 -0
- package/README.md +60 -11
- package/REFACTORING_SUMMARY.md +40 -0
- package/dist/api-registry-plugin.test.js +20 -20
- package/dist/dependency-resolver.d.ts +62 -0
- package/dist/dependency-resolver.d.ts.map +1 -0
- package/dist/dependency-resolver.js +317 -0
- package/dist/dependency-resolver.test.d.ts +2 -0
- package/dist/dependency-resolver.test.d.ts.map +1 -0
- package/dist/dependency-resolver.test.js +241 -0
- package/dist/health-monitor.d.ts +65 -0
- package/dist/health-monitor.d.ts.map +1 -0
- package/dist/health-monitor.js +269 -0
- package/dist/health-monitor.test.d.ts +2 -0
- package/dist/health-monitor.test.d.ts.map +1 -0
- package/dist/health-monitor.test.js +68 -0
- package/dist/hot-reload.d.ts +79 -0
- package/dist/hot-reload.d.ts.map +1 -0
- package/dist/hot-reload.js +313 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/kernel-base.d.ts +2 -2
- package/dist/kernel-base.js +2 -2
- package/dist/kernel.d.ts +79 -31
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +383 -73
- package/dist/kernel.test.js +373 -122
- package/dist/lite-kernel.d.ts +55 -0
- package/dist/lite-kernel.d.ts.map +1 -0
- package/dist/lite-kernel.js +112 -0
- package/dist/lite-kernel.test.d.ts +2 -0
- package/dist/lite-kernel.test.d.ts.map +1 -0
- package/dist/lite-kernel.test.js +161 -0
- package/dist/logger.d.ts +3 -2
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +61 -18
- package/dist/plugin-loader.d.ts +11 -0
- package/dist/plugin-loader.d.ts.map +1 -1
- package/dist/plugin-loader.js +34 -10
- package/dist/plugin-loader.test.js +9 -0
- package/dist/security/index.d.ts +3 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +4 -0
- package/dist/security/permission-manager.d.ts +96 -0
- package/dist/security/permission-manager.d.ts.map +1 -0
- package/dist/security/permission-manager.js +235 -0
- package/dist/security/permission-manager.test.d.ts +2 -0
- package/dist/security/permission-manager.test.d.ts.map +1 -0
- package/dist/security/permission-manager.test.js +220 -0
- package/dist/security/plugin-signature-verifier.js +3 -3
- package/dist/security/sandbox-runtime.d.ts +115 -0
- package/dist/security/sandbox-runtime.d.ts.map +1 -0
- package/dist/security/sandbox-runtime.js +310 -0
- package/dist/security/security-scanner.d.ts +92 -0
- package/dist/security/security-scanner.d.ts.map +1 -0
- package/dist/security/security-scanner.js +273 -0
- package/examples/{enhanced-kernel-example.ts → kernel-features-example.ts} +6 -6
- package/examples/phase2-integration.ts +355 -0
- package/package.json +2 -2
- package/src/api-registry-plugin.test.ts +20 -20
- package/src/dependency-resolver.test.ts +287 -0
- package/src/dependency-resolver.ts +388 -0
- package/src/health-monitor.test.ts +81 -0
- package/src/health-monitor.ts +316 -0
- package/src/hot-reload.ts +388 -0
- package/src/index.ts +6 -1
- package/src/kernel-base.ts +2 -2
- package/src/kernel.test.ts +469 -134
- package/src/kernel.ts +464 -78
- package/src/lite-kernel.test.ts +200 -0
- package/src/lite-kernel.ts +135 -0
- package/src/logger.ts +64 -18
- package/src/plugin-loader.test.ts +10 -1
- package/src/plugin-loader.ts +42 -13
- package/src/security/index.ts +19 -0
- package/src/security/permission-manager.test.ts +256 -0
- package/src/security/permission-manager.ts +336 -0
- package/src/security/plugin-signature-verifier.ts +3 -3
- package/src/security/sandbox-runtime.ts +432 -0
- package/src/security/security-scanner.ts +365 -0
- package/dist/enhanced-kernel.d.ts +0 -103
- package/dist/enhanced-kernel.d.ts.map +0 -1
- package/dist/enhanced-kernel.js +0 -403
- package/dist/enhanced-kernel.test.d.ts +0 -2
- package/dist/enhanced-kernel.test.d.ts.map +0 -1
- package/dist/enhanced-kernel.test.js +0 -412
- package/src/enhanced-kernel.test.ts +0 -535
- package/src/enhanced-kernel.ts +0 -496
|
@@ -9,7 +9,7 @@ describe('API Registry Plugin', () => {
|
|
|
9
9
|
});
|
|
10
10
|
describe('Plugin Registration', () => {
|
|
11
11
|
it('should register API Registry as a service', async () => {
|
|
12
|
-
kernel.use(createApiRegistryPlugin());
|
|
12
|
+
await kernel.use(createApiRegistryPlugin());
|
|
13
13
|
await kernel.bootstrap();
|
|
14
14
|
const registry = kernel.getService('api-registry');
|
|
15
15
|
expect(registry).toBeDefined();
|
|
@@ -17,7 +17,7 @@ describe('API Registry Plugin', () => {
|
|
|
17
17
|
await kernel.shutdown();
|
|
18
18
|
});
|
|
19
19
|
it('should register with custom conflict resolution', async () => {
|
|
20
|
-
kernel.use(createApiRegistryPlugin({
|
|
20
|
+
await kernel.use(createApiRegistryPlugin({
|
|
21
21
|
conflictResolution: 'priority',
|
|
22
22
|
version: '2.0.0',
|
|
23
23
|
}));
|
|
@@ -31,7 +31,7 @@ describe('API Registry Plugin', () => {
|
|
|
31
31
|
});
|
|
32
32
|
describe('Integration with Plugins', () => {
|
|
33
33
|
it('should allow plugins to register APIs', async () => {
|
|
34
|
-
kernel.use(createApiRegistryPlugin());
|
|
34
|
+
await kernel.use(createApiRegistryPlugin());
|
|
35
35
|
const testPlugin = {
|
|
36
36
|
name: 'test-plugin',
|
|
37
37
|
init: async (ctx) => {
|
|
@@ -60,7 +60,7 @@ describe('API Registry Plugin', () => {
|
|
|
60
60
|
registry.registerApi(api);
|
|
61
61
|
},
|
|
62
62
|
};
|
|
63
|
-
kernel.use(testPlugin);
|
|
63
|
+
await kernel.use(testPlugin);
|
|
64
64
|
await kernel.bootstrap();
|
|
65
65
|
const registry = kernel.getService('api-registry');
|
|
66
66
|
const api = registry.getApi('test_api');
|
|
@@ -70,7 +70,7 @@ describe('API Registry Plugin', () => {
|
|
|
70
70
|
await kernel.shutdown();
|
|
71
71
|
});
|
|
72
72
|
it('should allow multiple plugins to register APIs', async () => {
|
|
73
|
-
kernel.use(createApiRegistryPlugin());
|
|
73
|
+
await kernel.use(createApiRegistryPlugin());
|
|
74
74
|
const plugin1 = {
|
|
75
75
|
name: 'plugin-1',
|
|
76
76
|
init: async (ctx) => {
|
|
@@ -112,8 +112,8 @@ describe('API Registry Plugin', () => {
|
|
|
112
112
|
});
|
|
113
113
|
},
|
|
114
114
|
};
|
|
115
|
-
kernel.use(plugin1);
|
|
116
|
-
kernel.use(plugin2);
|
|
115
|
+
await kernel.use(plugin1);
|
|
116
|
+
await kernel.use(plugin2);
|
|
117
117
|
await kernel.bootstrap();
|
|
118
118
|
const registry = kernel.getService('api-registry');
|
|
119
119
|
const stats = registry.getStats();
|
|
@@ -123,7 +123,7 @@ describe('API Registry Plugin', () => {
|
|
|
123
123
|
await kernel.shutdown();
|
|
124
124
|
});
|
|
125
125
|
it('should support API discovery across plugins', async () => {
|
|
126
|
-
kernel.use(createApiRegistryPlugin());
|
|
126
|
+
await kernel.use(createApiRegistryPlugin());
|
|
127
127
|
const dataPlugin = {
|
|
128
128
|
name: 'data-plugin',
|
|
129
129
|
init: async (ctx) => {
|
|
@@ -172,8 +172,8 @@ describe('API Registry Plugin', () => {
|
|
|
172
172
|
});
|
|
173
173
|
},
|
|
174
174
|
};
|
|
175
|
-
kernel.use(dataPlugin);
|
|
176
|
-
kernel.use(analyticsPlugin);
|
|
175
|
+
await kernel.use(dataPlugin);
|
|
176
|
+
await kernel.use(analyticsPlugin);
|
|
177
177
|
await kernel.bootstrap();
|
|
178
178
|
const registry = kernel.getService('api-registry');
|
|
179
179
|
// Find all data APIs
|
|
@@ -189,7 +189,7 @@ describe('API Registry Plugin', () => {
|
|
|
189
189
|
await kernel.shutdown();
|
|
190
190
|
});
|
|
191
191
|
it('should handle route conflicts based on strategy', async () => {
|
|
192
|
-
kernel.use(createApiRegistryPlugin({
|
|
192
|
+
await kernel.use(createApiRegistryPlugin({
|
|
193
193
|
conflictResolution: 'priority',
|
|
194
194
|
}));
|
|
195
195
|
const corePlugin = {
|
|
@@ -238,8 +238,8 @@ describe('API Registry Plugin', () => {
|
|
|
238
238
|
});
|
|
239
239
|
},
|
|
240
240
|
};
|
|
241
|
-
kernel.use(corePlugin);
|
|
242
|
-
kernel.use(pluginOverride);
|
|
241
|
+
await kernel.use(corePlugin);
|
|
242
|
+
await kernel.use(pluginOverride);
|
|
243
243
|
await kernel.bootstrap();
|
|
244
244
|
const registry = kernel.getService('api-registry');
|
|
245
245
|
const result = registry.findEndpointByRoute('GET', '/api/data/:object');
|
|
@@ -249,7 +249,7 @@ describe('API Registry Plugin', () => {
|
|
|
249
249
|
await kernel.shutdown();
|
|
250
250
|
});
|
|
251
251
|
it('should support cleanup on plugin unload', async () => {
|
|
252
|
-
kernel.use(createApiRegistryPlugin());
|
|
252
|
+
await kernel.use(createApiRegistryPlugin());
|
|
253
253
|
const dynamicPlugin = {
|
|
254
254
|
name: 'dynamic-plugin',
|
|
255
255
|
init: async (ctx) => {
|
|
@@ -275,7 +275,7 @@ describe('API Registry Plugin', () => {
|
|
|
275
275
|
// For now, we'll test the registry's unregister capability
|
|
276
276
|
},
|
|
277
277
|
};
|
|
278
|
-
kernel.use(dynamicPlugin);
|
|
278
|
+
await kernel.use(dynamicPlugin);
|
|
279
279
|
await kernel.bootstrap();
|
|
280
280
|
const registry = kernel.getService('api-registry');
|
|
281
281
|
expect(registry.getApi('dynamic_api')).toBeDefined();
|
|
@@ -287,7 +287,7 @@ describe('API Registry Plugin', () => {
|
|
|
287
287
|
});
|
|
288
288
|
describe('API Registry Lifecycle', () => {
|
|
289
289
|
it('should be available during plugin start phase', async () => {
|
|
290
|
-
kernel.use(createApiRegistryPlugin());
|
|
290
|
+
await kernel.use(createApiRegistryPlugin());
|
|
291
291
|
let registryAvailable = false;
|
|
292
292
|
const testPlugin = {
|
|
293
293
|
name: 'test-plugin',
|
|
@@ -300,13 +300,13 @@ describe('API Registry Plugin', () => {
|
|
|
300
300
|
registryAvailable = registry !== undefined;
|
|
301
301
|
},
|
|
302
302
|
};
|
|
303
|
-
kernel.use(testPlugin);
|
|
303
|
+
await kernel.use(testPlugin);
|
|
304
304
|
await kernel.bootstrap();
|
|
305
305
|
expect(registryAvailable).toBe(true);
|
|
306
306
|
await kernel.shutdown();
|
|
307
307
|
});
|
|
308
308
|
it('should provide consistent registry across all plugins', async () => {
|
|
309
|
-
kernel.use(createApiRegistryPlugin());
|
|
309
|
+
await kernel.use(createApiRegistryPlugin());
|
|
310
310
|
let registry1;
|
|
311
311
|
let registry2;
|
|
312
312
|
const plugin1 = {
|
|
@@ -321,8 +321,8 @@ describe('API Registry Plugin', () => {
|
|
|
321
321
|
registry2 = ctx.getService('api-registry');
|
|
322
322
|
},
|
|
323
323
|
};
|
|
324
|
-
kernel.use(plugin1);
|
|
325
|
-
kernel.use(plugin2);
|
|
324
|
+
await kernel.use(plugin1);
|
|
325
|
+
await kernel.use(plugin2);
|
|
326
326
|
await kernel.bootstrap();
|
|
327
327
|
// Same registry instance should be shared
|
|
328
328
|
expect(registry1).toBe(registry2);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { SemanticVersion, VersionConstraint, CompatibilityLevel, DependencyConflict } from '@objectstack/spec/system';
|
|
2
|
+
import type { ObjectLogger } from './logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* Semantic Version Parser and Comparator
|
|
5
|
+
*
|
|
6
|
+
* Implements semantic versioning comparison and constraint matching
|
|
7
|
+
*/
|
|
8
|
+
export declare class SemanticVersionManager {
|
|
9
|
+
/**
|
|
10
|
+
* Parse a version string into semantic version components
|
|
11
|
+
*/
|
|
12
|
+
static parse(versionStr: string): SemanticVersion;
|
|
13
|
+
/**
|
|
14
|
+
* Convert semantic version back to string
|
|
15
|
+
*/
|
|
16
|
+
static toString(version: SemanticVersion): string;
|
|
17
|
+
/**
|
|
18
|
+
* Compare two semantic versions
|
|
19
|
+
* Returns: -1 if a < b, 0 if a === b, 1 if a > b
|
|
20
|
+
*/
|
|
21
|
+
static compare(a: SemanticVersion, b: SemanticVersion): number;
|
|
22
|
+
/**
|
|
23
|
+
* Check if version satisfies constraint
|
|
24
|
+
*/
|
|
25
|
+
static satisfies(version: SemanticVersion, constraint: VersionConstraint): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Determine compatibility level between two versions
|
|
28
|
+
*/
|
|
29
|
+
static getCompatibilityLevel(from: SemanticVersion, to: SemanticVersion): CompatibilityLevel;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Plugin Dependency Resolver
|
|
33
|
+
*
|
|
34
|
+
* Resolves plugin dependencies using topological sorting and conflict detection
|
|
35
|
+
*/
|
|
36
|
+
export declare class DependencyResolver {
|
|
37
|
+
private logger;
|
|
38
|
+
constructor(logger: ObjectLogger);
|
|
39
|
+
/**
|
|
40
|
+
* Resolve dependencies using topological sort
|
|
41
|
+
*/
|
|
42
|
+
resolve(plugins: Map<string, {
|
|
43
|
+
version?: string;
|
|
44
|
+
dependencies?: string[];
|
|
45
|
+
}>): string[];
|
|
46
|
+
/**
|
|
47
|
+
* Detect dependency conflicts
|
|
48
|
+
*/
|
|
49
|
+
detectConflicts(plugins: Map<string, {
|
|
50
|
+
version: string;
|
|
51
|
+
dependencies?: Record<string, VersionConstraint>;
|
|
52
|
+
}>): DependencyConflict[];
|
|
53
|
+
/**
|
|
54
|
+
* Find best version that satisfies all constraints
|
|
55
|
+
*/
|
|
56
|
+
findBestVersion(availableVersions: string[], constraints: VersionConstraint[]): string | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Check if dependencies form a valid DAG (no cycles)
|
|
59
|
+
*/
|
|
60
|
+
isAcyclic(dependencies: Map<string, string[]>): boolean;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=dependency-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dependency-resolver.d.ts","sourceRoot":"","sources":["../src/dependency-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;GAIG;AACH,qBAAa,sBAAsB;IACjC;;OAEG;IACH,MAAM,CAAC,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe;IAsBjD;;OAEG;IACH,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM;IAWjD;;;OAGG;IACH,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,eAAe,GAAG,MAAM;IAkB9D;;OAEG;IACH,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,iBAAiB,GAAG,OAAO;IAoElF;;OAEG;IACH,MAAM,CAAC,qBAAqB,CAAC,IAAI,EAAE,eAAe,EAAE,EAAE,EAAE,eAAe,GAAG,kBAAkB;CA0B7F;AAED;;;;GAIG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAe;gBAEjB,MAAM,EAAE,YAAY;IAIhC;;OAEG;IACH,OAAO,CACL,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,GAClE,MAAM,EAAE;IAkEX;;OAEG;IACH,eAAe,CACb,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;KAAE,CAAC,GAC1F,kBAAkB,EAAE;IA+EvB;;OAEG;IACH,eAAe,CACb,iBAAiB,EAAE,MAAM,EAAE,EAC3B,WAAW,EAAE,iBAAiB,EAAE,GAC/B,MAAM,GAAG,SAAS;IAoBrB;;OAEG;IACH,SAAS,CAAC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO;CAcxD"}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic Version Parser and Comparator
|
|
3
|
+
*
|
|
4
|
+
* Implements semantic versioning comparison and constraint matching
|
|
5
|
+
*/
|
|
6
|
+
export class SemanticVersionManager {
|
|
7
|
+
/**
|
|
8
|
+
* Parse a version string into semantic version components
|
|
9
|
+
*/
|
|
10
|
+
static parse(versionStr) {
|
|
11
|
+
// Remove 'v' prefix if present
|
|
12
|
+
const cleanVersion = versionStr.replace(/^v/, '');
|
|
13
|
+
// Match semver pattern: major.minor.patch[-prerelease][+build]
|
|
14
|
+
const match = cleanVersion.match(/^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9.-]+))?(?:\+([a-zA-Z0-9.-]+))?$/);
|
|
15
|
+
if (!match) {
|
|
16
|
+
throw new Error(`Invalid semantic version: ${versionStr}`);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
major: parseInt(match[1], 10),
|
|
20
|
+
minor: parseInt(match[2], 10),
|
|
21
|
+
patch: parseInt(match[3], 10),
|
|
22
|
+
preRelease: match[4],
|
|
23
|
+
build: match[5],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Convert semantic version back to string
|
|
28
|
+
*/
|
|
29
|
+
static toString(version) {
|
|
30
|
+
let str = `${version.major}.${version.minor}.${version.patch}`;
|
|
31
|
+
if (version.preRelease) {
|
|
32
|
+
str += `-${version.preRelease}`;
|
|
33
|
+
}
|
|
34
|
+
if (version.build) {
|
|
35
|
+
str += `+${version.build}`;
|
|
36
|
+
}
|
|
37
|
+
return str;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Compare two semantic versions
|
|
41
|
+
* Returns: -1 if a < b, 0 if a === b, 1 if a > b
|
|
42
|
+
*/
|
|
43
|
+
static compare(a, b) {
|
|
44
|
+
// Compare major, minor, patch
|
|
45
|
+
if (a.major !== b.major)
|
|
46
|
+
return a.major - b.major;
|
|
47
|
+
if (a.minor !== b.minor)
|
|
48
|
+
return a.minor - b.minor;
|
|
49
|
+
if (a.patch !== b.patch)
|
|
50
|
+
return a.patch - b.patch;
|
|
51
|
+
// Pre-release versions have lower precedence
|
|
52
|
+
if (a.preRelease && !b.preRelease)
|
|
53
|
+
return -1;
|
|
54
|
+
if (!a.preRelease && b.preRelease)
|
|
55
|
+
return 1;
|
|
56
|
+
// Compare pre-release versions
|
|
57
|
+
if (a.preRelease && b.preRelease) {
|
|
58
|
+
return a.preRelease.localeCompare(b.preRelease);
|
|
59
|
+
}
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Check if version satisfies constraint
|
|
64
|
+
*/
|
|
65
|
+
static satisfies(version, constraint) {
|
|
66
|
+
const constraintStr = constraint;
|
|
67
|
+
// Any version
|
|
68
|
+
if (constraintStr === '*' || constraintStr === 'latest') {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
// Exact version
|
|
72
|
+
if (/^[\d.]+$/.test(constraintStr)) {
|
|
73
|
+
const exact = this.parse(constraintStr);
|
|
74
|
+
return this.compare(version, exact) === 0;
|
|
75
|
+
}
|
|
76
|
+
// Caret range (^): Compatible with version
|
|
77
|
+
if (constraintStr.startsWith('^')) {
|
|
78
|
+
const base = this.parse(constraintStr.slice(1));
|
|
79
|
+
return (version.major === base.major &&
|
|
80
|
+
this.compare(version, base) >= 0);
|
|
81
|
+
}
|
|
82
|
+
// Tilde range (~): Approximately equivalent
|
|
83
|
+
if (constraintStr.startsWith('~')) {
|
|
84
|
+
const base = this.parse(constraintStr.slice(1));
|
|
85
|
+
return (version.major === base.major &&
|
|
86
|
+
version.minor === base.minor &&
|
|
87
|
+
this.compare(version, base) >= 0);
|
|
88
|
+
}
|
|
89
|
+
// Greater than or equal
|
|
90
|
+
if (constraintStr.startsWith('>=')) {
|
|
91
|
+
const base = this.parse(constraintStr.slice(2));
|
|
92
|
+
return this.compare(version, base) >= 0;
|
|
93
|
+
}
|
|
94
|
+
// Greater than
|
|
95
|
+
if (constraintStr.startsWith('>')) {
|
|
96
|
+
const base = this.parse(constraintStr.slice(1));
|
|
97
|
+
return this.compare(version, base) > 0;
|
|
98
|
+
}
|
|
99
|
+
// Less than or equal
|
|
100
|
+
if (constraintStr.startsWith('<=')) {
|
|
101
|
+
const base = this.parse(constraintStr.slice(2));
|
|
102
|
+
return this.compare(version, base) <= 0;
|
|
103
|
+
}
|
|
104
|
+
// Less than
|
|
105
|
+
if (constraintStr.startsWith('<')) {
|
|
106
|
+
const base = this.parse(constraintStr.slice(1));
|
|
107
|
+
return this.compare(version, base) < 0;
|
|
108
|
+
}
|
|
109
|
+
// Range (1.2.3 - 2.3.4)
|
|
110
|
+
const rangeMatch = constraintStr.match(/^([\d.]+)\s*-\s*([\d.]+)$/);
|
|
111
|
+
if (rangeMatch) {
|
|
112
|
+
const min = this.parse(rangeMatch[1]);
|
|
113
|
+
const max = this.parse(rangeMatch[2]);
|
|
114
|
+
return this.compare(version, min) >= 0 && this.compare(version, max) <= 0;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Determine compatibility level between two versions
|
|
120
|
+
*/
|
|
121
|
+
static getCompatibilityLevel(from, to) {
|
|
122
|
+
const cmp = this.compare(from, to);
|
|
123
|
+
// Same version
|
|
124
|
+
if (cmp === 0) {
|
|
125
|
+
return 'fully-compatible';
|
|
126
|
+
}
|
|
127
|
+
// Major version changed - breaking changes
|
|
128
|
+
if (from.major !== to.major) {
|
|
129
|
+
return 'breaking-changes';
|
|
130
|
+
}
|
|
131
|
+
// Minor version increased - backward compatible
|
|
132
|
+
if (from.minor < to.minor) {
|
|
133
|
+
return 'backward-compatible';
|
|
134
|
+
}
|
|
135
|
+
// Patch version increased - fully compatible
|
|
136
|
+
if (from.patch < to.patch) {
|
|
137
|
+
return 'fully-compatible';
|
|
138
|
+
}
|
|
139
|
+
// Downgrade - incompatible
|
|
140
|
+
return 'incompatible';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Plugin Dependency Resolver
|
|
145
|
+
*
|
|
146
|
+
* Resolves plugin dependencies using topological sorting and conflict detection
|
|
147
|
+
*/
|
|
148
|
+
export class DependencyResolver {
|
|
149
|
+
constructor(logger) {
|
|
150
|
+
this.logger = logger.child({ component: 'DependencyResolver' });
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Resolve dependencies using topological sort
|
|
154
|
+
*/
|
|
155
|
+
resolve(plugins) {
|
|
156
|
+
const graph = new Map();
|
|
157
|
+
const inDegree = new Map();
|
|
158
|
+
// Build dependency graph
|
|
159
|
+
for (const [pluginName, pluginInfo] of plugins) {
|
|
160
|
+
if (!graph.has(pluginName)) {
|
|
161
|
+
graph.set(pluginName, []);
|
|
162
|
+
inDegree.set(pluginName, 0);
|
|
163
|
+
}
|
|
164
|
+
const deps = pluginInfo.dependencies || [];
|
|
165
|
+
for (const dep of deps) {
|
|
166
|
+
// Check if dependency exists
|
|
167
|
+
if (!plugins.has(dep)) {
|
|
168
|
+
throw new Error(`Missing dependency: ${pluginName} requires ${dep}`);
|
|
169
|
+
}
|
|
170
|
+
// Add edge
|
|
171
|
+
if (!graph.has(dep)) {
|
|
172
|
+
graph.set(dep, []);
|
|
173
|
+
inDegree.set(dep, 0);
|
|
174
|
+
}
|
|
175
|
+
graph.get(dep).push(pluginName);
|
|
176
|
+
inDegree.set(pluginName, (inDegree.get(pluginName) || 0) + 1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Topological sort using Kahn's algorithm
|
|
180
|
+
const queue = [];
|
|
181
|
+
const result = [];
|
|
182
|
+
// Add all nodes with no incoming edges
|
|
183
|
+
for (const [node, degree] of inDegree) {
|
|
184
|
+
if (degree === 0) {
|
|
185
|
+
queue.push(node);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
while (queue.length > 0) {
|
|
189
|
+
const node = queue.shift();
|
|
190
|
+
result.push(node);
|
|
191
|
+
// Reduce in-degree for dependent nodes
|
|
192
|
+
const dependents = graph.get(node) || [];
|
|
193
|
+
for (const dependent of dependents) {
|
|
194
|
+
const newDegree = (inDegree.get(dependent) || 0) - 1;
|
|
195
|
+
inDegree.set(dependent, newDegree);
|
|
196
|
+
if (newDegree === 0) {
|
|
197
|
+
queue.push(dependent);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Check for circular dependencies
|
|
202
|
+
if (result.length !== plugins.size) {
|
|
203
|
+
const remaining = Array.from(plugins.keys()).filter(p => !result.includes(p));
|
|
204
|
+
this.logger.error('Circular dependency detected', { remaining });
|
|
205
|
+
throw new Error(`Circular dependency detected among: ${remaining.join(', ')}`);
|
|
206
|
+
}
|
|
207
|
+
this.logger.debug('Dependencies resolved', { order: result });
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Detect dependency conflicts
|
|
212
|
+
*/
|
|
213
|
+
detectConflicts(plugins) {
|
|
214
|
+
const conflicts = [];
|
|
215
|
+
const versionRequirements = new Map();
|
|
216
|
+
// Collect all version requirements
|
|
217
|
+
for (const [pluginName, pluginInfo] of plugins) {
|
|
218
|
+
if (!pluginInfo.dependencies)
|
|
219
|
+
continue;
|
|
220
|
+
for (const [depName, constraint] of Object.entries(pluginInfo.dependencies)) {
|
|
221
|
+
if (!versionRequirements.has(depName)) {
|
|
222
|
+
versionRequirements.set(depName, new Map());
|
|
223
|
+
}
|
|
224
|
+
versionRequirements.get(depName).set(pluginName, constraint);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Check for version mismatches
|
|
228
|
+
for (const [depName, requirements] of versionRequirements) {
|
|
229
|
+
const depInfo = plugins.get(depName);
|
|
230
|
+
if (!depInfo)
|
|
231
|
+
continue;
|
|
232
|
+
const depVersion = SemanticVersionManager.parse(depInfo.version);
|
|
233
|
+
const unsatisfied = [];
|
|
234
|
+
for (const [requiringPlugin, constraint] of requirements) {
|
|
235
|
+
if (!SemanticVersionManager.satisfies(depVersion, constraint)) {
|
|
236
|
+
unsatisfied.push({
|
|
237
|
+
pluginId: requiringPlugin,
|
|
238
|
+
version: constraint,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (unsatisfied.length > 0) {
|
|
243
|
+
conflicts.push({
|
|
244
|
+
type: 'version-mismatch',
|
|
245
|
+
severity: 'error',
|
|
246
|
+
description: `Version mismatch for ${depName}: detected ${unsatisfied.length} unsatisfied requirements`,
|
|
247
|
+
plugins: [
|
|
248
|
+
{ pluginId: depName, version: depInfo.version },
|
|
249
|
+
...unsatisfied,
|
|
250
|
+
],
|
|
251
|
+
resolutions: [{
|
|
252
|
+
strategy: 'upgrade',
|
|
253
|
+
description: `Upgrade ${depName} to satisfy all constraints`,
|
|
254
|
+
targetPlugins: [depName],
|
|
255
|
+
automatic: false,
|
|
256
|
+
}],
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Check for circular dependencies (will be caught by resolve())
|
|
261
|
+
try {
|
|
262
|
+
this.resolve(new Map(Array.from(plugins.entries()).map(([name, info]) => [
|
|
263
|
+
name,
|
|
264
|
+
{ version: info.version, dependencies: info.dependencies ? Object.keys(info.dependencies) : [] }
|
|
265
|
+
])));
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
if (error instanceof Error && error.message.includes('Circular dependency')) {
|
|
269
|
+
conflicts.push({
|
|
270
|
+
type: 'circular-dependency',
|
|
271
|
+
severity: 'critical',
|
|
272
|
+
description: error.message,
|
|
273
|
+
plugins: [], // Would need to extract from error
|
|
274
|
+
resolutions: [{
|
|
275
|
+
strategy: 'manual',
|
|
276
|
+
description: 'Remove circular dependency by restructuring plugins',
|
|
277
|
+
automatic: false,
|
|
278
|
+
}],
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return conflicts;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Find best version that satisfies all constraints
|
|
286
|
+
*/
|
|
287
|
+
findBestVersion(availableVersions, constraints) {
|
|
288
|
+
// Parse and sort versions (highest first)
|
|
289
|
+
const versions = availableVersions
|
|
290
|
+
.map(v => ({ str: v, parsed: SemanticVersionManager.parse(v) }))
|
|
291
|
+
.sort((a, b) => -SemanticVersionManager.compare(a.parsed, b.parsed));
|
|
292
|
+
// Find highest version that satisfies all constraints
|
|
293
|
+
for (const version of versions) {
|
|
294
|
+
const satisfiesAll = constraints.every(constraint => SemanticVersionManager.satisfies(version.parsed, constraint));
|
|
295
|
+
if (satisfiesAll) {
|
|
296
|
+
return version.str;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Check if dependencies form a valid DAG (no cycles)
|
|
303
|
+
*/
|
|
304
|
+
isAcyclic(dependencies) {
|
|
305
|
+
try {
|
|
306
|
+
const plugins = new Map(Array.from(dependencies.entries()).map(([name, deps]) => [
|
|
307
|
+
name,
|
|
308
|
+
{ dependencies: deps }
|
|
309
|
+
]));
|
|
310
|
+
this.resolve(plugins);
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dependency-resolver.test.d.ts","sourceRoot":"","sources":["../src/dependency-resolver.test.ts"],"names":[],"mappings":""}
|