UNPKG

6.6 kBJavaScriptView Raw
1'use strict'
2
3const path = require('path')
4const fs = require('fs')
5const assert = require('assert')
6
7// Read in all the handlebars templates and partials
8const hbsPartials = ['commit', 'header', 'main', 'notes'].reduce((acc, filename) => {
9 acc[filename] = fs.readFileSync(
10 path.resolve(__dirname, `../templates/${filename}.hbs`),
11 { encoding: 'utf-8' },
12 )
13 return acc
14}, {})
15
16/**
17 * Handles formatting a host, owner and repo into a base for the repo url
18 */
19const formatRepoUrlBase = (host, owner, repository) =>
20 `${host ? `${host}/` : ''}${owner ? `${owner}/` : ''}${repository}`
21
22/**
23 * Format the release header
24 */
25const formatReleaseHeader = ({
26 currentTag,
27 date,
28 host,
29 isPatch,
30 owner,
31 previousTag,
32 repository,
33 title,
34 version,
35}) => {
36 const repoUrlBase = formatRepoUrlBase(host, owner, repository)
37 const headerLevel = isPatch ? '###' : '##'
38
39 let header = `${headerLevel} [${version}](${repoUrlBase}/compare/${previousTag}...${currentTag})`
40 if (title) header += ` ${title}`
41 if (date) header += ` (${date})`
42 return header
43}
44
45/**
46 * Decorate each reference with: completeReference, referenceUrl
47 */
48const decorateReference = (ref, { host, owner, repository }) => {
49 // Ref: [owner/repository#issue](host/owner/repository/issue)
50 // Where owner && repository will not be defined for a same repo issue,
51 // and will be defined for outside repo references
52 const referenceBase = `${ref.owner ? `${ref.owner}/` : ''}${ref.repository || ''}`
53 let referenceRepoUrlBase
54 if (ref.owner || ref.repository) {
55 referenceRepoUrlBase = formatRepoUrlBase(host, ref.owner, ref.repository)
56 } else {
57 referenceRepoUrlBase = formatRepoUrlBase(host, owner, repository)
58 }
59
60 return {
61 ...ref,
62 completeReference: `${referenceBase}#${ref.issue}`,
63 referenceUrl: `${referenceRepoUrlBase}/issue/${ref.issue}`,
64 }
65}
66
67/**
68 * Decorate each commit with: commitUrl, shortHash
69 */
70const decorateCommit = (commit, { host, owner, repository }) => {
71 const { hash, message, references } = commit
72
73 // Ensure commit message exists b/c it's important
74 if (!message) console.error(`Commit is missing a message!`, commit) // eslint-disable-line no-console
75
76 return {
77 ...commit,
78 commitUrl: `${formatRepoUrlBase(host, owner, repository)}/commit/${hash}`,
79 // Add a short version of hash for anchor text
80 shortHash: hash.slice(0, 7),
81 // Ensure commit message is capitalized
82 message: message.slice(0, 1).toUpperCase() + message.slice(1),
83 references: references
84 ? references.map((ref) => decorateReference(ref, { host, owner, repository }))
85 : null,
86 }
87}
88
89const commitGroupOrder = [
90 'Breaking',
91 'New',
92 'Update',
93 'Fix',
94 'Chore',
95 'Docs',
96 'Upgrade',
97 'Build',
98]
99
100/**
101 * Handle combining the non consumer impacting commit types into a single group
102 */
103const mergeCommitGroups = (commitGroups) => {
104 const primaryGroupTitles = ['Breaking', 'New', 'Update', 'Fix']
105 const decoratedCommitGroups = []
106 const secondaryCommitGroup = { title: '', commits: [] }
107
108 // Sort commit groups for deterministic order
109 commitGroups.sort(
110 (a, b) => commitGroupOrder.indexOf(a.title) - commitGroupOrder.indexOf(b.title),
111 )
112
113 // Step through commit groups ->
114 // IF primary THEN push to primary groups unmodified
115 // IF secondary THEN merge into secondary groups
116 commitGroups.forEach((commitGroup) => {
117 if (primaryGroupTitles.indexOf(commitGroup.title) !== -1) {
118 decoratedCommitGroups.push(commitGroup)
119 } else {
120 const { title, commits } = commitGroup
121 secondaryCommitGroup.title += secondaryCommitGroup.title.length
122 ? `, ${title}`
123 : title
124 secondaryCommitGroup.commits = secondaryCommitGroup.commits.concat(commits)
125 }
126 })
127
128 decoratedCommitGroups.push(secondaryCommitGroup)
129 return decoratedCommitGroups
130}
131
132/**
133 * Decorate the important commit group titles with an emoji
134 */
135const decorateCommitGroupTitle = (title) => {
136 switch (title.toLowerCase()) {
137 case 'breaking':
138 return '💥 Breaking'
139 case 'new':
140 return '💖 New'
141 case 'update':
142 return '✨ Update'
143 case 'fix':
144 return '🛠 Fix'
145 default:
146 return title
147 }
148}
149
150/**
151 * Decorate the note group titles for better display
152 */
153const decorateNoteGroupTitle = (title) => {
154 switch (title.toLowerCase()) {
155 case 'breaking change':
156 return '💥 Breaking Changes!'
157 case 'release notes':
158 return '🔖 Release Notes'
159 default:
160 return title
161 }
162}
163
164// prettier-ignore
165const groupsOrder = ['Breaking','New','Update','Fix','Docs','Build','Upgrade','Chore']
166
167/**
168 * Configs for the changelog writer
169 */
170const writerOpts = {
171 // Commits are grouped by the tag
172 groupBy: 'tag',
173 // Groups are sorted by their title
174 commitGroupsSort: (a, b) => groupsOrder.indexOf(a.title) > groupsOrder.indexOf(b.title),
175 // Commits are sorted by tag then message?
176 commitsSort: ['tag', 'message'],
177 // Ensure that only properly formatted commits are formatting by using
178 // transform as a filter
179 transform: (commit) => {
180 if (!commit.tag || typeof commit.tag !== `string`) return false
181 return commit
182 },
183 // Handle decorating and preparing data to keep all logic out of the hbs
184 // templates
185 finalizeContext: (context /* , options, commits, keyCommit */) => {
186 const { host, owner, repository } = context
187
188 assert(Array.isArray(context.commitGroups), 'Commit groups must be an array')
189 assert(Array.isArray(context.noteGroups), 'Note groups must be an array')
190
191 const mergedCommitGroups = mergeCommitGroups(context.commitGroups)
192
193 return {
194 ...context,
195 releaseHeader: formatReleaseHeader(context),
196 commitGroups: mergedCommitGroups.map((commitGroup) => ({
197 ...commitGroup,
198 title: decorateCommitGroupTitle(commitGroup.title),
199 commits: commitGroup.commits.map((commit) =>
200 decorateCommit(commit, { host, owner, repository }),
201 ),
202 })),
203 noteGroups: context.noteGroups.map((noteGroup) => ({
204 ...noteGroup,
205 title: decorateNoteGroupTitle(noteGroup.title),
206 })),
207 }
208 },
209 // Overwrite the primary hbs partials
210 mainTemplate: hbsPartials.main,
211 headerPartial: hbsPartials.header,
212 commitPartial: hbsPartials.commit,
213 // Registers custom partials
214 partials: {
215 notes: hbsPartials.notes,
216 },
217}
218
219// Update release notes parser options to add ability to include release notes
220// in a commit with keyword `RELEASE NOTES`
221const parserOpts = { noteKeywords: ['BREAKING CHANGE', 'RELEASE NOTES'] }
222
223module.exports = { writerOpts, parserOpts }