@@ -3,11 +3,33 @@ import {createReadStream, createWriteStream, readFileSync} from 'fs';
3
3
import { prompt } from 'inquirer' ;
4
4
import { join } from 'path' ;
5
5
import { Readable } from 'stream' ;
6
+ import { releasePackages } from './release-output/release-packages' ;
6
7
7
8
// These imports lack type definitions.
8
9
const conventionalChangelog = require ( 'conventional-changelog' ) ;
10
+ const changelogCompare = require ( 'conventional-changelog-writer/lib/util' ) ;
9
11
const merge2 = require ( 'merge2' ) ;
10
12
13
+ /** Interface that describes a package in the changelog. */
14
+ interface ChangelogPackage {
15
+ commits : any [ ] ;
16
+ breakingChanges : any [ ] ;
17
+ }
18
+
19
+ /** Hardcoded order of packages shown in the changelog. */
20
+ const changelogPackageOrder = [
21
+ 'cdk' ,
22
+ 'material' ,
23
+ 'google-maps' ,
24
+ 'youtube-player' ,
25
+ 'material-moment-adapter' ,
26
+ 'cdk-experimental' ,
27
+ 'material-experimental' ,
28
+ ] ;
29
+
30
+ /** List of packages which are excluded in the changelog. */
31
+ const excludedChangelogPackages = [ 'google-maps' ] ;
32
+
11
33
/** Prompts for a changelog release name and prepends the new changelog. */
12
34
export async function promptAndGenerateChangelog ( changelogPath : string ) {
13
35
const releaseName = await promptChangelogReleaseName ( ) ;
@@ -21,11 +43,16 @@ export async function promptAndGenerateChangelog(changelogPath: string) {
21
43
*/
22
44
export async function prependChangelogFromLatestTag ( changelogPath : string , releaseName : string ) {
23
45
const outputStream : Readable = conventionalChangelog (
24
- /* core options */ { preset : 'angular' } ,
25
- /* context options */ { title : releaseName } ,
26
- /* raw-commits options */ null ,
27
- /* commit parser options */ null ,
28
- /* writer options */ createChangelogWriterOptions ( changelogPath ) ) ;
46
+ /* core options */ { preset : 'angular' } ,
47
+ /* context options */ { title : releaseName } ,
48
+ /* raw-commits options */ null ,
49
+ /* commit parser options */ {
50
+ // Expansion of the convention-changelog-angular preset to extract the package
51
+ // name from the commit message.
52
+ headerPattern : / ^ ( \w * ) (?: \( (?: ( [ ^ / ] + ) \/ ) ? ( .* ) \) ) ? : ( .* ) $ / ,
53
+ headerCorrespondence : [ 'type' , 'package' , 'scope' , 'subject' ] ,
54
+ } ,
55
+ /* writer options */ createChangelogWriterOptions ( changelogPath ) ) ;
29
56
30
57
// Stream for reading the existing changelog. This is necessary because we want to
31
58
// actually prepend the new changelog to the existing one.
@@ -41,19 +68,20 @@ export async function prependChangelogFromLatestTag(changelogPath: string, relea
41
68
// read and write from the same source which causes the content to be thrown off.
42
69
previousChangelogStream . on ( 'end' , ( ) => {
43
70
mergedCompleteChangelog . pipe ( createWriteStream ( changelogPath ) )
44
- . once ( 'error' , ( error : any ) => reject ( error ) )
45
- . once ( 'finish' , ( ) => resolve ( ) ) ;
71
+ . once ( 'error' , ( error : any ) => reject ( error ) )
72
+ . once ( 'finish' , ( ) => resolve ( ) ) ;
46
73
} ) ;
47
74
} ) ;
48
75
}
49
76
50
77
/** Prompts the terminal for a changelog release name. */
51
78
export async function promptChangelogReleaseName ( ) : Promise < string > {
52
79
return ( await prompt < { releaseName : string } > ( {
53
- type : 'text' ,
54
- name : 'releaseName' ,
55
- message : 'What should be the name of the release?'
56
- } ) ) . releaseName ;
80
+ type : 'text' ,
81
+ name : 'releaseName' ,
82
+ message : 'What should be the name of the release?'
83
+ } ) )
84
+ . releaseName ;
57
85
}
58
86
59
87
/**
@@ -68,45 +96,116 @@ export async function promptChangelogReleaseName(): Promise<string> {
68
96
*/
69
97
function createChangelogWriterOptions ( changelogPath : string ) {
70
98
const existingChangelogContent = readFileSync ( changelogPath , 'utf8' ) ;
99
+ const commitSortFunction = changelogCompare . functionify ( [ 'type' , 'scope' , 'subject' ] ) ;
71
100
72
101
return {
102
+ // Overwrite the changelog templates so that we can render the commits grouped
103
+ // by package names. Templates are based on the original templates of the
104
+ // angular preset: "conventional-changelog-angular/templates".
105
+ mainTemplate : readFileSync ( join ( __dirname , 'changelog-root-template.hbs' ) , 'utf8' ) ,
106
+ commitPartial : readFileSync ( join ( __dirname , 'changelog-commit-template.hbs' ) , 'utf8' ) ,
107
+
73
108
// Specify a writer option that can be used to modify the content of a new changelog section.
74
109
// See: conventional-changelog/tree/master/packages/conventional-changelog-writer
75
110
finalizeContext : ( context : any ) => {
76
- context . commitGroups = context . commitGroups . filter ( ( group : any ) => {
77
- group . commits = group . commits . filter ( ( commit : any ) => {
78
-
79
- // Commits that change things for "cdk-experimental" or "material-experimental" will also
80
- // show up in the changelog by default. We don't want to show these in the changelog.
81
- if ( commit . scope && commit . scope . includes ( 'experimental' ) ) {
82
- console . log ( yellow ( ` ↺ Skipping experimental: "${ bold ( commit . header ) } "` ) ) ;
83
- return false ;
84
- }
111
+ const packageGroups : { [ packageName : string ] : ChangelogPackage } = { } ;
85
112
113
+ context . commitGroups . forEach ( ( group : any ) => {
114
+ group . commits . forEach ( ( commit : any ) => {
86
115
// Filter out duplicate commits. Note that we cannot compare the SHA because the commits
87
116
// will have a different SHA if they are being cherry-picked into a different branch.
88
117
if ( existingChangelogContent . includes ( commit . subject ) ) {
89
118
console . log ( yellow ( ` ↺ Skipping duplicate: "${ bold ( commit . header ) } "` ) ) ;
90
119
return false ;
91
120
}
92
- return true ;
121
+
122
+ // Commits which just specify a scope that refers to a package but do not follow
123
+ // the commit format that is parsed by the conventional-changelog-parser, can be
124
+ // still resolved to their package from the scope. This handles the case where
125
+ // a commit targets the whole package and does not specify a specific scope.
126
+ // e.g. "refactor(material-experimental): support strictness flags".
127
+ if ( ! commit . package && commit . scope ) {
128
+ const matchingPackage = releasePackages . find ( pkgName => pkgName === commit . scope ) ;
129
+ if ( matchingPackage ) {
130
+ commit . scope = null ;
131
+ commit . package = matchingPackage ;
132
+ }
133
+ }
134
+
135
+ // TODO(devversion): once we formalize the commit message format and
136
+ // require specifying the "material" package explicitly, we can remove
137
+ // the fallback to the "material" package.
138
+ const packageName = commit . package || 'material' ;
139
+ const type = getTypeOfCommitGroupDescription ( group . title ) ;
140
+
141
+ if ( ! packageGroups [ packageName ] ) {
142
+ packageGroups [ packageName ] = { commits : [ ] , breakingChanges : [ ] } ;
143
+ }
144
+ const packageGroup = packageGroups [ packageName ] ;
145
+
146
+ packageGroup . breakingChanges . push ( ...commit . notes ) ;
147
+ packageGroup . commits . push ( { ...commit , type} ) ;
93
148
} ) ;
149
+ } ) ;
94
150
95
- // Filter out commit groups which don't have any commits. Commit groups will become
96
- // empty if we filter out all duplicated commits.
97
- return group . commits . length ;
151
+ const sortedPackageGroupNames =
152
+ Object . keys ( packageGroups )
153
+ . filter ( pkgName => ! excludedChangelogPackages . includes ( pkgName ) )
154
+ . sort ( preferredOrderComparator ) ;
155
+
156
+ context . packageGroups = sortedPackageGroupNames . map ( pkgName => {
157
+ const packageGroup = packageGroups [ pkgName ] ;
158
+ return {
159
+ title : pkgName ,
160
+ commits : packageGroup . commits . sort ( commitSortFunction ) ,
161
+ breakingChanges : packageGroup . breakingChanges ,
162
+ } ;
98
163
} ) ;
99
164
100
165
return context ;
101
166
}
102
167
} ;
103
168
}
104
169
170
+ /**
171
+ * Comparator function that sorts a given array of strings based on the
172
+ * hardcoded changelog package order. Entries which are not hardcoded are
173
+ * sorted in alphabetical order after the hardcoded entries.
174
+ */
175
+ function preferredOrderComparator ( a : string , b : string ) : number {
176
+ const aIndex = changelogPackageOrder . indexOf ( a ) ;
177
+ const bIndex = changelogPackageOrder . indexOf ( b ) ;
178
+ // If a package name could not be found in the hardcoded order, it should be
179
+ // sorted after the hardcoded entries in alphabetical order.
180
+ if ( aIndex === - 1 ) {
181
+ return bIndex === - 1 ? a . localeCompare ( b ) : 1 ;
182
+ } else if ( bIndex === - 1 ) {
183
+ return - 1 ;
184
+ }
185
+ return aIndex - bIndex ;
186
+ }
187
+
188
+ /** Gets the type of a commit group description. */
189
+ function getTypeOfCommitGroupDescription ( description : string ) : string {
190
+ if ( description === 'Features' ) {
191
+ return 'feature' ;
192
+ } else if ( description === 'Bug Fixes' ) {
193
+ return 'bug fix' ;
194
+ } else if ( description === 'Performance Improvements' ) {
195
+ return 'performance' ;
196
+ } else if ( description === 'Reverts' ) {
197
+ return 'revert' ;
198
+ } else if ( description === 'Documentation' ) {
199
+ return 'docs' ;
200
+ } else if ( description === 'Code Refactoring' ) {
201
+ return 'refactor' ;
202
+ }
203
+ return description . toLowerCase ( ) ;
204
+ }
205
+
105
206
/** Entry-point for generating the changelog when called through the CLI. */
106
207
if ( require . main === module ) {
107
208
promptAndGenerateChangelog ( join ( __dirname , '../../CHANGELOG.md' ) ) . then ( ( ) => {
108
209
console . log ( green ( ' ✓ Successfully updated the changelog.' ) ) ;
109
210
} ) ;
110
211
}
111
-
112
-
0 commit comments