sigdiff 0.1.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/README.md +71 -0
- package/dist/classify.d.ts +2 -0
- package/dist/classify.js +126 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +40 -0
- package/dist/diff.d.ts +2 -0
- package/dist/diff.js +23 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.js +11 -0
- package/dist/extract.d.ts +3 -0
- package/dist/extract.js +210 -0
- package/dist/format.d.ts +5 -0
- package/dist/format.js +93 -0
- package/dist/git.d.ts +7 -0
- package/dist/git.js +147 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.js +2 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# sigdiff
|
|
2
|
+
|
|
3
|
+
Diffs the public API surface of a TypeScript project between two git refs and outputs a structured changelog.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx sigdiff v1.0.0..v2.0.0
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
Commit messages are a human interpretation of a change — imprecise, incomplete, or missing entirely. `sigdiff` adds a second perspective: what the code actually exported. It catches things commit messages miss, like a signature change buried in a large PR.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Compare last tag to HEAD
|
|
17
|
+
npx sigdiff
|
|
18
|
+
|
|
19
|
+
# Compare two refs
|
|
20
|
+
npx sigdiff v1.0.0..v2.0.0
|
|
21
|
+
|
|
22
|
+
# Scope to a specific entrypoint
|
|
23
|
+
npx sigdiff --entrypoint src/index.ts
|
|
24
|
+
|
|
25
|
+
# JSON output
|
|
26
|
+
npx sigdiff --json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## What it detects
|
|
30
|
+
|
|
31
|
+
Functions, arrow functions, interfaces, type aliases, enums, classes, and constants — parameters, return types, property shapes, and member names.
|
|
32
|
+
|
|
33
|
+
Changes are classified as `major`, `minor`, or `patch` per semver rules.
|
|
34
|
+
|
|
35
|
+
## Example output
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
### Breaking Changes
|
|
39
|
+
|
|
40
|
+
- Removed function `fetchLegacyData`
|
|
41
|
+
- `createUser` signature changed: `(name: string, email: string)` → `(opts: CreateUserOpts)`
|
|
42
|
+
|
|
43
|
+
### New
|
|
44
|
+
|
|
45
|
+
- Added function `updateUser`
|
|
46
|
+
- Added interface `CreateUserOpts`
|
|
47
|
+
|
|
48
|
+
Suggested version bump: major
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Programmatic API
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { extract, diff, classify, buildResult, format } from 'sigdiff';
|
|
55
|
+
|
|
56
|
+
const before = extract(['src/v1/index.ts']);
|
|
57
|
+
const after = extract(['src/v2/index.ts']);
|
|
58
|
+
|
|
59
|
+
console.log(format(buildResult(classify(diff(before, after)))));
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Notes
|
|
63
|
+
|
|
64
|
+
- TypeScript only — no JS support (no type info to diff)
|
|
65
|
+
- Single-package projects only — no monorepo support yet
|
|
66
|
+
- Read-only — never touches your working tree
|
|
67
|
+
- 1 runtime dependency ([`cac`](https://github.com/cacjs/cac))
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
package/dist/classify.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classify = classify;
|
|
4
|
+
function classify(changes) {
|
|
5
|
+
return changes.map(classifyOne);
|
|
6
|
+
}
|
|
7
|
+
function classifyOne(change) {
|
|
8
|
+
switch (change.type) {
|
|
9
|
+
case 'removed':
|
|
10
|
+
return {
|
|
11
|
+
change,
|
|
12
|
+
level: 'major',
|
|
13
|
+
description: `Removed ${change.symbol.kind} \`${change.symbol.name}\``
|
|
14
|
+
};
|
|
15
|
+
case 'added':
|
|
16
|
+
return {
|
|
17
|
+
change,
|
|
18
|
+
level: 'minor',
|
|
19
|
+
description: `Added ${change.symbol.kind} \`${change.symbol.name}\``
|
|
20
|
+
};
|
|
21
|
+
case 'modified':
|
|
22
|
+
return {
|
|
23
|
+
change,
|
|
24
|
+
level: classifyModification(change),
|
|
25
|
+
description: buildModificationDescription(change)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function classifyModification(change) {
|
|
30
|
+
const { before, after } = change;
|
|
31
|
+
if (before.kind !== after.kind)
|
|
32
|
+
return 'major';
|
|
33
|
+
if (before.kind === 'function') {
|
|
34
|
+
return classifyFunctionChange(before.signature, after.signature);
|
|
35
|
+
}
|
|
36
|
+
if (before.kind === 'interface' || before.kind === 'type-alias' || before.kind === 'class') {
|
|
37
|
+
return classifyStructuralChange(before.signature, after.signature);
|
|
38
|
+
}
|
|
39
|
+
return 'major';
|
|
40
|
+
}
|
|
41
|
+
function classifyFunctionChange(beforeSig, afterSig) {
|
|
42
|
+
const beforeParams = extractParamsFromSignature(beforeSig);
|
|
43
|
+
const afterParams = extractParamsFromSignature(afterSig);
|
|
44
|
+
const beforeReturn = extractReturnFromSignature(beforeSig);
|
|
45
|
+
const afterReturn = extractReturnFromSignature(afterSig);
|
|
46
|
+
if (beforeReturn === afterReturn &&
|
|
47
|
+
afterParams.startsWith(beforeParams) &&
|
|
48
|
+
isOnlyOptionalParamsAdded(beforeParams, afterParams)) {
|
|
49
|
+
return 'minor';
|
|
50
|
+
}
|
|
51
|
+
return 'major';
|
|
52
|
+
}
|
|
53
|
+
function classifyStructuralChange(beforeSig, afterSig) {
|
|
54
|
+
const beforeProps = extractPropsFromSignature(beforeSig);
|
|
55
|
+
const afterProps = extractPropsFromSignature(afterSig);
|
|
56
|
+
const beforeSet = new Set(beforeProps);
|
|
57
|
+
const afterSet = new Set(afterProps);
|
|
58
|
+
for (const prop of beforeProps) {
|
|
59
|
+
if (!afterSet.has(prop))
|
|
60
|
+
return 'major';
|
|
61
|
+
}
|
|
62
|
+
let hasAdditions = false;
|
|
63
|
+
for (const prop of afterProps) {
|
|
64
|
+
if (!beforeSet.has(prop)) {
|
|
65
|
+
hasAdditions = true;
|
|
66
|
+
if (!prop.includes('?:'))
|
|
67
|
+
return 'major';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return hasAdditions ? 'minor' : 'patch';
|
|
71
|
+
}
|
|
72
|
+
function buildModificationDescription(change) {
|
|
73
|
+
const { before, after } = change;
|
|
74
|
+
if (before.kind !== after.kind) {
|
|
75
|
+
return `\`${before.name}\` changed from ${before.kind} to ${after.kind}`;
|
|
76
|
+
}
|
|
77
|
+
return `\`${before.name}\` signature changed: \`${before.signature}\` -> \`${after.signature}\``;
|
|
78
|
+
}
|
|
79
|
+
function extractParamsFromSignature(sig) {
|
|
80
|
+
const start = sig.indexOf('(');
|
|
81
|
+
if (start === -1)
|
|
82
|
+
return '';
|
|
83
|
+
const end = findMatchingParen(sig, start);
|
|
84
|
+
return sig.slice(start + 1, end);
|
|
85
|
+
}
|
|
86
|
+
function extractReturnFromSignature(sig) {
|
|
87
|
+
const start = sig.indexOf('(');
|
|
88
|
+
if (start === -1)
|
|
89
|
+
return '';
|
|
90
|
+
const end = findMatchingParen(sig, start);
|
|
91
|
+
const afterParen = sig.slice(end + 1);
|
|
92
|
+
const match = afterParen.match(/^:\s*(.+)$/);
|
|
93
|
+
return match ? match[1].trim() : '';
|
|
94
|
+
}
|
|
95
|
+
function findMatchingParen(str, openIndex) {
|
|
96
|
+
let depth = 0;
|
|
97
|
+
for (let i = openIndex; i < str.length; i++) {
|
|
98
|
+
if (str[i] === '(')
|
|
99
|
+
depth++;
|
|
100
|
+
else if (str[i] === ')') {
|
|
101
|
+
depth--;
|
|
102
|
+
if (depth === 0)
|
|
103
|
+
return i;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return str.length;
|
|
107
|
+
}
|
|
108
|
+
function isOnlyOptionalParamsAdded(beforeParams, afterParams) {
|
|
109
|
+
const added = afterParams.slice(beforeParams.length).trim();
|
|
110
|
+
if (!added)
|
|
111
|
+
return true;
|
|
112
|
+
const parts = added
|
|
113
|
+
.split(',')
|
|
114
|
+
.map((p) => p.trim())
|
|
115
|
+
.filter(Boolean);
|
|
116
|
+
return parts.every((p) => p.includes('?'));
|
|
117
|
+
}
|
|
118
|
+
function extractPropsFromSignature(sig) {
|
|
119
|
+
const match = sig.match(/\{([^}]*)\}/);
|
|
120
|
+
if (!match)
|
|
121
|
+
return [];
|
|
122
|
+
return match[1]
|
|
123
|
+
.split(/[;,]/)
|
|
124
|
+
.map((p) => p.trim())
|
|
125
|
+
.filter(Boolean);
|
|
126
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const cac_1 = __importDefault(require("cac"));
|
|
8
|
+
const errors_1 = require("./errors");
|
|
9
|
+
const git_1 = require("./git");
|
|
10
|
+
const diff_1 = require("./diff");
|
|
11
|
+
const classify_1 = require("./classify");
|
|
12
|
+
const format_1 = require("./format");
|
|
13
|
+
const cli = (0, cac_1.default)('sigdiff');
|
|
14
|
+
cli
|
|
15
|
+
.command('[range]', 'Diff the public API surface between two git refs')
|
|
16
|
+
.option('--entrypoint <path>', 'Scope to a specific file')
|
|
17
|
+
.option('--json', 'Output as JSON instead of markdown')
|
|
18
|
+
.action((range, options) => {
|
|
19
|
+
try {
|
|
20
|
+
(0, git_1.assertGitRepo)();
|
|
21
|
+
const refs = (0, git_1.resolveRefs)(range);
|
|
22
|
+
const before = (0, git_1.extractAtRef)(refs.before, options.entrypoint);
|
|
23
|
+
const after = (0, git_1.extractAtRef)(refs.after, options.entrypoint);
|
|
24
|
+
const changes = (0, diff_1.diff)(before, after);
|
|
25
|
+
const classified = (0, classify_1.classify)(changes);
|
|
26
|
+
const result = (0, format_1.buildResult)(classified);
|
|
27
|
+
const output = (0, format_1.format)(result, { json: options.json });
|
|
28
|
+
process.stdout.write(output);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
if (err instanceof errors_1.AstlogException) {
|
|
32
|
+
process.stderr.write(`Error: ${err.error.message}\n`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
cli.help();
|
|
39
|
+
cli.version('0.1.0');
|
|
40
|
+
cli.parse();
|
package/dist/diff.d.ts
ADDED
package/dist/diff.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.diff = diff;
|
|
4
|
+
function diff(before, after) {
|
|
5
|
+
const changes = [];
|
|
6
|
+
for (const [key, symbol] of before.symbols) {
|
|
7
|
+
if (!after.symbols.has(key)) {
|
|
8
|
+
changes.push({ type: 'removed', symbol });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
for (const [key, symbol] of after.symbols) {
|
|
12
|
+
if (!before.symbols.has(key)) {
|
|
13
|
+
changes.push({ type: 'added', symbol });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
for (const [key, afterSymbol] of after.symbols) {
|
|
17
|
+
const beforeSymbol = before.symbols.get(key);
|
|
18
|
+
if (beforeSymbol && beforeSymbol.signature !== afterSymbol.signature) {
|
|
19
|
+
changes.push({ type: 'modified', before: beforeSymbol, after: afterSymbol });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return changes;
|
|
23
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type AstlogError = {
|
|
2
|
+
code: 'INVALID_REF';
|
|
3
|
+
message: string;
|
|
4
|
+
} | {
|
|
5
|
+
code: 'NO_TAGS';
|
|
6
|
+
message: string;
|
|
7
|
+
} | {
|
|
8
|
+
code: 'NO_TYPESCRIPT';
|
|
9
|
+
message: string;
|
|
10
|
+
} | {
|
|
11
|
+
code: 'PARSE_ERROR';
|
|
12
|
+
message: string;
|
|
13
|
+
} | {
|
|
14
|
+
code: 'NOT_GIT_REPO';
|
|
15
|
+
message: string;
|
|
16
|
+
};
|
|
17
|
+
export declare class AstlogException extends Error {
|
|
18
|
+
readonly error: AstlogError;
|
|
19
|
+
constructor(error: AstlogError);
|
|
20
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AstlogException = void 0;
|
|
4
|
+
class AstlogException extends Error {
|
|
5
|
+
constructor(error) {
|
|
6
|
+
super(error.message);
|
|
7
|
+
this.error = error;
|
|
8
|
+
this.name = 'AstlogException';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.AstlogException = AstlogException;
|
package/dist/extract.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.extract = extract;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const ts = __importStar(require("typescript"));
|
|
39
|
+
function extract(fileNames, compilerOptions) {
|
|
40
|
+
const options = compilerOptions ?? {
|
|
41
|
+
target: ts.ScriptTarget.ES2020,
|
|
42
|
+
module: ts.ModuleKind.CommonJS,
|
|
43
|
+
strict: true,
|
|
44
|
+
skipLibCheck: true
|
|
45
|
+
};
|
|
46
|
+
const program = ts.createProgram(fileNames, options);
|
|
47
|
+
const checker = program.getTypeChecker();
|
|
48
|
+
const symbols = new Map();
|
|
49
|
+
for (const fileName of fileNames) {
|
|
50
|
+
const sourceFile = program.getSourceFile(fileName);
|
|
51
|
+
if (!sourceFile)
|
|
52
|
+
continue;
|
|
53
|
+
const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
|
|
54
|
+
if (!moduleSymbol)
|
|
55
|
+
continue;
|
|
56
|
+
const relativePath = path.relative(process.cwd(), fileName);
|
|
57
|
+
const exports = checker.getExportsOfModule(moduleSymbol);
|
|
58
|
+
for (const exp of exports) {
|
|
59
|
+
const extracted = extractSymbol(exp, relativePath, checker);
|
|
60
|
+
if (extracted) {
|
|
61
|
+
const key = `${extracted.filePath}:${extracted.name}`;
|
|
62
|
+
symbols.set(key, extracted);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { symbols };
|
|
67
|
+
}
|
|
68
|
+
function extractSymbol(symbol, filePath, checker) {
|
|
69
|
+
const declarations = symbol.getDeclarations();
|
|
70
|
+
if (!declarations || declarations.length === 0)
|
|
71
|
+
return null;
|
|
72
|
+
const decl = declarations[0];
|
|
73
|
+
const name = symbol.getName();
|
|
74
|
+
const kind = getSymbolKind(decl, checker);
|
|
75
|
+
if (!kind)
|
|
76
|
+
return null;
|
|
77
|
+
const rawSignature = buildSignature(name, kind, decl, checker);
|
|
78
|
+
const signature = normalizeSignature(rawSignature);
|
|
79
|
+
return { name, kind, signature, filePath };
|
|
80
|
+
}
|
|
81
|
+
function getSymbolKind(decl, checker) {
|
|
82
|
+
if (ts.isFunctionDeclaration(decl))
|
|
83
|
+
return 'function';
|
|
84
|
+
if (ts.isInterfaceDeclaration(decl))
|
|
85
|
+
return 'interface';
|
|
86
|
+
if (ts.isTypeAliasDeclaration(decl))
|
|
87
|
+
return 'type-alias';
|
|
88
|
+
if (ts.isEnumDeclaration(decl))
|
|
89
|
+
return 'enum';
|
|
90
|
+
if (ts.isClassDeclaration(decl))
|
|
91
|
+
return 'class';
|
|
92
|
+
if (ts.isVariableDeclaration(decl)) {
|
|
93
|
+
const type = checker.getTypeAtLocation(decl);
|
|
94
|
+
if (type.getCallSignatures().length > 0)
|
|
95
|
+
return 'function';
|
|
96
|
+
return 'constant';
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
function buildSignature(name, kind, decl, checker) {
|
|
101
|
+
switch (kind) {
|
|
102
|
+
case 'function':
|
|
103
|
+
if (ts.isFunctionDeclaration(decl)) {
|
|
104
|
+
return buildFunctionSignature(name, decl, checker);
|
|
105
|
+
}
|
|
106
|
+
return buildArrowFunctionSignature(name, decl, checker);
|
|
107
|
+
case 'interface':
|
|
108
|
+
return buildInterfaceSignature(name, decl, checker);
|
|
109
|
+
case 'type-alias':
|
|
110
|
+
return buildTypeAliasSignature(name, decl, checker);
|
|
111
|
+
case 'enum':
|
|
112
|
+
return buildEnumSignature(name, decl);
|
|
113
|
+
case 'class':
|
|
114
|
+
return buildClassSignature(name, decl, checker);
|
|
115
|
+
case 'constant':
|
|
116
|
+
return buildConstantSignature(name, decl, checker);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function buildFunctionSignature(name, decl, checker) {
|
|
120
|
+
const params = formatParams(decl.parameters, checker);
|
|
121
|
+
const returnType = inferReturnType(decl, checker);
|
|
122
|
+
return `function ${name}(${params}): ${returnType}`;
|
|
123
|
+
}
|
|
124
|
+
function buildInterfaceSignature(name, decl, checker) {
|
|
125
|
+
const props = formatMembers(decl.members, checker);
|
|
126
|
+
return `interface ${name} { ${props} }`;
|
|
127
|
+
}
|
|
128
|
+
function buildTypeAliasSignature(name, decl, checker) {
|
|
129
|
+
const type = checker.getTypeAtLocation(decl);
|
|
130
|
+
const typeStr = checker.typeToString(type, decl, ts.TypeFormatFlags.NoTruncation);
|
|
131
|
+
return `type ${name} = ${typeStr}`;
|
|
132
|
+
}
|
|
133
|
+
function buildEnumSignature(name, decl) {
|
|
134
|
+
const members = decl.members.map((m) => ts.isIdentifier(m.name) ? m.name.text : m.name.getText());
|
|
135
|
+
return `enum ${name} { ${members.join(', ')} }`;
|
|
136
|
+
}
|
|
137
|
+
function buildClassSignature(name, decl, checker) {
|
|
138
|
+
const props = formatMembers(decl.members, checker);
|
|
139
|
+
return `class ${name} { ${props} }`;
|
|
140
|
+
}
|
|
141
|
+
function buildArrowFunctionSignature(name, decl, checker) {
|
|
142
|
+
const type = checker.getTypeAtLocation(decl);
|
|
143
|
+
const sig = type.getCallSignatures()[0];
|
|
144
|
+
if (!sig)
|
|
145
|
+
return `function ${name}(): void`;
|
|
146
|
+
const params = sig
|
|
147
|
+
.getParameters()
|
|
148
|
+
.map((p) => {
|
|
149
|
+
const paramType = checker.getTypeOfSymbolAtLocation(p, decl);
|
|
150
|
+
const optional = p.flags & ts.SymbolFlags.Optional ? '?' : '';
|
|
151
|
+
return `${p.getName()}${optional}: ${checker.typeToString(paramType)}`;
|
|
152
|
+
})
|
|
153
|
+
.join(', ');
|
|
154
|
+
const returnType = checker.typeToString(checker.getReturnTypeOfSignature(sig));
|
|
155
|
+
return `function ${name}(${params}): ${returnType}`;
|
|
156
|
+
}
|
|
157
|
+
function buildConstantSignature(name, decl, checker) {
|
|
158
|
+
const type = checker.getTypeAtLocation(decl);
|
|
159
|
+
return `const ${name}: ${checker.typeToString(type)}`;
|
|
160
|
+
}
|
|
161
|
+
function formatParams(parameters, checker) {
|
|
162
|
+
return parameters
|
|
163
|
+
.map((param) => {
|
|
164
|
+
const name = param.name.getText();
|
|
165
|
+
const type = checker.typeToString(checker.getTypeAtLocation(param));
|
|
166
|
+
const optional = param.questionToken || param.initializer ? '?' : '';
|
|
167
|
+
return `${name}${optional}: ${type}`;
|
|
168
|
+
})
|
|
169
|
+
.join(', ');
|
|
170
|
+
}
|
|
171
|
+
function formatMembers(members, checker) {
|
|
172
|
+
const parts = [];
|
|
173
|
+
for (const member of members) {
|
|
174
|
+
if (ts.isPropertySignature(member) || ts.isPropertyDeclaration(member)) {
|
|
175
|
+
const name = member.name?.getText() ?? '';
|
|
176
|
+
let type = checker.typeToString(checker.getTypeAtLocation(member));
|
|
177
|
+
const optional = member.questionToken ? '?' : '';
|
|
178
|
+
if (optional && type.endsWith(' | undefined')) {
|
|
179
|
+
type = type.slice(0, -' | undefined'.length);
|
|
180
|
+
}
|
|
181
|
+
parts.push(`${name}${optional}: ${type}`);
|
|
182
|
+
}
|
|
183
|
+
else if (ts.isMethodSignature(member) || ts.isMethodDeclaration(member)) {
|
|
184
|
+
const name = member.name?.getText() ?? '';
|
|
185
|
+
const sig = checker.getSignatureFromDeclaration(member);
|
|
186
|
+
const type = sig ? checker.signatureToString(sig) : '(...args: any[]) => any';
|
|
187
|
+
parts.push(`${name}: ${type}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return parts.join(', ');
|
|
191
|
+
}
|
|
192
|
+
function inferReturnType(decl, checker) {
|
|
193
|
+
const sig = checker.getSignatureFromDeclaration(decl);
|
|
194
|
+
if (!sig)
|
|
195
|
+
return 'void';
|
|
196
|
+
return checker.typeToString(checker.getReturnTypeOfSignature(sig));
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Normalize signatures to prevent false positives from cosmetic differences
|
|
200
|
+
* between TS versions or source formatting (whitespace, semicolons, index signatures).
|
|
201
|
+
*/
|
|
202
|
+
function normalizeSignature(sig) {
|
|
203
|
+
let result = sig;
|
|
204
|
+
result = result.replace(/\s+/g, ' ');
|
|
205
|
+
result = result.replace(/;\s*}/g, ' }');
|
|
206
|
+
result = result.replace(/;\s+(?=\w)/g, ', ');
|
|
207
|
+
result = result.replace(/\{\s*\[\w+:\s*string\]:\s*([^}]+)\}/g, (_, valueType) => `Record<string, ${valueType.trim()}>`);
|
|
208
|
+
result = result.replace(/\s+/g, ' ').trim();
|
|
209
|
+
return result;
|
|
210
|
+
}
|
package/dist/format.d.ts
ADDED
package/dist/format.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildResult = buildResult;
|
|
4
|
+
exports.format = format;
|
|
5
|
+
function buildResult(classified) {
|
|
6
|
+
const breaking = [];
|
|
7
|
+
const features = [];
|
|
8
|
+
const fixes = [];
|
|
9
|
+
for (const c of classified) {
|
|
10
|
+
switch (c.level) {
|
|
11
|
+
case 'major':
|
|
12
|
+
breaking.push(c);
|
|
13
|
+
break;
|
|
14
|
+
case 'minor':
|
|
15
|
+
features.push(c);
|
|
16
|
+
break;
|
|
17
|
+
case 'patch':
|
|
18
|
+
fixes.push(c);
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
let suggestedBump = 'patch';
|
|
23
|
+
if (breaking.length > 0)
|
|
24
|
+
suggestedBump = 'major';
|
|
25
|
+
else if (features.length > 0)
|
|
26
|
+
suggestedBump = 'minor';
|
|
27
|
+
return { breaking, features, fixes, suggestedBump };
|
|
28
|
+
}
|
|
29
|
+
function format(result, options) {
|
|
30
|
+
if (options?.json) {
|
|
31
|
+
return JSON.stringify(result, null, 2);
|
|
32
|
+
}
|
|
33
|
+
return formatMarkdown(result);
|
|
34
|
+
}
|
|
35
|
+
const MAX_SIGNATURE_LENGTH = 120;
|
|
36
|
+
function truncateSignature(sig) {
|
|
37
|
+
if (sig.length <= MAX_SIGNATURE_LENGTH)
|
|
38
|
+
return sig;
|
|
39
|
+
return sig.slice(0, MAX_SIGNATURE_LENGTH) + '...';
|
|
40
|
+
}
|
|
41
|
+
function deduplicateDescriptions(changes) {
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
const descriptions = [];
|
|
44
|
+
for (const c of changes) {
|
|
45
|
+
const name = getChangeName(c);
|
|
46
|
+
if (seen.has(name))
|
|
47
|
+
continue;
|
|
48
|
+
seen.add(name);
|
|
49
|
+
descriptions.push(truncateDescription(c));
|
|
50
|
+
}
|
|
51
|
+
return descriptions;
|
|
52
|
+
}
|
|
53
|
+
function getChangeName(c) {
|
|
54
|
+
switch (c.change.type) {
|
|
55
|
+
case 'added':
|
|
56
|
+
return c.change.symbol.name;
|
|
57
|
+
case 'removed':
|
|
58
|
+
return c.change.symbol.name;
|
|
59
|
+
case 'modified':
|
|
60
|
+
return c.change.before.name;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function truncateDescription(c) {
|
|
64
|
+
if (c.change.type !== 'modified')
|
|
65
|
+
return c.description;
|
|
66
|
+
const { before, after } = c.change;
|
|
67
|
+
if (before.kind !== after.kind) {
|
|
68
|
+
return c.description;
|
|
69
|
+
}
|
|
70
|
+
const beforeSig = truncateSignature(before.signature);
|
|
71
|
+
const afterSig = truncateSignature(after.signature);
|
|
72
|
+
return `\`${before.name}\` signature changed: \`${beforeSig}\` -> \`${afterSig}\``;
|
|
73
|
+
}
|
|
74
|
+
function formatMarkdown(result) {
|
|
75
|
+
const sections = [
|
|
76
|
+
formatSection('### Breaking Changes', result.breaking),
|
|
77
|
+
formatSection('### New', result.features),
|
|
78
|
+
formatSection('### Changed', result.fixes)
|
|
79
|
+
]
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.join('\n');
|
|
82
|
+
if (!sections)
|
|
83
|
+
return 'No API changes detected.\n';
|
|
84
|
+
return `${sections}\n**Suggested version bump:** ${result.suggestedBump}\n`;
|
|
85
|
+
}
|
|
86
|
+
function formatSection(heading, changes) {
|
|
87
|
+
if (changes.length === 0)
|
|
88
|
+
return '';
|
|
89
|
+
const items = deduplicateDescriptions(changes)
|
|
90
|
+
.map((d) => `- ${d}`)
|
|
91
|
+
.join('\n');
|
|
92
|
+
return `${heading}\n\n${items}\n`;
|
|
93
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ApiSurface } from './types';
|
|
2
|
+
export declare function assertGitRepo(): void;
|
|
3
|
+
export declare function resolveRefs(rangeArg?: string): {
|
|
4
|
+
before: string;
|
|
5
|
+
after: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function extractAtRef(ref: string, entrypoint?: string): ApiSurface;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.assertGitRepo = assertGitRepo;
|
|
37
|
+
exports.resolveRefs = resolveRefs;
|
|
38
|
+
exports.extractAtRef = extractAtRef;
|
|
39
|
+
const child_process_1 = require("child_process");
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const errors_1 = require("./errors");
|
|
44
|
+
const extract_1 = require("./extract");
|
|
45
|
+
function assertGitRepo() {
|
|
46
|
+
try {
|
|
47
|
+
(0, child_process_1.execSync)('git rev-parse --git-dir', { stdio: 'ignore' });
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new errors_1.AstlogException({
|
|
51
|
+
code: 'NOT_GIT_REPO',
|
|
52
|
+
message: 'Not a git repository. Run sigdiff from inside a git repo.'
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function resolveRefs(rangeArg) {
|
|
57
|
+
if (rangeArg) {
|
|
58
|
+
const parts = rangeArg.split('..');
|
|
59
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
60
|
+
throw new errors_1.AstlogException({
|
|
61
|
+
code: 'INVALID_REF',
|
|
62
|
+
message: `Invalid range format: "${rangeArg}". Expected format: <ref>..<ref>`
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
validateRef(parts[0]);
|
|
66
|
+
validateRef(parts[1]);
|
|
67
|
+
return { before: parts[0], after: parts[1] };
|
|
68
|
+
}
|
|
69
|
+
let lastTag;
|
|
70
|
+
try {
|
|
71
|
+
lastTag = (0, child_process_1.execSync)('git describe --tags --abbrev=0', {
|
|
72
|
+
encoding: 'utf-8'
|
|
73
|
+
}).trim();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
throw new errors_1.AstlogException({
|
|
77
|
+
code: 'NO_TAGS',
|
|
78
|
+
message: 'No git tags found. Provide an explicit range: sigdiff <ref>..<ref>'
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return { before: lastTag, after: 'HEAD' };
|
|
82
|
+
}
|
|
83
|
+
function extractAtRef(ref, entrypoint) {
|
|
84
|
+
const files = discoverFiles(ref, entrypoint);
|
|
85
|
+
if (files.length === 0) {
|
|
86
|
+
throw new errors_1.AstlogException({
|
|
87
|
+
code: 'NO_TYPESCRIPT',
|
|
88
|
+
message: `No TypeScript files found at ref "${ref}".`
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sigdiff-'));
|
|
92
|
+
try {
|
|
93
|
+
const pathMap = new Map();
|
|
94
|
+
for (const logicalPath of files) {
|
|
95
|
+
const content = (0, child_process_1.execSync)(`git show '${ref}:${logicalPath}'`, {
|
|
96
|
+
encoding: 'utf-8'
|
|
97
|
+
});
|
|
98
|
+
const tempAbsPath = path.join(tmpDir, logicalPath);
|
|
99
|
+
fs.mkdirSync(path.dirname(tempAbsPath), { recursive: true });
|
|
100
|
+
fs.writeFileSync(tempAbsPath, content);
|
|
101
|
+
const tempRelative = path.relative(process.cwd(), tempAbsPath);
|
|
102
|
+
pathMap.set(tempRelative, logicalPath);
|
|
103
|
+
}
|
|
104
|
+
const tempPaths = files.map((f) => path.join(tmpDir, f));
|
|
105
|
+
const rawSurface = (0, extract_1.extract)(tempPaths);
|
|
106
|
+
return normalizeSurface(rawSurface, pathMap);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function validateRef(ref) {
|
|
113
|
+
try {
|
|
114
|
+
(0, child_process_1.execSync)(`git rev-parse --verify ${ref}`, { stdio: 'ignore' });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
throw new errors_1.AstlogException({
|
|
118
|
+
code: 'INVALID_REF',
|
|
119
|
+
message: `Git ref "${ref}" does not exist.`
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function discoverFiles(ref, entrypoint) {
|
|
124
|
+
const output = (0, child_process_1.execSync)(`git ls-tree -r --name-only ${ref}`, {
|
|
125
|
+
encoding: 'utf-8'
|
|
126
|
+
});
|
|
127
|
+
const allFiles = output.trim().split('\n').filter(Boolean);
|
|
128
|
+
let tsFiles = allFiles.filter((f) => (f.endsWith('.ts') || f.endsWith('.tsx')) &&
|
|
129
|
+
!f.endsWith('.d.ts') &&
|
|
130
|
+
!f.includes('node_modules/'));
|
|
131
|
+
if (entrypoint) {
|
|
132
|
+
tsFiles = tsFiles.filter((f) => f === entrypoint);
|
|
133
|
+
}
|
|
134
|
+
return tsFiles;
|
|
135
|
+
}
|
|
136
|
+
function normalizeSurface(surface, pathMap) {
|
|
137
|
+
const normalized = new Map();
|
|
138
|
+
for (const [, symbol] of surface.symbols) {
|
|
139
|
+
const logicalPath = pathMap.get(symbol.filePath);
|
|
140
|
+
if (!logicalPath)
|
|
141
|
+
continue;
|
|
142
|
+
const newSymbol = { ...symbol, filePath: logicalPath };
|
|
143
|
+
const key = `${logicalPath}:${newSymbol.name}`;
|
|
144
|
+
normalized.set(key, newSymbol);
|
|
145
|
+
}
|
|
146
|
+
return { symbols: normalized };
|
|
147
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { extract } from './extract';
|
|
2
|
+
export { diff } from './diff';
|
|
3
|
+
export { classify } from './classify';
|
|
4
|
+
export { buildResult, format } from './format';
|
|
5
|
+
export { AstlogException } from './errors';
|
|
6
|
+
export type { ApiSurface, ExportedSymbol, Change, ClassifiedChange, ChangelogResult, SemverLevel, SymbolKind } from './types';
|
|
7
|
+
export type { AstlogError } from './errors';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AstlogException = exports.format = exports.buildResult = exports.classify = exports.diff = exports.extract = void 0;
|
|
4
|
+
var extract_1 = require("./extract");
|
|
5
|
+
Object.defineProperty(exports, "extract", { enumerable: true, get: function () { return extract_1.extract; } });
|
|
6
|
+
var diff_1 = require("./diff");
|
|
7
|
+
Object.defineProperty(exports, "diff", { enumerable: true, get: function () { return diff_1.diff; } });
|
|
8
|
+
var classify_1 = require("./classify");
|
|
9
|
+
Object.defineProperty(exports, "classify", { enumerable: true, get: function () { return classify_1.classify; } });
|
|
10
|
+
var format_1 = require("./format");
|
|
11
|
+
Object.defineProperty(exports, "buildResult", { enumerable: true, get: function () { return format_1.buildResult; } });
|
|
12
|
+
Object.defineProperty(exports, "format", { enumerable: true, get: function () { return format_1.format; } });
|
|
13
|
+
var errors_1 = require("./errors");
|
|
14
|
+
Object.defineProperty(exports, "AstlogException", { enumerable: true, get: function () { return errors_1.AstlogException; } });
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type SymbolKind = 'function' | 'interface' | 'type-alias' | 'class' | 'constant' | 'enum';
|
|
2
|
+
export interface ExportedSymbol {
|
|
3
|
+
name: string;
|
|
4
|
+
kind: SymbolKind;
|
|
5
|
+
signature: string;
|
|
6
|
+
filePath: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ApiSurface {
|
|
9
|
+
symbols: Map<string, ExportedSymbol>;
|
|
10
|
+
}
|
|
11
|
+
export type Change = {
|
|
12
|
+
type: 'added';
|
|
13
|
+
symbol: ExportedSymbol;
|
|
14
|
+
} | {
|
|
15
|
+
type: 'removed';
|
|
16
|
+
symbol: ExportedSymbol;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'modified';
|
|
19
|
+
before: ExportedSymbol;
|
|
20
|
+
after: ExportedSymbol;
|
|
21
|
+
};
|
|
22
|
+
export type SemverLevel = 'major' | 'minor' | 'patch';
|
|
23
|
+
export interface ClassifiedChange {
|
|
24
|
+
change: Change;
|
|
25
|
+
level: SemverLevel;
|
|
26
|
+
description: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ChangelogResult {
|
|
29
|
+
breaking: ClassifiedChange[];
|
|
30
|
+
features: ClassifiedChange[];
|
|
31
|
+
fixes: ClassifiedChange[];
|
|
32
|
+
suggestedBump: SemverLevel;
|
|
33
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sigdiff",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Changelog from code, not commits. AST-based API surface diffing for TypeScript.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sigdiff": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"dev": "tsc --watch",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"lint": "eslint src/",
|
|
18
|
+
"lint:fix": "eslint src/ --fix && prettier --write src/ tests/ fixtures/",
|
|
19
|
+
"format": "prettier --write src/ tests/ fixtures/",
|
|
20
|
+
"format:check": "prettier --check src/ tests/ fixtures/",
|
|
21
|
+
"prepublishOnly": "npm run build && npm test"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"changelog",
|
|
25
|
+
"ast",
|
|
26
|
+
"typescript",
|
|
27
|
+
"semver",
|
|
28
|
+
"diff",
|
|
29
|
+
"api-surface",
|
|
30
|
+
"breaking-changes"
|
|
31
|
+
],
|
|
32
|
+
"author": "Andy Deng",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/1dengaroo/sigdiff"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"typescript": ">=4.7.0"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"cac": "^6.7.14"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^25.5.2",
|
|
49
|
+
"@typescript-eslint/eslint-plugin": "^8.58.0",
|
|
50
|
+
"@typescript-eslint/parser": "^8.58.0",
|
|
51
|
+
"eslint": "^10.2.0",
|
|
52
|
+
"prettier": "^3.8.1",
|
|
53
|
+
"typescript": "^6.0.2",
|
|
54
|
+
"vitest": "^4.1.2"
|
|
55
|
+
}
|
|
56
|
+
}
|