@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alleyinteractive/changelog-extractor",
3
- "version": "0.1.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 run version:major && npm publish --access public && git push --follow-tags",
19
- "release:minor": "npm run version:minor && npm publish --access public && git push --follow-tags",
20
- "release:patch": "npm run version:patch && npm publish --access public && git push --follow-tags",
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
- console.log(`šŸ“– Parsing: ${changelogPath}`)
91
- console.log(`šŸŽÆ Version: ${versionArg || 'all versions'}`)
92
- console.log('')
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
- console.log(`āœ… Found ${results.length} version(s)\n`)
100
- console.log('─'.repeat(80))
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
- // Output JSON at the end
130
- console.log('\n' + '═'.repeat(80))
131
- console.log('\nšŸ“‹ JSON Output:\n')
132
- console.log(JSON.stringify(results, null, 2))
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
- currentVersion.sections[currentSection] = content
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
- currentVersion.sections['general'] = content
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
  }