@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
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { SemanticVersionManager, DependencyResolver } from './dependency-resolver.js';
|
|
3
|
+
import { createLogger } from './logger.js';
|
|
4
|
+
|
|
5
|
+
describe('SemanticVersionManager', () => {
|
|
6
|
+
describe('parse', () => {
|
|
7
|
+
it('should parse standard semver', () => {
|
|
8
|
+
const version = SemanticVersionManager.parse('1.2.3');
|
|
9
|
+
expect(version).toEqual({
|
|
10
|
+
major: 1,
|
|
11
|
+
minor: 2,
|
|
12
|
+
patch: 3,
|
|
13
|
+
preRelease: undefined,
|
|
14
|
+
build: undefined,
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should parse semver with pre-release', () => {
|
|
19
|
+
const version = SemanticVersionManager.parse('1.2.3-alpha.1');
|
|
20
|
+
expect(version).toEqual({
|
|
21
|
+
major: 1,
|
|
22
|
+
minor: 2,
|
|
23
|
+
patch: 3,
|
|
24
|
+
preRelease: 'alpha.1',
|
|
25
|
+
build: undefined,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should parse semver with build metadata', () => {
|
|
30
|
+
const version = SemanticVersionManager.parse('1.2.3+build.123');
|
|
31
|
+
expect(version).toEqual({
|
|
32
|
+
major: 1,
|
|
33
|
+
minor: 2,
|
|
34
|
+
patch: 3,
|
|
35
|
+
preRelease: undefined,
|
|
36
|
+
build: 'build.123',
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should parse semver with both pre-release and build', () => {
|
|
41
|
+
const version = SemanticVersionManager.parse('1.2.3-beta.2+build.456');
|
|
42
|
+
expect(version).toEqual({
|
|
43
|
+
major: 1,
|
|
44
|
+
minor: 2,
|
|
45
|
+
patch: 3,
|
|
46
|
+
preRelease: 'beta.2',
|
|
47
|
+
build: 'build.456',
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should handle v prefix', () => {
|
|
52
|
+
const version = SemanticVersionManager.parse('v1.2.3');
|
|
53
|
+
expect(version.major).toBe(1);
|
|
54
|
+
expect(version.minor).toBe(2);
|
|
55
|
+
expect(version.patch).toBe(3);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('compare', () => {
|
|
60
|
+
it('should compare major versions', () => {
|
|
61
|
+
const v1 = SemanticVersionManager.parse('2.0.0');
|
|
62
|
+
const v2 = SemanticVersionManager.parse('1.0.0');
|
|
63
|
+
expect(SemanticVersionManager.compare(v1, v2)).toBeGreaterThan(0);
|
|
64
|
+
expect(SemanticVersionManager.compare(v2, v1)).toBeLessThan(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should compare minor versions', () => {
|
|
68
|
+
const v1 = SemanticVersionManager.parse('1.2.0');
|
|
69
|
+
const v2 = SemanticVersionManager.parse('1.1.0');
|
|
70
|
+
expect(SemanticVersionManager.compare(v1, v2)).toBeGreaterThan(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should compare patch versions', () => {
|
|
74
|
+
const v1 = SemanticVersionManager.parse('1.0.2');
|
|
75
|
+
const v2 = SemanticVersionManager.parse('1.0.1');
|
|
76
|
+
expect(SemanticVersionManager.compare(v1, v2)).toBeGreaterThan(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should handle equal versions', () => {
|
|
80
|
+
const v1 = SemanticVersionManager.parse('1.2.3');
|
|
81
|
+
const v2 = SemanticVersionManager.parse('1.2.3');
|
|
82
|
+
expect(SemanticVersionManager.compare(v1, v2)).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should treat pre-release as lower than release', () => {
|
|
86
|
+
const v1 = SemanticVersionManager.parse('1.0.0-alpha');
|
|
87
|
+
const v2 = SemanticVersionManager.parse('1.0.0');
|
|
88
|
+
expect(SemanticVersionManager.compare(v1, v2)).toBeLessThan(0);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('satisfies', () => {
|
|
93
|
+
it('should match exact version', () => {
|
|
94
|
+
const version = SemanticVersionManager.parse('1.2.3');
|
|
95
|
+
expect(SemanticVersionManager.satisfies(version, '1.2.3')).toBe(true);
|
|
96
|
+
expect(SemanticVersionManager.satisfies(version, '1.2.4')).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should match caret range', () => {
|
|
100
|
+
const version = SemanticVersionManager.parse('1.2.5');
|
|
101
|
+
expect(SemanticVersionManager.satisfies(version, '^1.2.3')).toBe(true);
|
|
102
|
+
expect(SemanticVersionManager.satisfies(version, '^1.3.0')).toBe(false);
|
|
103
|
+
expect(SemanticVersionManager.satisfies(version, '^2.0.0')).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should match tilde range', () => {
|
|
107
|
+
const version = SemanticVersionManager.parse('1.2.5');
|
|
108
|
+
expect(SemanticVersionManager.satisfies(version, '~1.2.3')).toBe(true);
|
|
109
|
+
expect(SemanticVersionManager.satisfies(version, '~1.3.0')).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should match greater than or equal', () => {
|
|
113
|
+
const version = SemanticVersionManager.parse('1.2.5');
|
|
114
|
+
expect(SemanticVersionManager.satisfies(version, '>=1.2.3')).toBe(true);
|
|
115
|
+
expect(SemanticVersionManager.satisfies(version, '>=1.2.5')).toBe(true);
|
|
116
|
+
expect(SemanticVersionManager.satisfies(version, '>=1.3.0')).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should match less than', () => {
|
|
120
|
+
const version = SemanticVersionManager.parse('1.2.5');
|
|
121
|
+
expect(SemanticVersionManager.satisfies(version, '<1.3.0')).toBe(true);
|
|
122
|
+
expect(SemanticVersionManager.satisfies(version, '<1.2.5')).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should match range', () => {
|
|
126
|
+
const version = SemanticVersionManager.parse('1.2.5');
|
|
127
|
+
expect(SemanticVersionManager.satisfies(version, '1.2.0 - 1.3.0')).toBe(true);
|
|
128
|
+
expect(SemanticVersionManager.satisfies(version, '1.3.0 - 1.4.0')).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should match wildcard', () => {
|
|
132
|
+
const version = SemanticVersionManager.parse('1.2.5');
|
|
133
|
+
expect(SemanticVersionManager.satisfies(version, '*')).toBe(true);
|
|
134
|
+
expect(SemanticVersionManager.satisfies(version, 'latest')).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('getCompatibilityLevel', () => {
|
|
139
|
+
it('should detect fully compatible versions', () => {
|
|
140
|
+
const from = SemanticVersionManager.parse('1.2.3');
|
|
141
|
+
const to = SemanticVersionManager.parse('1.2.3');
|
|
142
|
+
expect(SemanticVersionManager.getCompatibilityLevel(from, to)).toBe('fully-compatible');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should detect backward compatible versions', () => {
|
|
146
|
+
const from = SemanticVersionManager.parse('1.2.3');
|
|
147
|
+
const to = SemanticVersionManager.parse('1.3.0');
|
|
148
|
+
expect(SemanticVersionManager.getCompatibilityLevel(from, to)).toBe('backward-compatible');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should detect breaking changes', () => {
|
|
152
|
+
const from = SemanticVersionManager.parse('1.2.3');
|
|
153
|
+
const to = SemanticVersionManager.parse('2.0.0');
|
|
154
|
+
expect(SemanticVersionManager.getCompatibilityLevel(from, to)).toBe('breaking-changes');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should detect incompatible (downgrade)', () => {
|
|
158
|
+
const from = SemanticVersionManager.parse('1.3.0');
|
|
159
|
+
const to = SemanticVersionManager.parse('1.2.0');
|
|
160
|
+
expect(SemanticVersionManager.getCompatibilityLevel(from, to)).toBe('incompatible');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('DependencyResolver', () => {
|
|
166
|
+
let resolver: DependencyResolver;
|
|
167
|
+
let logger: ReturnType<typeof createLogger>;
|
|
168
|
+
|
|
169
|
+
beforeEach(() => {
|
|
170
|
+
logger = createLogger({ level: 'silent' });
|
|
171
|
+
resolver = new DependencyResolver(logger);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('resolve', () => {
|
|
175
|
+
it('should resolve dependencies in topological order', () => {
|
|
176
|
+
const plugins = new Map([
|
|
177
|
+
['a', { dependencies: [] }],
|
|
178
|
+
['b', { dependencies: ['a'] }],
|
|
179
|
+
['c', { dependencies: ['a', 'b'] }],
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
const order = resolver.resolve(plugins);
|
|
183
|
+
|
|
184
|
+
expect(order.indexOf('a')).toBeLessThan(order.indexOf('b'));
|
|
185
|
+
expect(order.indexOf('b')).toBeLessThan(order.indexOf('c'));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should handle plugins with no dependencies', () => {
|
|
189
|
+
const plugins = new Map([
|
|
190
|
+
['a', { dependencies: [] }],
|
|
191
|
+
['b', { dependencies: [] }],
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
const order = resolver.resolve(plugins);
|
|
195
|
+
expect(order).toHaveLength(2);
|
|
196
|
+
expect(order).toContain('a');
|
|
197
|
+
expect(order).toContain('b');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should detect circular dependencies', () => {
|
|
201
|
+
const plugins = new Map([
|
|
202
|
+
['a', { dependencies: ['b'] }],
|
|
203
|
+
['b', { dependencies: ['a'] }],
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
expect(() => resolver.resolve(plugins)).toThrow('Circular dependency');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should detect missing dependencies', () => {
|
|
210
|
+
const plugins = new Map([
|
|
211
|
+
['a', { dependencies: ['missing'] }],
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
expect(() => resolver.resolve(plugins)).toThrow('Missing dependency');
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('detectConflicts', () => {
|
|
219
|
+
it('should detect version mismatches', () => {
|
|
220
|
+
const plugins = new Map<string, any>([
|
|
221
|
+
['core', { version: '1.0.0', dependencies: {} }],
|
|
222
|
+
['plugin-a', { version: '1.0.0', dependencies: { core: '^2.0.0' } }],
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
const conflicts = resolver.detectConflicts(plugins);
|
|
226
|
+
expect(conflicts.length).toBeGreaterThan(0);
|
|
227
|
+
expect(conflicts[0].type).toBe('version-mismatch');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should return no conflicts for compatible versions', () => {
|
|
231
|
+
const plugins = new Map<string, any>([
|
|
232
|
+
['core', { version: '1.2.0', dependencies: {} }],
|
|
233
|
+
['plugin-a', { version: '1.0.0', dependencies: { core: '^1.0.0' } }],
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
const conflicts = resolver.detectConflicts(plugins);
|
|
237
|
+
expect(conflicts.length).toBe(0);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('findBestVersion', () => {
|
|
242
|
+
it('should find highest matching version', () => {
|
|
243
|
+
const available = ['1.0.0', '1.1.0', '1.2.0', '2.0.0'];
|
|
244
|
+
const constraints = ['^1.0.0'];
|
|
245
|
+
|
|
246
|
+
const best = resolver.findBestVersion(available, constraints);
|
|
247
|
+
expect(best).toBe('1.2.0');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should satisfy all constraints', () => {
|
|
251
|
+
const available = ['1.0.0', '1.1.0', '1.2.0', '2.0.0'];
|
|
252
|
+
const constraints = ['^1.0.0', '>=1.1.0', '<2.0.0'];
|
|
253
|
+
|
|
254
|
+
const best = resolver.findBestVersion(available, constraints);
|
|
255
|
+
expect(best).toBe('1.2.0');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should return undefined if no version satisfies', () => {
|
|
259
|
+
const available = ['1.0.0', '1.1.0'];
|
|
260
|
+
const constraints = ['^2.0.0'];
|
|
261
|
+
|
|
262
|
+
const best = resolver.findBestVersion(available, constraints);
|
|
263
|
+
expect(best).toBeUndefined();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('isAcyclic', () => {
|
|
268
|
+
it('should detect acyclic graph', () => {
|
|
269
|
+
const deps = new Map([
|
|
270
|
+
['a', []],
|
|
271
|
+
['b', ['a']],
|
|
272
|
+
['c', ['a', 'b']],
|
|
273
|
+
]);
|
|
274
|
+
|
|
275
|
+
expect(resolver.isAcyclic(deps)).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should detect cyclic graph', () => {
|
|
279
|
+
const deps = new Map([
|
|
280
|
+
['a', ['b']],
|
|
281
|
+
['b', ['a']],
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
expect(resolver.isAcyclic(deps)).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SemanticVersion,
|
|
3
|
+
VersionConstraint,
|
|
4
|
+
CompatibilityLevel,
|
|
5
|
+
DependencyConflict
|
|
6
|
+
} from '@objectstack/spec/system';
|
|
7
|
+
import type { ObjectLogger } from './logger.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Semantic Version Parser and Comparator
|
|
11
|
+
*
|
|
12
|
+
* Implements semantic versioning comparison and constraint matching
|
|
13
|
+
*/
|
|
14
|
+
export class SemanticVersionManager {
|
|
15
|
+
/**
|
|
16
|
+
* Parse a version string into semantic version components
|
|
17
|
+
*/
|
|
18
|
+
static parse(versionStr: string): SemanticVersion {
|
|
19
|
+
// Remove 'v' prefix if present
|
|
20
|
+
const cleanVersion = versionStr.replace(/^v/, '');
|
|
21
|
+
|
|
22
|
+
// Match semver pattern: major.minor.patch[-prerelease][+build]
|
|
23
|
+
const match = cleanVersion.match(
|
|
24
|
+
/^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9.-]+))?(?:\+([a-zA-Z0-9.-]+))?$/
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (!match) {
|
|
28
|
+
throw new Error(`Invalid semantic version: ${versionStr}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
major: parseInt(match[1], 10),
|
|
33
|
+
minor: parseInt(match[2], 10),
|
|
34
|
+
patch: parseInt(match[3], 10),
|
|
35
|
+
preRelease: match[4],
|
|
36
|
+
build: match[5],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Convert semantic version back to string
|
|
42
|
+
*/
|
|
43
|
+
static toString(version: SemanticVersion): string {
|
|
44
|
+
let str = `${version.major}.${version.minor}.${version.patch}`;
|
|
45
|
+
if (version.preRelease) {
|
|
46
|
+
str += `-${version.preRelease}`;
|
|
47
|
+
}
|
|
48
|
+
if (version.build) {
|
|
49
|
+
str += `+${version.build}`;
|
|
50
|
+
}
|
|
51
|
+
return str;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compare two semantic versions
|
|
56
|
+
* Returns: -1 if a < b, 0 if a === b, 1 if a > b
|
|
57
|
+
*/
|
|
58
|
+
static compare(a: SemanticVersion, b: SemanticVersion): number {
|
|
59
|
+
// Compare major, minor, patch
|
|
60
|
+
if (a.major !== b.major) return a.major - b.major;
|
|
61
|
+
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
62
|
+
if (a.patch !== b.patch) return a.patch - b.patch;
|
|
63
|
+
|
|
64
|
+
// Pre-release versions have lower precedence
|
|
65
|
+
if (a.preRelease && !b.preRelease) return -1;
|
|
66
|
+
if (!a.preRelease && b.preRelease) return 1;
|
|
67
|
+
|
|
68
|
+
// Compare pre-release versions
|
|
69
|
+
if (a.preRelease && b.preRelease) {
|
|
70
|
+
return a.preRelease.localeCompare(b.preRelease);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if version satisfies constraint
|
|
78
|
+
*/
|
|
79
|
+
static satisfies(version: SemanticVersion, constraint: VersionConstraint): boolean {
|
|
80
|
+
const constraintStr = constraint as string;
|
|
81
|
+
|
|
82
|
+
// Any version
|
|
83
|
+
if (constraintStr === '*' || constraintStr === 'latest') {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Exact version
|
|
88
|
+
if (/^[\d.]+$/.test(constraintStr)) {
|
|
89
|
+
const exact = this.parse(constraintStr);
|
|
90
|
+
return this.compare(version, exact) === 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Caret range (^): Compatible with version
|
|
94
|
+
if (constraintStr.startsWith('^')) {
|
|
95
|
+
const base = this.parse(constraintStr.slice(1));
|
|
96
|
+
return (
|
|
97
|
+
version.major === base.major &&
|
|
98
|
+
this.compare(version, base) >= 0
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Tilde range (~): Approximately equivalent
|
|
103
|
+
if (constraintStr.startsWith('~')) {
|
|
104
|
+
const base = this.parse(constraintStr.slice(1));
|
|
105
|
+
return (
|
|
106
|
+
version.major === base.major &&
|
|
107
|
+
version.minor === base.minor &&
|
|
108
|
+
this.compare(version, base) >= 0
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Greater than or equal
|
|
113
|
+
if (constraintStr.startsWith('>=')) {
|
|
114
|
+
const base = this.parse(constraintStr.slice(2));
|
|
115
|
+
return this.compare(version, base) >= 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Greater than
|
|
119
|
+
if (constraintStr.startsWith('>')) {
|
|
120
|
+
const base = this.parse(constraintStr.slice(1));
|
|
121
|
+
return this.compare(version, base) > 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Less than or equal
|
|
125
|
+
if (constraintStr.startsWith('<=')) {
|
|
126
|
+
const base = this.parse(constraintStr.slice(2));
|
|
127
|
+
return this.compare(version, base) <= 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Less than
|
|
131
|
+
if (constraintStr.startsWith('<')) {
|
|
132
|
+
const base = this.parse(constraintStr.slice(1));
|
|
133
|
+
return this.compare(version, base) < 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Range (1.2.3 - 2.3.4)
|
|
137
|
+
const rangeMatch = constraintStr.match(/^([\d.]+)\s*-\s*([\d.]+)$/);
|
|
138
|
+
if (rangeMatch) {
|
|
139
|
+
const min = this.parse(rangeMatch[1]);
|
|
140
|
+
const max = this.parse(rangeMatch[2]);
|
|
141
|
+
return this.compare(version, min) >= 0 && this.compare(version, max) <= 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Determine compatibility level between two versions
|
|
149
|
+
*/
|
|
150
|
+
static getCompatibilityLevel(from: SemanticVersion, to: SemanticVersion): CompatibilityLevel {
|
|
151
|
+
const cmp = this.compare(from, to);
|
|
152
|
+
|
|
153
|
+
// Same version
|
|
154
|
+
if (cmp === 0) {
|
|
155
|
+
return 'fully-compatible';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Major version changed - breaking changes
|
|
159
|
+
if (from.major !== to.major) {
|
|
160
|
+
return 'breaking-changes';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Minor version increased - backward compatible
|
|
164
|
+
if (from.minor < to.minor) {
|
|
165
|
+
return 'backward-compatible';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Patch version increased - fully compatible
|
|
169
|
+
if (from.patch < to.patch) {
|
|
170
|
+
return 'fully-compatible';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Downgrade - incompatible
|
|
174
|
+
return 'incompatible';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Plugin Dependency Resolver
|
|
180
|
+
*
|
|
181
|
+
* Resolves plugin dependencies using topological sorting and conflict detection
|
|
182
|
+
*/
|
|
183
|
+
export class DependencyResolver {
|
|
184
|
+
private logger: ObjectLogger;
|
|
185
|
+
|
|
186
|
+
constructor(logger: ObjectLogger) {
|
|
187
|
+
this.logger = logger.child({ component: 'DependencyResolver' });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Resolve dependencies using topological sort
|
|
192
|
+
*/
|
|
193
|
+
resolve(
|
|
194
|
+
plugins: Map<string, { version?: string; dependencies?: string[] }>
|
|
195
|
+
): string[] {
|
|
196
|
+
const graph = new Map<string, string[]>();
|
|
197
|
+
const inDegree = new Map<string, number>();
|
|
198
|
+
|
|
199
|
+
// Build dependency graph
|
|
200
|
+
for (const [pluginName, pluginInfo] of plugins) {
|
|
201
|
+
if (!graph.has(pluginName)) {
|
|
202
|
+
graph.set(pluginName, []);
|
|
203
|
+
inDegree.set(pluginName, 0);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const deps = pluginInfo.dependencies || [];
|
|
207
|
+
for (const dep of deps) {
|
|
208
|
+
// Check if dependency exists
|
|
209
|
+
if (!plugins.has(dep)) {
|
|
210
|
+
throw new Error(`Missing dependency: ${pluginName} requires ${dep}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Add edge
|
|
214
|
+
if (!graph.has(dep)) {
|
|
215
|
+
graph.set(dep, []);
|
|
216
|
+
inDegree.set(dep, 0);
|
|
217
|
+
}
|
|
218
|
+
graph.get(dep)!.push(pluginName);
|
|
219
|
+
inDegree.set(pluginName, (inDegree.get(pluginName) || 0) + 1);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Topological sort using Kahn's algorithm
|
|
224
|
+
const queue: string[] = [];
|
|
225
|
+
const result: string[] = [];
|
|
226
|
+
|
|
227
|
+
// Add all nodes with no incoming edges
|
|
228
|
+
for (const [node, degree] of inDegree) {
|
|
229
|
+
if (degree === 0) {
|
|
230
|
+
queue.push(node);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
while (queue.length > 0) {
|
|
235
|
+
const node = queue.shift()!;
|
|
236
|
+
result.push(node);
|
|
237
|
+
|
|
238
|
+
// Reduce in-degree for dependent nodes
|
|
239
|
+
const dependents = graph.get(node) || [];
|
|
240
|
+
for (const dependent of dependents) {
|
|
241
|
+
const newDegree = (inDegree.get(dependent) || 0) - 1;
|
|
242
|
+
inDegree.set(dependent, newDegree);
|
|
243
|
+
|
|
244
|
+
if (newDegree === 0) {
|
|
245
|
+
queue.push(dependent);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check for circular dependencies
|
|
251
|
+
if (result.length !== plugins.size) {
|
|
252
|
+
const remaining = Array.from(plugins.keys()).filter(p => !result.includes(p));
|
|
253
|
+
this.logger.error('Circular dependency detected', { remaining });
|
|
254
|
+
throw new Error(`Circular dependency detected among: ${remaining.join(', ')}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this.logger.debug('Dependencies resolved', { order: result });
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Detect dependency conflicts
|
|
263
|
+
*/
|
|
264
|
+
detectConflicts(
|
|
265
|
+
plugins: Map<string, { version: string; dependencies?: Record<string, VersionConstraint> }>
|
|
266
|
+
): DependencyConflict[] {
|
|
267
|
+
const conflicts: DependencyConflict[] = [];
|
|
268
|
+
const versionRequirements = new Map<string, Map<string, VersionConstraint>>();
|
|
269
|
+
|
|
270
|
+
// Collect all version requirements
|
|
271
|
+
for (const [pluginName, pluginInfo] of plugins) {
|
|
272
|
+
if (!pluginInfo.dependencies) continue;
|
|
273
|
+
|
|
274
|
+
for (const [depName, constraint] of Object.entries(pluginInfo.dependencies)) {
|
|
275
|
+
if (!versionRequirements.has(depName)) {
|
|
276
|
+
versionRequirements.set(depName, new Map());
|
|
277
|
+
}
|
|
278
|
+
versionRequirements.get(depName)!.set(pluginName, constraint);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Check for version mismatches
|
|
283
|
+
for (const [depName, requirements] of versionRequirements) {
|
|
284
|
+
const depInfo = plugins.get(depName);
|
|
285
|
+
if (!depInfo) continue;
|
|
286
|
+
|
|
287
|
+
const depVersion = SemanticVersionManager.parse(depInfo.version);
|
|
288
|
+
const unsatisfied: Array<{ pluginId: string; version: string }> = [];
|
|
289
|
+
|
|
290
|
+
for (const [requiringPlugin, constraint] of requirements) {
|
|
291
|
+
if (!SemanticVersionManager.satisfies(depVersion, constraint)) {
|
|
292
|
+
unsatisfied.push({
|
|
293
|
+
pluginId: requiringPlugin,
|
|
294
|
+
version: constraint as string,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (unsatisfied.length > 0) {
|
|
300
|
+
conflicts.push({
|
|
301
|
+
type: 'version-mismatch',
|
|
302
|
+
severity: 'error',
|
|
303
|
+
description: `Version mismatch for ${depName}: detected ${unsatisfied.length} unsatisfied requirements`,
|
|
304
|
+
plugins: [
|
|
305
|
+
{ pluginId: depName, version: depInfo.version },
|
|
306
|
+
...unsatisfied,
|
|
307
|
+
],
|
|
308
|
+
resolutions: [{
|
|
309
|
+
strategy: 'upgrade',
|
|
310
|
+
description: `Upgrade ${depName} to satisfy all constraints`,
|
|
311
|
+
targetPlugins: [depName],
|
|
312
|
+
automatic: false,
|
|
313
|
+
} as any],
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Check for circular dependencies (will be caught by resolve())
|
|
319
|
+
try {
|
|
320
|
+
this.resolve(new Map(
|
|
321
|
+
Array.from(plugins.entries()).map(([name, info]) => [
|
|
322
|
+
name,
|
|
323
|
+
{ version: info.version, dependencies: info.dependencies ? Object.keys(info.dependencies) : [] }
|
|
324
|
+
])
|
|
325
|
+
));
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (error instanceof Error && error.message.includes('Circular dependency')) {
|
|
328
|
+
conflicts.push({
|
|
329
|
+
type: 'circular-dependency',
|
|
330
|
+
severity: 'critical',
|
|
331
|
+
description: error.message,
|
|
332
|
+
plugins: [], // Would need to extract from error
|
|
333
|
+
resolutions: [{
|
|
334
|
+
strategy: 'manual',
|
|
335
|
+
description: 'Remove circular dependency by restructuring plugins',
|
|
336
|
+
automatic: false,
|
|
337
|
+
} as any],
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return conflicts;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Find best version that satisfies all constraints
|
|
347
|
+
*/
|
|
348
|
+
findBestVersion(
|
|
349
|
+
availableVersions: string[],
|
|
350
|
+
constraints: VersionConstraint[]
|
|
351
|
+
): string | undefined {
|
|
352
|
+
// Parse and sort versions (highest first)
|
|
353
|
+
const versions = availableVersions
|
|
354
|
+
.map(v => ({ str: v, parsed: SemanticVersionManager.parse(v) }))
|
|
355
|
+
.sort((a, b) => -SemanticVersionManager.compare(a.parsed, b.parsed));
|
|
356
|
+
|
|
357
|
+
// Find highest version that satisfies all constraints
|
|
358
|
+
for (const version of versions) {
|
|
359
|
+
const satisfiesAll = constraints.every(constraint =>
|
|
360
|
+
SemanticVersionManager.satisfies(version.parsed, constraint)
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
if (satisfiesAll) {
|
|
364
|
+
return version.str;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Check if dependencies form a valid DAG (no cycles)
|
|
373
|
+
*/
|
|
374
|
+
isAcyclic(dependencies: Map<string, string[]>): boolean {
|
|
375
|
+
try {
|
|
376
|
+
const plugins = new Map(
|
|
377
|
+
Array.from(dependencies.entries()).map(([name, deps]) => [
|
|
378
|
+
name,
|
|
379
|
+
{ dependencies: deps }
|
|
380
|
+
])
|
|
381
|
+
);
|
|
382
|
+
this.resolve(plugins);
|
|
383
|
+
return true;
|
|
384
|
+
} catch {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|