@versatiles/release-tool 2.5.0 → 2.7.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 +44 -30
- package/dist/commands/check.d.ts +24 -0
- package/dist/commands/check.js +27 -3
- package/dist/commands/deps-graph.d.ts +22 -0
- package/dist/commands/deps-graph.js +23 -1
- package/dist/commands/doc-command.js +6 -6
- package/dist/commands/doc-typescript.d.ts +39 -3
- package/dist/commands/doc-typescript.js +25 -5
- package/dist/commands/markdown.d.ts +9 -1
- package/dist/commands/markdown.js +167 -38
- package/dist/commands/release-npm.d.ts +44 -1
- package/dist/commands/release-npm.js +195 -49
- package/dist/index.js +28 -13
- package/dist/lib/benchmark.d.ts +119 -0
- package/dist/lib/benchmark.js +148 -0
- package/dist/lib/changelog.d.ts +23 -0
- package/dist/lib/changelog.js +117 -0
- package/dist/lib/errors.d.ts +32 -0
- package/dist/lib/errors.js +47 -0
- package/dist/lib/git.d.ts +92 -0
- package/dist/lib/git.js +120 -8
- package/dist/lib/log.d.ts +61 -1
- package/dist/lib/log.js +76 -3
- package/dist/lib/retry.d.ts +24 -0
- package/dist/lib/retry.js +44 -0
- package/dist/lib/shell.d.ts +131 -10
- package/dist/lib/shell.js +142 -28
- package/dist/lib/utils.d.ts +29 -0
- package/dist/lib/utils.js +43 -2
- package/package.json +27 -14
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance benchmarking utilities for CLI operations.
|
|
3
|
+
*
|
|
4
|
+
* Provides timing measurement and optional logging for tracking
|
|
5
|
+
* execution duration of key operations.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Formats a duration in milliseconds to a human-readable string.
|
|
9
|
+
*
|
|
10
|
+
* @param ms - Duration in milliseconds
|
|
11
|
+
* @returns Formatted duration string (e.g., "1.23s", "456ms")
|
|
12
|
+
*/
|
|
13
|
+
export function formatDuration(ms) {
|
|
14
|
+
if (ms >= 1000) {
|
|
15
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
16
|
+
}
|
|
17
|
+
return `${Math.round(ms)}ms`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Measures the execution time of a synchronous function.
|
|
21
|
+
*
|
|
22
|
+
* @param fn - The function to benchmark
|
|
23
|
+
* @param options - Optional benchmark configuration
|
|
24
|
+
* @returns The function result along with timing information
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const { result, durationMs } = benchmarkSync(() => {
|
|
29
|
+
* return heavyComputation();
|
|
30
|
+
* }, { label: 'Heavy computation', log: true });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function benchmarkSync(fn, options = {}) {
|
|
34
|
+
const { label, log = false, logger = console.log } = options;
|
|
35
|
+
const start = performance.now();
|
|
36
|
+
const result = fn();
|
|
37
|
+
const end = performance.now();
|
|
38
|
+
const durationMs = end - start;
|
|
39
|
+
const durationFormatted = formatDuration(durationMs);
|
|
40
|
+
if (log && label) {
|
|
41
|
+
logger(`[benchmark] ${label}: ${durationFormatted}`);
|
|
42
|
+
}
|
|
43
|
+
return { result, durationMs, durationFormatted };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Measures the execution time of an asynchronous function.
|
|
47
|
+
*
|
|
48
|
+
* @param fn - The async function to benchmark
|
|
49
|
+
* @param options - Optional benchmark configuration
|
|
50
|
+
* @returns Promise resolving to the function result along with timing information
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* const { result, durationMs } = await benchmark(async () => {
|
|
55
|
+
* return await fetchData();
|
|
56
|
+
* }, { label: 'API fetch', log: true });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export async function benchmark(fn, options = {}) {
|
|
60
|
+
const { label, log = false, logger = console.log } = options;
|
|
61
|
+
const start = performance.now();
|
|
62
|
+
const result = await fn();
|
|
63
|
+
const end = performance.now();
|
|
64
|
+
const durationMs = end - start;
|
|
65
|
+
const durationFormatted = formatDuration(durationMs);
|
|
66
|
+
if (log && label) {
|
|
67
|
+
logger(`[benchmark] ${label}: ${durationFormatted}`);
|
|
68
|
+
}
|
|
69
|
+
return { result, durationMs, durationFormatted };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Creates a timer for manual timing control.
|
|
73
|
+
*
|
|
74
|
+
* @returns Timer object with start, stop, and elapsed methods
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* const timer = createTimer();
|
|
79
|
+
* timer.start();
|
|
80
|
+
* // ... do work ...
|
|
81
|
+
* const elapsed = timer.stop();
|
|
82
|
+
* console.log(`Operation took ${elapsed.durationFormatted}`);
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function createTimer() {
|
|
86
|
+
let startTime = null;
|
|
87
|
+
let endTime = null;
|
|
88
|
+
return {
|
|
89
|
+
start() {
|
|
90
|
+
startTime = performance.now();
|
|
91
|
+
endTime = null;
|
|
92
|
+
},
|
|
93
|
+
stop() {
|
|
94
|
+
if (startTime === null) {
|
|
95
|
+
throw new Error('Timer was not started');
|
|
96
|
+
}
|
|
97
|
+
endTime = performance.now();
|
|
98
|
+
const durationMs = endTime - startTime;
|
|
99
|
+
return { durationMs, durationFormatted: formatDuration(durationMs) };
|
|
100
|
+
},
|
|
101
|
+
elapsed() {
|
|
102
|
+
if (startTime === null) {
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
const end = endTime ?? performance.now();
|
|
106
|
+
return end - startTime;
|
|
107
|
+
},
|
|
108
|
+
isRunning() {
|
|
109
|
+
return startTime !== null && endTime === null;
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Calculates statistics from an array of benchmark durations.
|
|
115
|
+
*
|
|
116
|
+
* @param durations - Array of duration values in milliseconds
|
|
117
|
+
* @returns Statistical summary of the benchmarks
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* const durations = [100, 150, 120, 130, 110];
|
|
122
|
+
* const stats = calculateStats(durations);
|
|
123
|
+
* console.log(`Average: ${stats.avgFormatted}`);
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export function calculateStats(durations) {
|
|
127
|
+
if (durations.length === 0) {
|
|
128
|
+
return {
|
|
129
|
+
count: 0,
|
|
130
|
+
totalMs: 0,
|
|
131
|
+
avgMs: 0,
|
|
132
|
+
minMs: 0,
|
|
133
|
+
maxMs: 0,
|
|
134
|
+
avgFormatted: '0ms',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const totalMs = durations.reduce((sum, d) => sum + d, 0);
|
|
138
|
+
const avgMs = totalMs / durations.length;
|
|
139
|
+
return {
|
|
140
|
+
count: durations.length,
|
|
141
|
+
totalMs,
|
|
142
|
+
avgMs,
|
|
143
|
+
minMs: Math.min(...durations),
|
|
144
|
+
maxMs: Math.max(...durations),
|
|
145
|
+
avgFormatted: formatDuration(avgMs),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=benchmark.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ParsedCommit } from './git.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generates a changelog entry for a release from parsed commits.
|
|
4
|
+
*
|
|
5
|
+
* @param version - The version being released
|
|
6
|
+
* @param commits - Array of parsed conventional commits
|
|
7
|
+
* @param date - The release date (defaults to today)
|
|
8
|
+
* @returns Formatted changelog entry string
|
|
9
|
+
*/
|
|
10
|
+
export declare function generateChangelogEntry(version: string, commits: ParsedCommit[], date?: Date): string;
|
|
11
|
+
/**
|
|
12
|
+
* Updates or creates a CHANGELOG.md file with a new release entry.
|
|
13
|
+
*
|
|
14
|
+
* @param directory - The project directory containing CHANGELOG.md
|
|
15
|
+
* @param version - The version being released
|
|
16
|
+
* @param commits - Array of parsed conventional commits
|
|
17
|
+
* @param date - The release date (defaults to today)
|
|
18
|
+
* @returns Object indicating whether the file was created or updated
|
|
19
|
+
*/
|
|
20
|
+
export declare function updateChangelog(directory: string, version: string, commits: ParsedCommit[], date?: Date): {
|
|
21
|
+
created: boolean;
|
|
22
|
+
path: string;
|
|
23
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { COMMIT_TYPES, groupCommitsByType } from './git.js';
|
|
4
|
+
/**
|
|
5
|
+
* Generates a changelog entry for a release from parsed commits.
|
|
6
|
+
*
|
|
7
|
+
* @param version - The version being released
|
|
8
|
+
* @param commits - Array of parsed conventional commits
|
|
9
|
+
* @param date - The release date (defaults to today)
|
|
10
|
+
* @returns Formatted changelog entry string
|
|
11
|
+
*/
|
|
12
|
+
export function generateChangelogEntry(version, commits, date = new Date()) {
|
|
13
|
+
const dateStr = date.toISOString().split('T')[0];
|
|
14
|
+
const reversed = [...commits].reverse();
|
|
15
|
+
const grouped = groupCommitsByType(reversed);
|
|
16
|
+
let entry = `## [${version}] - ${dateStr}\n\n`;
|
|
17
|
+
// Order: breaking changes first, then features, fixes, and others
|
|
18
|
+
const typeOrder = [
|
|
19
|
+
'feat',
|
|
20
|
+
'fix',
|
|
21
|
+
'perf',
|
|
22
|
+
'refactor',
|
|
23
|
+
'docs',
|
|
24
|
+
'test',
|
|
25
|
+
'build',
|
|
26
|
+
'ci',
|
|
27
|
+
'chore',
|
|
28
|
+
'style',
|
|
29
|
+
'revert',
|
|
30
|
+
'other',
|
|
31
|
+
];
|
|
32
|
+
// Add breaking changes section if any
|
|
33
|
+
const breakingCommits = reversed.filter((c) => c.breaking);
|
|
34
|
+
if (breakingCommits.length > 0) {
|
|
35
|
+
entry += '### Breaking Changes\n\n';
|
|
36
|
+
for (const commit of breakingCommits) {
|
|
37
|
+
entry += `- ${commit.description.replace(/\s+/g, ' ')}\n`;
|
|
38
|
+
}
|
|
39
|
+
entry += '\n';
|
|
40
|
+
}
|
|
41
|
+
// Add grouped commits
|
|
42
|
+
for (const type of typeOrder) {
|
|
43
|
+
const typeCommits = grouped.get(type);
|
|
44
|
+
if (!typeCommits || typeCommits.length === 0)
|
|
45
|
+
continue;
|
|
46
|
+
// Skip commits already shown in breaking changes
|
|
47
|
+
const nonBreaking = typeCommits.filter((c) => !c.breaking);
|
|
48
|
+
if (nonBreaking.length === 0)
|
|
49
|
+
continue;
|
|
50
|
+
const label = type === 'other' ? 'Other Changes' : COMMIT_TYPES[type];
|
|
51
|
+
entry += `### ${label}\n\n`;
|
|
52
|
+
for (const commit of nonBreaking) {
|
|
53
|
+
const scope = commit.scope ? `**${commit.scope}:** ` : '';
|
|
54
|
+
entry += `- ${scope}${commit.description.replace(/\s+/g, ' ')}\n`;
|
|
55
|
+
}
|
|
56
|
+
entry += '\n';
|
|
57
|
+
}
|
|
58
|
+
return entry;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Default changelog header for new CHANGELOG.md files
|
|
62
|
+
*/
|
|
63
|
+
const DEFAULT_CHANGELOG_HEADER = `# Changelog
|
|
64
|
+
|
|
65
|
+
All notable changes to this project will be documented in this file.
|
|
66
|
+
|
|
67
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
68
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
69
|
+
|
|
70
|
+
`;
|
|
71
|
+
/**
|
|
72
|
+
* Updates or creates a CHANGELOG.md file with a new release entry.
|
|
73
|
+
*
|
|
74
|
+
* @param directory - The project directory containing CHANGELOG.md
|
|
75
|
+
* @param version - The version being released
|
|
76
|
+
* @param commits - Array of parsed conventional commits
|
|
77
|
+
* @param date - The release date (defaults to today)
|
|
78
|
+
* @returns Object indicating whether the file was created or updated
|
|
79
|
+
*/
|
|
80
|
+
export function updateChangelog(directory, version, commits, date = new Date()) {
|
|
81
|
+
const changelogPath = resolve(directory, 'CHANGELOG.md');
|
|
82
|
+
const entry = generateChangelogEntry(version, commits, date);
|
|
83
|
+
let content;
|
|
84
|
+
let created = false;
|
|
85
|
+
if (existsSync(changelogPath)) {
|
|
86
|
+
const existing = readFileSync(changelogPath, 'utf8');
|
|
87
|
+
// Insert new entry after the header (after the first double newline or at the start if no header)
|
|
88
|
+
const headerEndIndex = findHeaderEnd(existing);
|
|
89
|
+
content = existing.slice(0, headerEndIndex) + entry + existing.slice(headerEndIndex);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
content = DEFAULT_CHANGELOG_HEADER + entry;
|
|
93
|
+
created = true;
|
|
94
|
+
}
|
|
95
|
+
writeFileSync(changelogPath, content);
|
|
96
|
+
return { created, path: changelogPath };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Finds the end of the changelog header section.
|
|
100
|
+
* Looks for patterns like "# Changelog" followed by description text,
|
|
101
|
+
* ending before the first "## " section header.
|
|
102
|
+
*/
|
|
103
|
+
function findHeaderEnd(content) {
|
|
104
|
+
// Look for the first version section (## [x.y.z] or ## x.y.z)
|
|
105
|
+
const versionSectionMatch = content.match(/^## \[?\d+\.\d+\.\d+\]?/m);
|
|
106
|
+
if (versionSectionMatch?.index !== undefined) {
|
|
107
|
+
return versionSectionMatch.index;
|
|
108
|
+
}
|
|
109
|
+
// If no version section found, look for any ## heading
|
|
110
|
+
const anySectionMatch = content.match(/^## /m);
|
|
111
|
+
if (anySectionMatch?.index !== undefined) {
|
|
112
|
+
return anySectionMatch.index;
|
|
113
|
+
}
|
|
114
|
+
// No sections found, append at end (after ensuring trailing newline)
|
|
115
|
+
return content.endsWith('\n') ? content.length : content.length;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=changelog.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error codes for categorizing different types of errors in the release tool.
|
|
3
|
+
*/
|
|
4
|
+
export type VrtErrorCode = 'VALIDATION_ERROR' | 'MARKDOWN_ERROR' | 'GIT_ERROR' | 'RELEASE_ERROR' | 'NOT_IMPLEMENTED';
|
|
5
|
+
/**
|
|
6
|
+
* Custom error class for the VersaTiles Release Tool.
|
|
7
|
+
* Provides consistent error handling with categorization via error codes.
|
|
8
|
+
*/
|
|
9
|
+
export declare class VrtError extends Error {
|
|
10
|
+
readonly code: VrtErrorCode;
|
|
11
|
+
constructor(message: string, code?: VrtErrorCode);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Helper function to create a validation error.
|
|
15
|
+
*/
|
|
16
|
+
export declare function validationError(message: string): VrtError;
|
|
17
|
+
/**
|
|
18
|
+
* Helper function to create a markdown processing error.
|
|
19
|
+
*/
|
|
20
|
+
export declare function markdownError(message: string): VrtError;
|
|
21
|
+
/**
|
|
22
|
+
* Helper function to create a git operation error.
|
|
23
|
+
*/
|
|
24
|
+
export declare function gitError(message: string): VrtError;
|
|
25
|
+
/**
|
|
26
|
+
* Helper function to create a release process error.
|
|
27
|
+
*/
|
|
28
|
+
export declare function releaseError(message: string): VrtError;
|
|
29
|
+
/**
|
|
30
|
+
* Helper function to create a not implemented error.
|
|
31
|
+
*/
|
|
32
|
+
export declare function notImplementedError(feature: string): VrtError;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error class for the VersaTiles Release Tool.
|
|
3
|
+
* Provides consistent error handling with categorization via error codes.
|
|
4
|
+
*/
|
|
5
|
+
export class VrtError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(message, code = 'VALIDATION_ERROR') {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'VrtError';
|
|
10
|
+
this.code = code;
|
|
11
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
12
|
+
if (Error.captureStackTrace) {
|
|
13
|
+
Error.captureStackTrace(this, VrtError);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Helper function to create a validation error.
|
|
19
|
+
*/
|
|
20
|
+
export function validationError(message) {
|
|
21
|
+
return new VrtError(message, 'VALIDATION_ERROR');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Helper function to create a markdown processing error.
|
|
25
|
+
*/
|
|
26
|
+
export function markdownError(message) {
|
|
27
|
+
return new VrtError(message, 'MARKDOWN_ERROR');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Helper function to create a git operation error.
|
|
31
|
+
*/
|
|
32
|
+
export function gitError(message) {
|
|
33
|
+
return new VrtError(message, 'GIT_ERROR');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Helper function to create a release process error.
|
|
37
|
+
*/
|
|
38
|
+
export function releaseError(message) {
|
|
39
|
+
return new VrtError(message, 'RELEASE_ERROR');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Helper function to create a not implemented error.
|
|
43
|
+
*/
|
|
44
|
+
export function notImplementedError(feature) {
|
|
45
|
+
return new VrtError(`Not implemented yet: "${feature}"`, 'NOT_IMPLEMENTED');
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=errors.js.map
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -1,14 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a Git commit with its SHA, message, and optional tag.
|
|
3
|
+
*/
|
|
1
4
|
export interface Commit {
|
|
5
|
+
/** The full SHA hash of the commit. */
|
|
2
6
|
sha: string;
|
|
7
|
+
/** The commit message (first line / subject). */
|
|
3
8
|
message: string;
|
|
9
|
+
/** The tag associated with this commit, if any. */
|
|
4
10
|
tag?: string;
|
|
5
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Conventional commit types and their display labels
|
|
14
|
+
*/
|
|
15
|
+
export declare const COMMIT_TYPES: {
|
|
16
|
+
readonly feat: "Features";
|
|
17
|
+
readonly fix: "Bug Fixes";
|
|
18
|
+
readonly docs: "Documentation";
|
|
19
|
+
readonly style: "Styles";
|
|
20
|
+
readonly refactor: "Code Refactoring";
|
|
21
|
+
readonly perf: "Performance Improvements";
|
|
22
|
+
readonly test: "Tests";
|
|
23
|
+
readonly build: "Build System";
|
|
24
|
+
readonly ci: "CI/CD";
|
|
25
|
+
readonly chore: "Chores";
|
|
26
|
+
readonly revert: "Reverts";
|
|
27
|
+
};
|
|
28
|
+
export type CommitType = keyof typeof COMMIT_TYPES;
|
|
29
|
+
/**
|
|
30
|
+
* Represents a parsed conventional commit
|
|
31
|
+
*/
|
|
32
|
+
export interface ParsedCommit extends Commit {
|
|
33
|
+
/** The conventional commit type (feat, fix, etc.) */
|
|
34
|
+
type?: CommitType;
|
|
35
|
+
/** The scope of the change (e.g., "api" in "feat(api): add endpoint") */
|
|
36
|
+
scope?: string;
|
|
37
|
+
/** The commit description without the type/scope prefix */
|
|
38
|
+
description: string;
|
|
39
|
+
/** Whether this is a breaking change */
|
|
40
|
+
breaking: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Parses a commit message according to the Conventional Commits specification.
|
|
44
|
+
* @see https://www.conventionalcommits.org/
|
|
45
|
+
*
|
|
46
|
+
* @param commit - The commit to parse
|
|
47
|
+
* @returns A ParsedCommit with type, scope, description, and breaking change info
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseConventionalCommit(commit: Commit): ParsedCommit;
|
|
50
|
+
/**
|
|
51
|
+
* Determines the suggested semver bump based on parsed commits.
|
|
52
|
+
* - Breaking changes -> major
|
|
53
|
+
* - Features -> minor
|
|
54
|
+
* - Everything else -> patch
|
|
55
|
+
*
|
|
56
|
+
* @param commits - Array of parsed commits
|
|
57
|
+
* @returns The suggested bump type: 'major', 'minor', or 'patch'
|
|
58
|
+
*/
|
|
59
|
+
export declare function getSuggestedBump(commits: ParsedCommit[]): 'major' | 'minor' | 'patch';
|
|
60
|
+
/**
|
|
61
|
+
* Groups parsed commits by their type for release notes.
|
|
62
|
+
*
|
|
63
|
+
* @param commits - Array of parsed commits
|
|
64
|
+
* @returns A Map of commit type to array of commits, plus 'other' for non-conventional commits
|
|
65
|
+
*/
|
|
66
|
+
export declare function groupCommitsByType(commits: ParsedCommit[]): Map<string, ParsedCommit[]>;
|
|
67
|
+
/**
|
|
68
|
+
* Interface for Git operations used by the release tool.
|
|
69
|
+
*/
|
|
6
70
|
export interface Git {
|
|
71
|
+
/**
|
|
72
|
+
* Gets the most recent semver tag from the repository.
|
|
73
|
+
* @returns The SHA and version string of the last tag, or undefined if no semver tags exist.
|
|
74
|
+
*/
|
|
7
75
|
getLastGitHubTag: () => Promise<{
|
|
8
76
|
sha: string;
|
|
9
77
|
version: string;
|
|
10
78
|
} | undefined>;
|
|
79
|
+
/**
|
|
80
|
+
* Gets the current (most recent) commit.
|
|
81
|
+
* @returns The current commit object.
|
|
82
|
+
*/
|
|
11
83
|
getCurrentGitHubCommit: () => Promise<Commit>;
|
|
84
|
+
/**
|
|
85
|
+
* Gets all commits between two commit SHAs.
|
|
86
|
+
* @param shaLast - The older commit SHA (exclusive).
|
|
87
|
+
* @param shaCurrent - The newer commit SHA (inclusive).
|
|
88
|
+
* @returns Array of commits between the two SHAs.
|
|
89
|
+
*/
|
|
12
90
|
getCommitsBetween: (shaLast?: string, shaCurrent?: string) => Promise<Commit[]>;
|
|
13
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Creates a Git interface for the specified directory.
|
|
94
|
+
* Provides methods to query commit history and tags.
|
|
95
|
+
*
|
|
96
|
+
* @param cwd - The working directory of the Git repository.
|
|
97
|
+
* @returns An object with methods to interact with the Git repository.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* const git = getGit('/path/to/repo');
|
|
102
|
+
* const lastTag = await git.getLastGitHubTag();
|
|
103
|
+
* const commits = await git.getCommitsBetween(lastTag?.sha, 'HEAD');
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
14
106
|
export declare function getGit(cwd: string): Git;
|
package/dist/lib/git.js
CHANGED
|
@@ -1,4 +1,100 @@
|
|
|
1
1
|
import { Shell } from './shell.js';
|
|
2
|
+
/**
|
|
3
|
+
* Conventional commit types and their display labels
|
|
4
|
+
*/
|
|
5
|
+
export const COMMIT_TYPES = {
|
|
6
|
+
feat: 'Features',
|
|
7
|
+
fix: 'Bug Fixes',
|
|
8
|
+
docs: 'Documentation',
|
|
9
|
+
style: 'Styles',
|
|
10
|
+
refactor: 'Code Refactoring',
|
|
11
|
+
perf: 'Performance Improvements',
|
|
12
|
+
test: 'Tests',
|
|
13
|
+
build: 'Build System',
|
|
14
|
+
ci: 'CI/CD',
|
|
15
|
+
chore: 'Chores',
|
|
16
|
+
revert: 'Reverts',
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Parses a commit message according to the Conventional Commits specification.
|
|
20
|
+
* @see https://www.conventionalcommits.org/
|
|
21
|
+
*
|
|
22
|
+
* @param commit - The commit to parse
|
|
23
|
+
* @returns A ParsedCommit with type, scope, description, and breaking change info
|
|
24
|
+
*/
|
|
25
|
+
export function parseConventionalCommit(commit) {
|
|
26
|
+
const message = commit.message.trim();
|
|
27
|
+
// Pattern: type(scope)!: description OR type!: description OR type(scope): description OR type: description
|
|
28
|
+
// Note: No space allowed before colon per Conventional Commits spec
|
|
29
|
+
const conventionalPattern = /^(\w+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/;
|
|
30
|
+
const match = message.match(conventionalPattern);
|
|
31
|
+
if (!match) {
|
|
32
|
+
// Not a conventional commit - return with original message as description
|
|
33
|
+
return {
|
|
34
|
+
...commit,
|
|
35
|
+
description: message,
|
|
36
|
+
breaking: message.toUpperCase().includes('BREAKING CHANGE'),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const [, typeStr, scope, breakingMark, description] = match;
|
|
40
|
+
const type = typeStr.toLowerCase();
|
|
41
|
+
const isKnownType = type in COMMIT_TYPES;
|
|
42
|
+
return {
|
|
43
|
+
...commit,
|
|
44
|
+
type: isKnownType ? type : undefined,
|
|
45
|
+
scope: scope || undefined,
|
|
46
|
+
description: description.trim(),
|
|
47
|
+
breaking: breakingMark === '!' || message.toUpperCase().includes('BREAKING CHANGE'),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Determines the suggested semver bump based on parsed commits.
|
|
52
|
+
* - Breaking changes -> major
|
|
53
|
+
* - Features -> minor
|
|
54
|
+
* - Everything else -> patch
|
|
55
|
+
*
|
|
56
|
+
* @param commits - Array of parsed commits
|
|
57
|
+
* @returns The suggested bump type: 'major', 'minor', or 'patch'
|
|
58
|
+
*/
|
|
59
|
+
export function getSuggestedBump(commits) {
|
|
60
|
+
const hasBreaking = commits.some((c) => c.breaking);
|
|
61
|
+
if (hasBreaking)
|
|
62
|
+
return 'major';
|
|
63
|
+
const hasFeature = commits.some((c) => c.type === 'feat');
|
|
64
|
+
if (hasFeature)
|
|
65
|
+
return 'minor';
|
|
66
|
+
return 'patch';
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Groups parsed commits by their type for release notes.
|
|
70
|
+
*
|
|
71
|
+
* @param commits - Array of parsed commits
|
|
72
|
+
* @returns A Map of commit type to array of commits, plus 'other' for non-conventional commits
|
|
73
|
+
*/
|
|
74
|
+
export function groupCommitsByType(commits) {
|
|
75
|
+
const groups = new Map();
|
|
76
|
+
for (const commit of commits) {
|
|
77
|
+
const key = commit.type ?? 'other';
|
|
78
|
+
const existing = groups.get(key) ?? [];
|
|
79
|
+
existing.push(commit);
|
|
80
|
+
groups.set(key, existing);
|
|
81
|
+
}
|
|
82
|
+
return groups;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Creates a Git interface for the specified directory.
|
|
86
|
+
* Provides methods to query commit history and tags.
|
|
87
|
+
*
|
|
88
|
+
* @param cwd - The working directory of the Git repository.
|
|
89
|
+
* @returns An object with methods to interact with the Git repository.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* const git = getGit('/path/to/repo');
|
|
94
|
+
* const lastTag = await git.getLastGitHubTag();
|
|
95
|
+
* const commits = await git.getCommitsBetween(lastTag?.sha, 'HEAD');
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
2
98
|
export function getGit(cwd) {
|
|
3
99
|
const shell = new Shell(cwd);
|
|
4
100
|
return {
|
|
@@ -6,22 +102,30 @@ export function getGit(cwd) {
|
|
|
6
102
|
getCurrentGitHubCommit,
|
|
7
103
|
getCommitsBetween,
|
|
8
104
|
};
|
|
105
|
+
/**
|
|
106
|
+
* Finds the most recent commit tagged with a semver version (vX.Y.Z format).
|
|
107
|
+
* Supports pre-release and build metadata suffixes.
|
|
108
|
+
*/
|
|
9
109
|
async function getLastGitHubTag() {
|
|
10
110
|
const commits = await getAllCommits();
|
|
11
111
|
const result = commits
|
|
12
|
-
.map(commit => ({
|
|
112
|
+
.map((commit) => ({
|
|
13
113
|
sha: commit.sha,
|
|
14
|
-
version: commit.tag?.match(/^v(\d+\.\d+\.\d+)$/)?.[1],
|
|
114
|
+
version: commit.tag?.match(/^v(\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?)$/)?.[1],
|
|
15
115
|
}))
|
|
16
|
-
.find(r => r.version);
|
|
116
|
+
.find((r) => r.version);
|
|
17
117
|
return result;
|
|
18
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Retrieves all commits from the repository's history.
|
|
121
|
+
* Parses the git log output to extract SHA, message, and tag information.
|
|
122
|
+
*/
|
|
19
123
|
async function getAllCommits() {
|
|
20
|
-
const result = await shell.stdout(
|
|
124
|
+
const result = await shell.stdout("git log --pretty=format:'⍃%H⍄%s⍄%D⍄'");
|
|
21
125
|
return result
|
|
22
126
|
.split('⍃')
|
|
23
|
-
.filter(line => line.length > 2)
|
|
24
|
-
.map(line => {
|
|
127
|
+
.filter((line) => line.length > 2)
|
|
128
|
+
.map((line) => {
|
|
25
129
|
const obj = line.split('⍄');
|
|
26
130
|
return {
|
|
27
131
|
sha: obj[0],
|
|
@@ -30,15 +134,23 @@ export function getGit(cwd) {
|
|
|
30
134
|
};
|
|
31
135
|
});
|
|
32
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Returns the most recent commit in the repository.
|
|
139
|
+
*/
|
|
33
140
|
async function getCurrentGitHubCommit() {
|
|
34
141
|
return (await getAllCommits())[0];
|
|
35
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Gets commits between two SHA hashes.
|
|
145
|
+
* If shaCurrent is provided, starts from that commit.
|
|
146
|
+
* If shaLast is provided, stops before that commit.
|
|
147
|
+
*/
|
|
36
148
|
async function getCommitsBetween(shaLast, shaCurrent) {
|
|
37
149
|
let commits = await getAllCommits();
|
|
38
|
-
const start = commits.findIndex(commit => commit.sha === shaCurrent);
|
|
150
|
+
const start = commits.findIndex((commit) => commit.sha === shaCurrent);
|
|
39
151
|
if (start >= 0)
|
|
40
152
|
commits = commits.slice(start);
|
|
41
|
-
const end = commits.findIndex(commit => commit.sha === shaLast);
|
|
153
|
+
const end = commits.findIndex((commit) => commit.sha === shaLast);
|
|
42
154
|
if (end >= 0)
|
|
43
155
|
commits = commits.slice(0, end);
|
|
44
156
|
return commits;
|