1 | #!/usr/bin/env node
|
2 | import { resolve, extname } from 'path'
|
3 | import { pathToFileURL } from 'url'
|
4 | import {
|
5 | createReadStream,
|
6 | createWriteStream
|
7 | } from 'fs'
|
8 | import { readFile } from 'fs/promises'
|
9 | import addStream from 'add-stream'
|
10 | import tempfile from 'tempfile'
|
11 | import meow from 'meow'
|
12 | import conventionalChangelog from 'conventional-changelog'
|
13 |
|
14 | function relativeResolve (filePath) {
|
15 | return pathToFileURL(resolve(process.cwd(), filePath))
|
16 | }
|
17 |
|
18 | async function loadDataFile (filePath) {
|
19 | const resolvedFilePath = relativeResolve(filePath)
|
20 | const ext = extname(resolvedFilePath.toString())
|
21 |
|
22 | if (ext === '.json') {
|
23 | return JSON.parse(await readFile(resolvedFilePath, 'utf8'))
|
24 | }
|
25 |
|
26 | return (await import(resolvedFilePath)).default
|
27 | }
|
28 |
|
29 | const cli = meow(`
|
30 | Usage
|
31 | conventional-changelog
|
32 |
|
33 | Example
|
34 | conventional-changelog -i CHANGELOG.md --same-file
|
35 |
|
36 | Options
|
37 | -i, --infile Read the CHANGELOG from this file
|
38 |
|
39 | -o, --outfile Write the CHANGELOG to this file
|
40 | If unspecified, it prints to stdout
|
41 |
|
42 | -s, --same-file Outputting to the infile so you don't need to specify the same file as outfile
|
43 |
|
44 | -p, --preset Name of the preset you want to use. Must be one of the following:
|
45 | angular, atom, codemirror, conventionalcommits, ember, eslint, express, jquery or jshint
|
46 |
|
47 | -k, --pkg A filepath of where your package.json is located
|
48 | Default is the closest package.json from cwd
|
49 |
|
50 | -a, --append Should the newer release be appended to the older release
|
51 | Default: false
|
52 |
|
53 | -r, --release-count How many releases to be generated from the latest
|
54 | If 0, the whole changelog will be regenerated and the outfile will be overwritten
|
55 | Default: 1
|
56 |
|
57 | --skip-unstable If given, unstable tags will be skipped, e.g., x.x.x-alpha.1, x.x.x-rc.2
|
58 |
|
59 | -u, --output-unreleased Output unreleased changelog
|
60 |
|
61 | -v, --verbose Verbose output. Use this for debugging
|
62 | Default: false
|
63 |
|
64 | -n, --config A filepath of your config script
|
65 | Example of a config script: https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/conventional-changelog-cli/test/fixtures/config.cjs
|
66 |
|
67 | -c, --context A filepath of a json that is used to define template variables
|
68 | -l, --lerna-package Generate a changelog for a specific lerna package (:pkg-name@1.0.0)
|
69 | -t, --tag-prefix Tag prefix to consider when reading the tags
|
70 | --commit-path Generate a changelog scoped to a specific directory
|
71 | `, {
|
72 | importMeta: import.meta,
|
73 | booleanDefault: undefined,
|
74 | flags: {
|
75 | infile: {
|
76 | shortFlag: 'i',
|
77 | type: 'string'
|
78 | },
|
79 | outfile: {
|
80 | shortFlag: 'o',
|
81 | type: 'string'
|
82 | },
|
83 | sameFile: {
|
84 | shortFlag: 's',
|
85 | type: 'boolean'
|
86 | },
|
87 | preset: {
|
88 | shortFlag: 'p',
|
89 | type: 'string'
|
90 | },
|
91 | pkg: {
|
92 | shortFlag: 'k',
|
93 | type: 'string'
|
94 | },
|
95 | append: {
|
96 | shortFlag: 'a',
|
97 | type: 'boolean'
|
98 | },
|
99 | releaseCount: {
|
100 | shortFlag: 'r',
|
101 | type: 'number'
|
102 | },
|
103 | skipUnstable: {
|
104 | type: 'boolean'
|
105 | },
|
106 | outputUnreleased: {
|
107 | shortFlag: 'u',
|
108 | type: 'boolean'
|
109 | },
|
110 | verbose: {
|
111 | shortFlag: 'v',
|
112 | type: 'boolean'
|
113 | },
|
114 | config: {
|
115 | shortFlag: 'n',
|
116 | type: 'string'
|
117 | },
|
118 | context: {
|
119 | shortFlag: 'c',
|
120 | type: 'string'
|
121 | },
|
122 | lernaPackage: {
|
123 | shortFlag: 'l',
|
124 | type: 'string'
|
125 | },
|
126 | tagPrefix: {
|
127 | shortFlag: 't',
|
128 | type: 'string'
|
129 | }
|
130 | }
|
131 | })
|
132 |
|
133 | let config
|
134 | const flags = cli.flags
|
135 | const infile = flags.infile
|
136 | let outfile = flags.outfile
|
137 | let sameFile = flags.sameFile
|
138 | const append = flags.append
|
139 | const releaseCount = flags.releaseCount
|
140 | const skipUnstable = flags.skipUnstable
|
141 |
|
142 | if (infile && infile === outfile) {
|
143 | sameFile = true
|
144 | } else if (sameFile) {
|
145 | if (infile) {
|
146 | outfile = infile
|
147 | } else {
|
148 | console.error('infile must be provided if same-file flag presents.')
|
149 | process.exit(1)
|
150 | }
|
151 | }
|
152 |
|
153 | let options = {
|
154 | preset: flags.preset,
|
155 | pkg: {
|
156 | path: flags.pkg
|
157 | },
|
158 | append,
|
159 | releaseCount,
|
160 | skipUnstable,
|
161 | outputUnreleased: flags.outputUnreleased,
|
162 | lernaPackage: flags.lernaPackage,
|
163 | tagPrefix: flags.tagPrefix
|
164 | }
|
165 |
|
166 | if (flags.verbose) {
|
167 | options.debug = console.info.bind(console)
|
168 | options.warn = console.warn.bind(console)
|
169 | }
|
170 |
|
171 | let templateContext
|
172 |
|
173 | let outStream
|
174 |
|
175 | try {
|
176 | if (flags.context) {
|
177 | templateContext = await loadDataFile(flags.context)
|
178 | }
|
179 |
|
180 | if (flags.config) {
|
181 | config = await loadDataFile(flags.config)
|
182 | options.config = config
|
183 |
|
184 | if (config.options) {
|
185 | options = {
|
186 | ...options,
|
187 | ...config.options,
|
188 | pkg: {
|
189 | ...options.pkg,
|
190 | ...config.options.pkg
|
191 | }
|
192 | }
|
193 | }
|
194 | } else {
|
195 | config = {}
|
196 | }
|
197 | } catch (err) {
|
198 | console.error('Failed to get file. ' + err)
|
199 | process.exit(1)
|
200 | }
|
201 |
|
202 | const gitRawCommitsOpts = {
|
203 | ...config.gitRawCommitsOpts
|
204 | }
|
205 | if (flags.commitPath) gitRawCommitsOpts.path = flags.commitPath
|
206 |
|
207 | const changelogStream = conventionalChangelog(options, templateContext, gitRawCommitsOpts, config.parserOpts, config.writerOpts)
|
208 | .on('error', (err) => {
|
209 | if (flags.verbose) {
|
210 | console.error(err.stack)
|
211 | } else {
|
212 | console.error(err.toString())
|
213 | }
|
214 | process.exit(1)
|
215 | })
|
216 |
|
217 | function noInputFile () {
|
218 | if (outfile) {
|
219 | outStream = createWriteStream(outfile)
|
220 | } else {
|
221 | outStream = process.stdout
|
222 | }
|
223 |
|
224 | changelogStream
|
225 | .pipe(outStream)
|
226 | }
|
227 |
|
228 | if (infile && releaseCount !== 0) {
|
229 | const readStream = createReadStream(infile)
|
230 | .on('error', () => {
|
231 | if (flags.verbose) {
|
232 | console.warn('infile does not exist.')
|
233 | }
|
234 |
|
235 | if (sameFile) {
|
236 | noInputFile()
|
237 | }
|
238 | })
|
239 |
|
240 | if (sameFile) {
|
241 | if (options.append) {
|
242 | changelogStream
|
243 | .pipe(createWriteStream(outfile, {
|
244 | flags: 'a'
|
245 | }))
|
246 | } else {
|
247 | const tmp = tempfile()
|
248 |
|
249 | changelogStream
|
250 | .pipe(addStream(readStream))
|
251 | .pipe(createWriteStream(tmp))
|
252 | .on('finish', () => {
|
253 | createReadStream(tmp)
|
254 | .pipe(createWriteStream(outfile))
|
255 | })
|
256 | }
|
257 | } else {
|
258 | if (outfile) {
|
259 | outStream = createWriteStream(outfile)
|
260 | } else {
|
261 | outStream = process.stdout
|
262 | }
|
263 |
|
264 | let stream
|
265 |
|
266 | if (options.append) {
|
267 | stream = readStream
|
268 | .pipe(addStream(changelogStream))
|
269 | } else {
|
270 | stream = changelogStream
|
271 | .pipe(addStream(readStream))
|
272 | }
|
273 |
|
274 | stream
|
275 | .pipe(outStream)
|
276 | }
|
277 | } else {
|
278 | noInputFile()
|
279 | }
|