@deplens/mcp 0.1.7 → 0.1.8

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.
@@ -0,0 +1,590 @@
1
+ /**
2
+ * diff-analyzer.mjs - Semantic comparison of package versions
3
+ * Detects breaking changes, additions, and modifications
4
+ * Self-contained: no dependencies on inspect.mjs or fast-glob
5
+ */
6
+
7
+ import ts from "typescript";
8
+ import fs from "fs";
9
+ import path from "path";
10
+
11
+ /**
12
+ * Change types and their severity
13
+ */
14
+ export const ChangeType = {
15
+ REMOVED: "removed", // BREAKING
16
+ SIGNATURE_CHANGED: "changed", // Potentially BREAKING
17
+ ADDED: "added", // Safe
18
+ DEPRECATED: "deprecated", // Warning
19
+ COMPLEXITY_INCREASED: "complexity", // Info
20
+ };
21
+
22
+ export const Severity = {
23
+ BREAKING: "breaking",
24
+ WARNING: "warning",
25
+ INFO: "info",
26
+ SAFE: "safe",
27
+ };
28
+
29
+ /**
30
+ * Normalize type string for comparison
31
+ */
32
+ function normalizeType(type) {
33
+ if (!type) return "";
34
+ return type
35
+ .replace(/\s+/g, " ")
36
+ .replace(/\s*([,:<>{}()[\]|&])\s*/g, "$1")
37
+ .trim();
38
+ }
39
+
40
+ /**
41
+ * Compare function signatures
42
+ */
43
+ function compareFunctionSignatures(from, to) {
44
+ const changes = [];
45
+
46
+ // Compare parameters
47
+ const fromParams = from.params || [];
48
+ const toParams = to.params || [];
49
+
50
+ // Check for removed required params (BREAKING)
51
+ for (let i = 0; i < fromParams.length; i++) {
52
+ const fromParam = fromParams[i];
53
+ const toParam = toParams[i];
54
+
55
+ if (!toParam) {
56
+ // Parameter removed
57
+ if (!fromParam.optional) {
58
+ changes.push({
59
+ type: "param_removed",
60
+ severity: Severity.BREAKING,
61
+ detail: `Required parameter '${fromParam.name}' removed`,
62
+ });
63
+ }
64
+ } else if (normalizeType(fromParam.type) !== normalizeType(toParam.type)) {
65
+ changes.push({
66
+ type: "param_type_changed",
67
+ severity: Severity.WARNING,
68
+ detail: `Parameter '${fromParam.name}' type: ${fromParam.type} → ${toParam.type}`,
69
+ });
70
+ }
71
+ }
72
+
73
+ // Check for new required params (BREAKING)
74
+ for (let i = fromParams.length; i < toParams.length; i++) {
75
+ const toParam = toParams[i];
76
+ if (!toParam.optional && !toParam.default) {
77
+ changes.push({
78
+ type: "param_added_required",
79
+ severity: Severity.BREAKING,
80
+ detail: `New required parameter '${toParam.name}: ${toParam.type}'`,
81
+ });
82
+ } else {
83
+ changes.push({
84
+ type: "param_added_optional",
85
+ severity: Severity.SAFE,
86
+ detail: `New optional parameter '${toParam.name}?: ${toParam.type}'`,
87
+ });
88
+ }
89
+ }
90
+
91
+ // Compare return types
92
+ const fromReturn = normalizeType(from.returnType || from.type);
93
+ const toReturn = normalizeType(to.returnType || to.type);
94
+
95
+ if (fromReturn && toReturn && fromReturn !== toReturn) {
96
+ // Check if return type was narrowed (safe) or widened (potentially breaking)
97
+ changes.push({
98
+ type: "return_type_changed",
99
+ severity: Severity.WARNING,
100
+ detail: `Return type: ${fromReturn} → ${toReturn}`,
101
+ });
102
+ }
103
+
104
+ return changes;
105
+ }
106
+
107
+ /**
108
+ * Compare interface/type properties
109
+ */
110
+ function compareProperties(fromProps, toProps) {
111
+ const changes = [];
112
+ const fromMap = new Map(Object.entries(fromProps || {}));
113
+ const toMap = new Map(Object.entries(toProps || {}));
114
+
115
+ // Check removed properties
116
+ for (const [name, prop] of fromMap) {
117
+ if (!toMap.has(name)) {
118
+ changes.push({
119
+ type: "property_removed",
120
+ severity: prop.optional ? Severity.SAFE : Severity.BREAKING,
121
+ detail: `Property '${name}' removed`,
122
+ });
123
+ }
124
+ }
125
+
126
+ // Check added/changed properties
127
+ for (const [name, toProp] of toMap) {
128
+ const fromProp = fromMap.get(name);
129
+ if (!fromProp) {
130
+ changes.push({
131
+ type: "property_added",
132
+ severity: toProp.optional ? Severity.SAFE : Severity.BREAKING,
133
+ detail: `Property '${name}${toProp.optional ? "?" : ""}: ${toProp.type}' added`,
134
+ });
135
+ } else if (normalizeType(fromProp.type) !== normalizeType(toProp.type)) {
136
+ changes.push({
137
+ type: "property_type_changed",
138
+ severity: Severity.WARNING,
139
+ detail: `Property '${name}' type: ${fromProp.type} → ${toProp.type}`,
140
+ });
141
+ }
142
+ }
143
+
144
+ return changes;
145
+ }
146
+
147
+ /**
148
+ * Analyze types from a package directory - self-contained implementation
149
+ */
150
+ async function analyzePackageTypes(packageDir, options = {}) {
151
+ const pkgJsonPath = path.join(packageDir, "package.json");
152
+ if (!fs.existsSync(pkgJsonPath)) {
153
+ return { error: "package.json not found", exports: {} };
154
+ }
155
+
156
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
157
+
158
+ const result = {
159
+ version: pkg.version,
160
+ name: pkg.name,
161
+ exports: {
162
+ functions: {},
163
+ classes: {},
164
+ interfaces: {},
165
+ types: {},
166
+ enums: {},
167
+ },
168
+ };
169
+
170
+ // Find and parse all .d.ts files, following re-exports
171
+ const visited = new Set();
172
+ const allExports = {
173
+ functions: {},
174
+ classes: {},
175
+ interfaces: {},
176
+ types: {},
177
+ enums: {},
178
+ };
179
+
180
+ // Start from the main types entry point
181
+ const entryPoints = [
182
+ pkg.types,
183
+ pkg.typings,
184
+ "index.d.ts",
185
+ "lib/index.d.ts",
186
+ "dist/index.d.ts",
187
+ ].filter(Boolean);
188
+
189
+ for (const entry of entryPoints) {
190
+ const fullPath = path.join(packageDir, entry);
191
+ if (fs.existsSync(fullPath)) {
192
+ parseTypesRecursively(fullPath, packageDir, allExports, visited);
193
+ break;
194
+ }
195
+ }
196
+
197
+ result.exports = allExports;
198
+ return result;
199
+ }
200
+
201
+ /**
202
+ * Parse a .d.ts file and follow re-exports recursively
203
+ */
204
+ function parseTypesRecursively(filePath, baseDir, allExports, visited) {
205
+ if (visited.has(filePath) || !fs.existsSync(filePath)) return;
206
+ visited.add(filePath);
207
+
208
+ const content = fs.readFileSync(filePath, "utf-8");
209
+ const sourceFile = ts.createSourceFile(
210
+ path.basename(filePath),
211
+ content,
212
+ ts.ScriptTarget.Latest,
213
+ true,
214
+ );
215
+
216
+ const fileDir = path.dirname(filePath);
217
+
218
+ ts.forEachChild(sourceFile, (node) => {
219
+ // Handle export declarations: export { x } from './module'
220
+ if (ts.isExportDeclaration(node)) {
221
+ if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
222
+ const modulePath = node.moduleSpecifier.text;
223
+ const resolvedPath = resolveModulePath(modulePath, fileDir, baseDir);
224
+ if (resolvedPath) {
225
+ parseTypesRecursively(resolvedPath, baseDir, allExports, visited);
226
+ }
227
+ }
228
+ }
229
+
230
+ // Handle function declarations
231
+ if (ts.isFunctionDeclaration(node) && node.name) {
232
+ const name = node.name.text;
233
+ const params =
234
+ node.parameters?.map((p) => ({
235
+ name: p.name.getText(sourceFile),
236
+ type: p.type ? p.type.getText(sourceFile) : "any",
237
+ optional: !!p.questionToken,
238
+ })) || [];
239
+ const returnType = node.type ? node.type.getText(sourceFile) : "void";
240
+ allExports.functions[name] = { params, returnType };
241
+ }
242
+
243
+ // Handle class declarations
244
+ if (ts.isClassDeclaration(node) && node.name) {
245
+ const name = node.name.text;
246
+ const methods = {};
247
+ node.members?.forEach((member) => {
248
+ if (ts.isMethodDeclaration(member) && member.name) {
249
+ const methodName = member.name.getText(sourceFile);
250
+ const params =
251
+ member.parameters?.map((p) => ({
252
+ name: p.name.getText(sourceFile),
253
+ type: p.type ? p.type.getText(sourceFile) : "any",
254
+ optional: !!p.questionToken,
255
+ })) || [];
256
+ const returnType = member.type
257
+ ? member.type.getText(sourceFile)
258
+ : "void";
259
+ methods[methodName] = { params, returnType };
260
+ }
261
+ });
262
+ allExports.classes[name] = { methods };
263
+ }
264
+
265
+ // Handle interface declarations
266
+ if (ts.isInterfaceDeclaration(node) && node.name) {
267
+ const name = node.name.text;
268
+ const properties = {};
269
+ node.members?.forEach((member) => {
270
+ if (ts.isPropertySignature(member) && member.name) {
271
+ const propName = member.name.getText(sourceFile);
272
+ properties[propName] = {
273
+ type: member.type ? member.type.getText(sourceFile) : "any",
274
+ optional: !!member.questionToken,
275
+ };
276
+ }
277
+ });
278
+ allExports.interfaces[name] = { properties };
279
+ }
280
+
281
+ // Handle type aliases
282
+ if (ts.isTypeAliasDeclaration(node) && node.name) {
283
+ const name = node.name.text;
284
+ allExports.types[name] = {
285
+ type: node.type ? node.type.getText(sourceFile) : "unknown",
286
+ };
287
+ }
288
+
289
+ // Handle enum declarations
290
+ if (ts.isEnumDeclaration(node) && node.name) {
291
+ const name = node.name.text;
292
+ const members =
293
+ node.members?.map((m) => m.name.getText(sourceFile)) || [];
294
+ allExports.enums[name] = { members };
295
+ }
296
+ });
297
+ }
298
+
299
+ /**
300
+ * Resolve a module path to an actual file path
301
+ */
302
+ function resolveModulePath(modulePath, fromDir, baseDir) {
303
+ // Handle relative paths
304
+ if (modulePath.startsWith(".")) {
305
+ const candidates = [
306
+ path.join(fromDir, modulePath + ".d.ts"),
307
+ path.join(fromDir, modulePath, "index.d.ts"),
308
+ path.join(fromDir, modulePath + ".ts"),
309
+ path.join(fromDir, modulePath),
310
+ ];
311
+ for (const candidate of candidates) {
312
+ if (fs.existsSync(candidate)) return candidate;
313
+ }
314
+ }
315
+ return null;
316
+ }
317
+
318
+ /**
319
+ * Compare two package versions
320
+ */
321
+ export async function compareVersions(fromDir, toDir, options = {}) {
322
+ const { filter, includeSource = false } = options;
323
+
324
+ // Analyze both versions
325
+ const [fromAnalysis, toAnalysis] = await Promise.all([
326
+ analyzePackageTypes(fromDir, { filter, includeSource }),
327
+ analyzePackageTypes(toDir, { filter, includeSource }),
328
+ ]);
329
+
330
+ const diff = {
331
+ from: {
332
+ version: fromAnalysis.version,
333
+ name: fromAnalysis.name,
334
+ },
335
+ to: {
336
+ version: toAnalysis.version,
337
+ name: toAnalysis.name,
338
+ },
339
+ breaking: [],
340
+ warnings: [],
341
+ additions: [],
342
+ info: [],
343
+ summary: {
344
+ breaking: 0,
345
+ warnings: 0,
346
+ additions: 0,
347
+ removals: 0,
348
+ },
349
+ };
350
+
351
+ // Compare each export category
352
+ const categories = ["functions", "classes", "interfaces", "types", "enums"];
353
+
354
+ for (const category of categories) {
355
+ const fromExports = fromAnalysis.exports[category] || {};
356
+ const toExports = toAnalysis.exports[category] || {};
357
+
358
+ // Find removed exports (BREAKING)
359
+ for (const [name, fromItem] of Object.entries(fromExports)) {
360
+ if (!(name in toExports)) {
361
+ diff.breaking.push({
362
+ category,
363
+ name,
364
+ type: ChangeType.REMOVED,
365
+ severity: Severity.BREAKING,
366
+ detail: `${category.slice(0, -1)} '${name}' was removed`,
367
+ from: fromItem,
368
+ to: null,
369
+ });
370
+ diff.summary.breaking++;
371
+ diff.summary.removals++;
372
+ }
373
+ }
374
+
375
+ // Find added exports
376
+ for (const [name, toItem] of Object.entries(toExports)) {
377
+ if (!(name in fromExports)) {
378
+ diff.additions.push({
379
+ category,
380
+ name,
381
+ type: ChangeType.ADDED,
382
+ severity: Severity.SAFE,
383
+ detail: `${category.slice(0, -1)} '${name}' was added`,
384
+ from: null,
385
+ to: toItem,
386
+ });
387
+ diff.summary.additions++;
388
+ }
389
+ }
390
+
391
+ // Find changed exports
392
+ for (const [name, fromItem] of Object.entries(fromExports)) {
393
+ const toItem = toExports[name];
394
+ if (!toItem) continue;
395
+
396
+ let changes = [];
397
+
398
+ if (category === "functions") {
399
+ changes = compareFunctionSignatures(fromItem, toItem);
400
+ } else if (category === "interfaces" || category === "types") {
401
+ if (fromItem.properties && toItem.properties) {
402
+ changes = compareProperties(fromItem.properties, toItem.properties);
403
+ }
404
+ } else if (category === "classes") {
405
+ // Compare class methods and properties
406
+ if (fromItem.methods && toItem.methods) {
407
+ for (const [methodName, fromMethod] of Object.entries(
408
+ fromItem.methods,
409
+ )) {
410
+ const toMethod = toItem.methods[methodName];
411
+ if (!toMethod) {
412
+ changes.push({
413
+ type: "method_removed",
414
+ severity: Severity.BREAKING,
415
+ detail: `Method '${methodName}' removed from class '${name}'`,
416
+ });
417
+ } else {
418
+ const methodChanges = compareFunctionSignatures(
419
+ fromMethod,
420
+ toMethod,
421
+ );
422
+ changes.push(
423
+ ...methodChanges.map((c) => ({
424
+ ...c,
425
+ detail: `${name}.${methodName}: ${c.detail}`,
426
+ })),
427
+ );
428
+ }
429
+ }
430
+ // Check for new methods
431
+ for (const methodName of Object.keys(toItem.methods || {})) {
432
+ if (!fromItem.methods?.[methodName]) {
433
+ changes.push({
434
+ type: "method_added",
435
+ severity: Severity.SAFE,
436
+ detail: `Method '${methodName}' added to class '${name}'`,
437
+ });
438
+ }
439
+ }
440
+ }
441
+ }
442
+
443
+ // Categorize changes by severity
444
+ for (const change of changes) {
445
+ const entry = {
446
+ category,
447
+ name,
448
+ ...change,
449
+ from: fromItem,
450
+ to: toItem,
451
+ };
452
+
453
+ if (change.severity === Severity.BREAKING) {
454
+ diff.breaking.push(entry);
455
+ diff.summary.breaking++;
456
+ } else if (change.severity === Severity.WARNING) {
457
+ diff.warnings.push(entry);
458
+ diff.summary.warnings++;
459
+ } else {
460
+ diff.info.push(entry);
461
+ }
462
+ }
463
+ }
464
+ }
465
+
466
+ // Compare source complexity if available
467
+ if (
468
+ includeSource &&
469
+ fromAnalysis.sourceAnalysis &&
470
+ toAnalysis.sourceAnalysis
471
+ ) {
472
+ const fromAvg = fromAnalysis.sourceAnalysis.summary?.avgComplexity || 0;
473
+ const toAvg = toAnalysis.sourceAnalysis.summary?.avgComplexity || 0;
474
+
475
+ if (toAvg > fromAvg * 1.5) {
476
+ // 50% increase
477
+ diff.warnings.push({
478
+ category: "source",
479
+ name: "complexity",
480
+ type: ChangeType.COMPLEXITY_INCREASED,
481
+ severity: Severity.WARNING,
482
+ detail: `Average complexity increased significantly: ${fromAvg} → ${toAvg}`,
483
+ });
484
+ }
485
+
486
+ diff.sourceComparison = {
487
+ from: fromAnalysis.sourceAnalysis.summary,
488
+ to: toAnalysis.sourceAnalysis.summary,
489
+ };
490
+ }
491
+
492
+ return diff;
493
+ }
494
+
495
+ /**
496
+ * Format diff result as text
497
+ */
498
+ export function formatDiffAsText(diff, options = {}) {
499
+ const { colors = true, verbose = false } = options;
500
+
501
+ const lines = [];
502
+ const red = colors ? "\x1b[31m" : "";
503
+ const green = colors ? "\x1b[32m" : "";
504
+ const yellow = colors ? "\x1b[33m" : "";
505
+ const reset = colors ? "\x1b[0m" : "";
506
+ const bold = colors ? "\x1b[1m" : "";
507
+
508
+ lines.push(
509
+ `${bold}📦 ${diff.from.name}: ${diff.from.version} → ${diff.to.version}${reset}`,
510
+ );
511
+ lines.push("");
512
+
513
+ // Breaking changes
514
+ if (diff.breaking.length > 0) {
515
+ lines.push(
516
+ `${red}${bold}🔴 BREAKING CHANGES (${diff.breaking.length}):${reset}`,
517
+ );
518
+ for (const change of diff.breaking) {
519
+ lines.push(`${red} - ${change.detail}${reset}`);
520
+ }
521
+ lines.push("");
522
+ }
523
+
524
+ // Warnings
525
+ if (diff.warnings.length > 0) {
526
+ lines.push(
527
+ `${yellow}${bold}🟡 WARNINGS (${diff.warnings.length}):${reset}`,
528
+ );
529
+ for (const change of diff.warnings) {
530
+ lines.push(`${yellow} ~ ${change.detail}${reset}`);
531
+ }
532
+ lines.push("");
533
+ }
534
+
535
+ // Additions
536
+ if (diff.additions.length > 0) {
537
+ lines.push(`${green}${bold}🟢 ADDED (${diff.additions.length}):${reset}`);
538
+ for (const change of diff.additions) {
539
+ lines.push(
540
+ `${green} + ${change.name} (${change.category.slice(0, -1)})${reset}`,
541
+ );
542
+ }
543
+ lines.push("");
544
+ }
545
+
546
+ // Info (only in verbose mode)
547
+ if (verbose && diff.info.length > 0) {
548
+ lines.push(`ℹ️ INFO (${diff.info.length}):`);
549
+ for (const change of diff.info) {
550
+ lines.push(` · ${change.detail}`);
551
+ }
552
+ lines.push("");
553
+ }
554
+
555
+ // Summary
556
+ lines.push(`${bold}📊 Summary:${reset}`);
557
+ lines.push(` Breaking: ${diff.summary.breaking}`);
558
+ lines.push(` Warnings: ${diff.summary.warnings}`);
559
+ lines.push(` Additions: ${diff.summary.additions}`);
560
+ lines.push(` Removals: ${diff.summary.removals}`);
561
+
562
+ // Source comparison if available
563
+ if (diff.sourceComparison) {
564
+ lines.push("");
565
+ lines.push(`${bold}📝 Source Analysis:${reset}`);
566
+ lines.push(
567
+ ` Functions: ${diff.sourceComparison.from?.totalFunctions || 0} → ${diff.sourceComparison.to?.totalFunctions || 0}`,
568
+ );
569
+ lines.push(
570
+ ` Avg Complexity: ${diff.sourceComparison.from?.avgComplexity || 0} → ${diff.sourceComparison.to?.avgComplexity || 0}`,
571
+ );
572
+ }
573
+
574
+ return lines.join("\n");
575
+ }
576
+
577
+ /**
578
+ * Format diff result as JSON
579
+ */
580
+ export function formatDiffAsJson(diff) {
581
+ return JSON.stringify(diff, null, 2);
582
+ }
583
+
584
+ export default {
585
+ compareVersions,
586
+ formatDiffAsText,
587
+ formatDiffAsJson,
588
+ ChangeType,
589
+ Severity,
590
+ };