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 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
@@ -0,0 +1,2 @@
1
+ import { Change, ClassifiedChange } from './types';
2
+ export declare function classify(changes: Change[]): ClassifiedChange[];
@@ -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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
@@ -0,0 +1,2 @@
1
+ import { ApiSurface, Change } from './types';
2
+ export declare function diff(before: ApiSurface, after: ApiSurface): Change[];
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
+ }
@@ -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;
@@ -0,0 +1,3 @@
1
+ import * as ts from 'typescript';
2
+ import { ApiSurface } from './types';
3
+ export declare function extract(fileNames: string[], compilerOptions?: ts.CompilerOptions): ApiSurface;
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ import { ChangelogResult, ClassifiedChange } from './types';
2
+ export declare function buildResult(classified: ClassifiedChange[]): ChangelogResult;
3
+ export declare function format(result: ChangelogResult, options?: {
4
+ json?: boolean;
5
+ }): string;
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
+ }
@@ -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; } });
@@ -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
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
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
+ }