@alleyinteractive/changelog-extractor 0.1.0 ā 0.4.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 +16 -4
- package/dist/index.js +86 -2
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/cli.js +35 -9
- package/src/parser.js +86 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alleyinteractive/changelog-extractor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Parse and extract structured information from Keep a Changelog formatted changelogs",
|
|
6
6
|
"main": "src/parser.js",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
"prepublishOnly": "npm test",
|
|
16
16
|
"prettier:fix": "npx prettier --write .",
|
|
17
17
|
"prettier": "npx prettier --check .",
|
|
18
|
-
"release:major": "npm
|
|
19
|
-
"release:minor": "npm
|
|
20
|
-
"release:patch": "npm
|
|
18
|
+
"release:major": "npm version major && npm publish --access public && git push --follow-tags",
|
|
19
|
+
"release:minor": "npm version minor && npm publish --access public && git push --follow-tags",
|
|
20
|
+
"release:patch": "npm version patch && npm publish --access public && git push --follow-tags",
|
|
21
21
|
"test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
package/src/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ function printUsage() {
|
|
|
20
20
|
Usage: npx @alleyinteractive/changelog-extractor [options]
|
|
21
21
|
|
|
22
22
|
Options:
|
|
23
|
+
--format <type> Output format (default: "json"). Options: dump, text, json.
|
|
23
24
|
-f, --file <path> Path to changelog file (default: CHANGELOG.md)
|
|
24
25
|
-v, --version <ver> Specific version to extract (e.g., "1.14.0" or "v1.14.0")
|
|
25
26
|
Leave empty to return all versions
|
|
@@ -64,6 +65,20 @@ if (versionIndex !== -1 && args[versionIndex + 1]) {
|
|
|
64
65
|
|
|
65
66
|
const version = versionArg
|
|
66
67
|
|
|
68
|
+
// Parse format option.
|
|
69
|
+
let format = 'json'
|
|
70
|
+
const formatIndex = args.findIndex((arg) => arg === '--format')
|
|
71
|
+
if (formatIndex !== -1 && args[formatIndex + 1]) {
|
|
72
|
+
format = args[formatIndex + 1].toLowerCase()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!['json', 'dump', 'text'].includes(format)) {
|
|
76
|
+
console.error(
|
|
77
|
+
`ā Error: Invalid format "${format}". Valid options are: json, dump, text.`
|
|
78
|
+
)
|
|
79
|
+
process.exit(1)
|
|
80
|
+
}
|
|
81
|
+
|
|
67
82
|
// Read changelog file
|
|
68
83
|
const changelogPath = path.resolve(process.cwd(), changelogFile)
|
|
69
84
|
|
|
@@ -86,18 +101,28 @@ try {
|
|
|
86
101
|
process.exit(0)
|
|
87
102
|
}
|
|
88
103
|
|
|
104
|
+
// Default JSON output.
|
|
105
|
+
if (format === 'json') {
|
|
106
|
+
process.stdout.write(JSON.stringify(results, null, 2) + '\n')
|
|
107
|
+
process.exit(0)
|
|
108
|
+
}
|
|
109
|
+
|
|
89
110
|
// Parse changelog
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
111
|
+
if (format === 'dump') {
|
|
112
|
+
console.log(`š Parsing: ${changelogPath}`)
|
|
113
|
+
console.log(`šÆ Version: ${versionArg || 'all versions'}`)
|
|
114
|
+
console.log('')
|
|
115
|
+
}
|
|
93
116
|
|
|
94
117
|
if (results.length === 0) {
|
|
95
118
|
console.log('ā ļø No changelog entries found')
|
|
96
119
|
process.exit(1)
|
|
97
120
|
}
|
|
98
121
|
|
|
99
|
-
|
|
100
|
-
|
|
122
|
+
if (format === 'dump') {
|
|
123
|
+
console.log(`ā
Found ${results.length} version(s)\n`)
|
|
124
|
+
console.log('ā'.repeat(80))
|
|
125
|
+
}
|
|
101
126
|
|
|
102
127
|
// Pretty print results
|
|
103
128
|
results.forEach((entry, index) => {
|
|
@@ -126,10 +151,11 @@ try {
|
|
|
126
151
|
}
|
|
127
152
|
})
|
|
128
153
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
154
|
+
if (format === 'dump') {
|
|
155
|
+
console.log('\n' + 'ā'.repeat(80))
|
|
156
|
+
console.log('\nš JSON Output:\n')
|
|
157
|
+
process.stdout.write(JSON.stringify(results, null, 2) + '\n')
|
|
158
|
+
}
|
|
133
159
|
} catch (error) {
|
|
134
160
|
console.error('ā Error parsing changelog:', error.message)
|
|
135
161
|
process.exit(1)
|
package/src/parser.js
CHANGED
|
@@ -1,3 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unwrap text that has been wrapped across multiple lines.
|
|
3
|
+
* This fixes changelog text that has been hard-wrapped for readability
|
|
4
|
+
* but should flow as continuous text.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} text - The text content to unwrap
|
|
7
|
+
* @returns {string} The unwrapped text
|
|
8
|
+
*/
|
|
9
|
+
function unwrapText(text) {
|
|
10
|
+
const lines = text.split('\n')
|
|
11
|
+
const unwrappedLines = []
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < lines.length; i++) {
|
|
14
|
+
let currentLine = lines[i]
|
|
15
|
+
|
|
16
|
+
// Skip empty lines - preserve them as-is
|
|
17
|
+
if (currentLine.trim() === '') {
|
|
18
|
+
unwrappedLines.push(currentLine)
|
|
19
|
+
continue
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Collect continuation lines
|
|
23
|
+
let j = i + 1
|
|
24
|
+
while (j < lines.length) {
|
|
25
|
+
const nextLine = lines[j]
|
|
26
|
+
|
|
27
|
+
// Stop if next line is empty
|
|
28
|
+
if (nextLine.trim() === '') {
|
|
29
|
+
break
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const trimmedCurrent = currentLine.trim()
|
|
33
|
+
const trimmedNext = nextLine.trim()
|
|
34
|
+
|
|
35
|
+
// Don't unwrap if current line looks like it ends a sentence/item
|
|
36
|
+
const endsWithPunctuation = /[.!?:;]$/.test(trimmedCurrent)
|
|
37
|
+
const endsWithCode = /`$/.test(trimmedCurrent)
|
|
38
|
+
const endsWithBullet = /[-*+]\s*$/.test(trimmedCurrent)
|
|
39
|
+
|
|
40
|
+
// Don't unwrap if next line starts a new bullet point, section, or sentence
|
|
41
|
+
const startsWithBullet = /^[-*+]\s/.test(trimmedNext)
|
|
42
|
+
const startsWithNumber = /^\d+\.\s/.test(trimmedNext)
|
|
43
|
+
const startsWithMarkdown = /^[#>]/.test(trimmedNext)
|
|
44
|
+
|
|
45
|
+
// More nuanced check for new sentences
|
|
46
|
+
const prevEndsWithContinuation =
|
|
47
|
+
/[,&]$/.test(trimmedCurrent) ||
|
|
48
|
+
/\b(and|or|but|with|for|to|from|in|on|at|by)$/.test(trimmedCurrent)
|
|
49
|
+
const startsWithCapitalNewSentence =
|
|
50
|
+
/^[A-Z]/.test(trimmedNext) &&
|
|
51
|
+
!prevEndsWithContinuation &&
|
|
52
|
+
endsWithPunctuation
|
|
53
|
+
|
|
54
|
+
// Determine if we should unwrap this line with the next
|
|
55
|
+
const shouldUnwrap =
|
|
56
|
+
!endsWithPunctuation &&
|
|
57
|
+
!endsWithCode &&
|
|
58
|
+
!endsWithBullet &&
|
|
59
|
+
!startsWithBullet &&
|
|
60
|
+
!startsWithNumber &&
|
|
61
|
+
!startsWithCapitalNewSentence &&
|
|
62
|
+
!startsWithMarkdown
|
|
63
|
+
|
|
64
|
+
if (shouldUnwrap) {
|
|
65
|
+
// Join the lines, preserving the original indentation of the first line
|
|
66
|
+
// but removing indentation from continuation lines
|
|
67
|
+
currentLine = currentLine + ' ' + trimmedNext
|
|
68
|
+
j++ // Move to next line
|
|
69
|
+
} else {
|
|
70
|
+
break // Stop unwrapping
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
unwrappedLines.push(currentLine)
|
|
75
|
+
i = j - 1 // Skip the lines we've consumed (j-1 because for loop will increment)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return unwrappedLines.join('\n')
|
|
79
|
+
}
|
|
80
|
+
|
|
1
81
|
/**
|
|
2
82
|
* Parse a Keep a Changelog formatted changelog file.
|
|
3
83
|
*
|
|
@@ -98,7 +178,9 @@ export function parseChangelog(changelogContent, version = null) {
|
|
|
98
178
|
|
|
99
179
|
const content = currentSectionContent.join('\n')
|
|
100
180
|
if (content.trim()) {
|
|
101
|
-
|
|
181
|
+
// Unwrap any text that has been hard-wrapped across lines
|
|
182
|
+
const unwrappedContent = unwrapText(content)
|
|
183
|
+
currentVersion.sections[currentSection] = unwrappedContent
|
|
102
184
|
}
|
|
103
185
|
}
|
|
104
186
|
}
|
|
@@ -120,7 +202,9 @@ export function parseChangelog(changelogContent, version = null) {
|
|
|
120
202
|
|
|
121
203
|
const content = trimmedContent.join('\n')
|
|
122
204
|
if (content.trim()) {
|
|
123
|
-
|
|
205
|
+
// Unwrap any text that has been hard-wrapped across lines
|
|
206
|
+
const unwrappedContent = unwrapText(content)
|
|
207
|
+
currentVersion.sections['general'] = unwrappedContent
|
|
124
208
|
}
|
|
125
209
|
generalContent = []
|
|
126
210
|
}
|