changelog-tool 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
package/changelog.md CHANGED
@@ -1,6 +1,18 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ 0.4.0 (2023-02-12)
5
+ ------------------
6
+
7
+ * Implemented the "format", "parse" and "release" commands.
8
+
9
+
10
+ 0.3.0 (2023-02-12)
11
+ ------------------
12
+
13
+ * Implemented the 'show' command.
14
+
15
+
4
16
  0.2.0 (2023-02-12)
5
17
  ------------------
6
18
 
package/changelog.mjs CHANGED
@@ -16,8 +16,7 @@ export class Changelog {
16
16
  this.title + '\n' +
17
17
  ('='.repeat(this.title.length)) + '\n' +
18
18
  '\n' +
19
- this.versions.map(version => version.toString()).join('\n\n') +
20
- '\n'
19
+ this.versions.map(version => version.toString()).join('\n\n')
21
20
  );
22
21
 
23
22
  }
@@ -26,17 +25,59 @@ export class Changelog {
26
25
  * Adds a new Version log
27
26
  *
28
27
  * @param version {VersionLog}
28
+ * @returns {VersionLog}
29
29
  */
30
30
  add(version) {
31
31
 
32
- this.versions.push(version);
32
+ this.versions = [version, ...this.versions];
33
+ return version;
34
+
35
+ }
36
+
37
+ /**
38
+ * Adds a new version to the log. Version string is automatically increased
39
+ * from the previous one
40
+ *
41
+ * @returns {VersionLog}
42
+ */
43
+ newVersion() {
44
+
45
+ const lastVersionStr = this.versions[0].version;
46
+ const lastVersionParts = lastVersionStr.split('.');
47
+ if (!lastVersionParts.at(-1)?.match(/^[0-9]+$/)) {
48
+ throw new Error(`Could not automatically determine the next version string after "${lastVersionStr}"`);
49
+ }
50
+ const newVersionStr = [
51
+ ...lastVersionParts.slice(0, -1),
52
+ // @ts-ignore-error 'Possibly udefined', but we know it isnt
53
+ parseInt(lastVersionParts.at(-1)) + 1
54
+ ].join('.');
55
+ const versionLog = new VersionLog(newVersionStr);
56
+
57
+ return this.add(versionLog);
58
+
59
+ }
60
+
61
+ /**
62
+ * Finds a VersionLog by its version string
63
+ *
64
+ * @param {string} version
65
+ * @returns {VersionLog}
66
+ */
67
+ get(version) {
68
+
69
+ const log = this.versions.find( myLog => myLog.version === version);
70
+ if (!log) {
71
+ throw new Error(`Couldn't find version ${version} in the changelog`);
72
+ }
73
+ return log;
33
74
 
34
75
  }
35
76
 
36
77
  }
37
78
 
38
79
  export class VersionLog {
39
-
80
+
40
81
  /**
41
82
  * @type {string}
42
83
  */
@@ -72,9 +113,12 @@ export class VersionLog {
72
113
 
73
114
  /**
74
115
  * @param {string} message
116
+ * @returns {LogItem}
75
117
  */
76
118
  add(message) {
77
- this.items.push(new LogItem(message));
119
+ const item = new LogItem(message);
120
+ this.items.push(item);
121
+ return item;
78
122
  }
79
123
 
80
124
  toString() {
@@ -83,11 +127,11 @@ export class VersionLog {
83
127
  return (
84
128
  title + '\n' +
85
129
  ('-'.repeat(title.length)) + '\n' +
86
- (this.preface ? wrap(this.preface) + '\n' : '') +
130
+ (this.preface ? '\n' + wrap(this.preface) : '') +
87
131
  '\n' +
88
132
  this.items.map(version => version.toString()).join('\n') +
89
133
  '\n' +
90
- (this.postface ? wrap(this.postface) + '\n' : '')
134
+ (this.postface ? '\n' + wrap(this.postface) + '\n' : '')
91
135
  );
92
136
 
93
137
  }
package/cli.mjs ADDED
@@ -0,0 +1,213 @@
1
+ // @ts-check
2
+ import { parseArgs } from 'node:util';
3
+ import * as fs from 'node:fs/promises';
4
+ import * as url from 'node:url';
5
+ import { readPackageVersion, exists } from './util.mjs';
6
+ import { Changelog, VersionLog, LogItem } from './changelog.mjs';
7
+ import { parseFile } from './parse.mjs';
8
+
9
+ const filename = 'changelog.md';
10
+
11
+ const pkg = JSON.parse(
12
+ await fs.readFile(
13
+ url.fileURLToPath(url.resolve(import.meta.url, './package.json')),
14
+ 'utf-8',
15
+ )
16
+ );
17
+
18
+ async function main() {
19
+
20
+ const { positionals, values } = parseArgs({
21
+ options: {
22
+ help: {
23
+ type: 'boolean',
24
+ short: 'h',
25
+ default: false,
26
+ description: 'This help screen',
27
+ },
28
+ all: {
29
+ type: 'boolean',
30
+ default: false,
31
+ description: 'Show all versions',
32
+ },
33
+ },
34
+ allowPositionals: true,
35
+ });
36
+
37
+
38
+
39
+ if (positionals.length < 1 || values.help) {
40
+ help();
41
+ process.exit(1);
42
+ }
43
+
44
+ const command = positionals[0];
45
+
46
+ switch(command) {
47
+ case 'help' :
48
+ await help();
49
+ break;
50
+ case 'init' :
51
+ await init();
52
+ break;
53
+ case 'add' :
54
+ if (positionals.length < 2) {
55
+ throw new Error('The "message" argument must be specified with the "add" command');
56
+ }
57
+ await add(positionals.slice(1).join(' '));
58
+ break;
59
+ case 'release' :
60
+ await release();
61
+ break;
62
+ case 'format' :
63
+ await format();
64
+ break;
65
+ case 'show' :
66
+ await show({ all: !!values.all, version: positionals[1]});
67
+ break;
68
+ case 'list' :
69
+ await list();
70
+ break;
71
+ default:
72
+ process.stderr.write(`Unknown command ${command}\n`);
73
+ process.exit(1);
74
+ break;
75
+ }
76
+
77
+ }
78
+
79
+ try {
80
+ await main();
81
+ } catch (err) {
82
+ process.stderr.write('Error: ' + err.message + '\n');
83
+ process.exit(1);
84
+ }
85
+
86
+ function help() {
87
+ console.log(
88
+ `Changelog Tool v${pkg.version}
89
+
90
+ Manipulate your changelog file
91
+
92
+ Usage:
93
+
94
+ changelog init - Create a new, empty changelog.
95
+ changelog add [message] - Adds a new line to the changelog.
96
+ changelog release - Marks the current changelog as released.
97
+ changelog show - Show the last changelog.
98
+ changelog show [version] - Show the changelog of a specific version.
99
+ changelog list - List all versions in the changelog.
100
+ changelog format - Reformats the changelog in the standard format.
101
+
102
+ The logs this tool uses follows a specific markdown format. Currently it will
103
+ only look for a file named 'changelog.md' in the current directory.
104
+
105
+ To see an example of this format, you can either run 'changelog init' or
106
+ check out the changelog shipped with this project:
107
+
108
+ https://github.com/evert/changelog-tool
109
+ `);
110
+
111
+ }
112
+
113
+ async function init() {
114
+
115
+ if (await exists(filename)) {
116
+ throw new Error(`A file named ${filename} already exists`);
117
+ }
118
+
119
+ const changelog = new Changelog();
120
+ const version = new VersionLog(await readPackageVersion());
121
+ version.add('New project!');
122
+ changelog.versions.push(version);
123
+
124
+ await fs.writeFile(filename, changelog.toString());
125
+ console.log(`${filename} created`);
126
+
127
+ }
128
+
129
+ async function list() {
130
+
131
+ const changelog = await parseChangelog();
132
+
133
+ for(const version of changelog.versions) {
134
+ console.log(version.version);
135
+ }
136
+
137
+ }
138
+
139
+ /**
140
+ * @param {Object} showOptions
141
+ * @param {boolean} showOptions.all Show all versions
142
+ * @param {string?} showOptions.version show a specific version
143
+ */
144
+ async function show({all, version}) {
145
+
146
+ const changelog = await parseChangelog();
147
+
148
+ let toRender;
149
+ if (all) {
150
+ toRender = changelog.versions;
151
+ } else if (version) {
152
+ toRender = [changelog.get(version)];
153
+ } else {
154
+ toRender = [changelog.versions[0]];
155
+ }
156
+
157
+ console.log(
158
+ toRender
159
+ .map( log => log.toString())
160
+ .join('\n\n')
161
+ );
162
+
163
+ }
164
+
165
+ async function format() {
166
+ const changelog = await parseChangelog();
167
+ await fs.writeFile(filename, changelog.toString());
168
+ console.log(`${changelog.versions.length} changelogs saved to ${filename}`);
169
+ }
170
+
171
+ /**
172
+ * @param {string} message
173
+ */
174
+ async function add(message) {
175
+ const changelog = await parseChangelog();
176
+
177
+ let lastVersion = changelog.versions[0];
178
+ if (lastVersion.date) {
179
+ lastVersion = changelog.newVersion();
180
+ console.log('Creating new version: %s', lastVersion.version);
181
+ }
182
+ lastVersion.add(message);
183
+
184
+ await fs.writeFile(filename, changelog.toString());
185
+ console.log(`${changelog.versions.length} changelogs saved to ${filename}`);
186
+ }
187
+
188
+ async function release() {
189
+ const changelog = await parseChangelog();
190
+
191
+ let lastVersion = changelog.versions[0];
192
+ if (lastVersion.date) {
193
+ throw new Error(`Previous version "${lastVersion.version}" already had a release date`);
194
+ }
195
+ lastVersion.date = new Date().toISOString().substr(0,10);
196
+ console.log(`Releasing ${lastVersion.version}`);
197
+ await fs.writeFile(filename, changelog.toString());
198
+ console.log(`${changelog.versions.length} changelogs saved to ${filename}`);
199
+
200
+ }
201
+
202
+ /**
203
+ * @returns {Promise<Changelog>}
204
+ */
205
+ async function parseChangelog() {
206
+
207
+ if (!await exists(filename)) {
208
+ throw new Error(`${filename} not found in current directory`);
209
+ }
210
+
211
+ return await parseFile(filename);
212
+
213
+ }
package/index.mjs CHANGED
@@ -1,131 +1,2 @@
1
- // @ts-check
2
- import { parseArgs } from 'node:util';
3
- import * as fs from 'node:fs/promises';
4
- import * as url from 'node:url';
5
- import { readPackageVersion, exists } from './util.mjs';
6
- import { Changelog, VersionLog, LogItem } from './changelog.mjs';
7
- import { parseFile } from './parse.mjs';
8
-
9
- const filename = 'changelog.md';
10
-
11
- const pkg = JSON.parse(
12
- await fs.readFile(
13
- url.fileURLToPath(url.resolve(import.meta.url, './package.json')),
14
- 'utf-8',
15
- )
16
- );
17
-
18
- async function main() {
19
-
20
- const { positionals, values } = parseArgs({
21
- options: {
22
- help: {
23
- type: 'boolean',
24
- short: 'h',
25
- default: false,
26
- description: 'This help screen',
27
- },
28
- },
29
- allowPositionals: true,
30
- });
31
-
32
-
33
-
34
- if (positionals.length < 1 || values.help) {
35
- help();
36
- process.exit(1);
37
- }
38
-
39
- const command = positionals[0];
40
-
41
- switch(command) {
42
- case 'help' :
43
- await help();
44
- break;
45
- case 'init' :
46
- await init();
47
- break;
48
- /*
49
- case 'add' :
50
- await add();
51
- break;
52
- case 'release' :
53
- await release();
54
- break;
55
- case 'show' :
56
- await show();
57
- break;
58
- */
59
- case 'list' :
60
- await list();
61
- break;
62
- default:
63
- process.stderr.write(`Unknown command ${command}\n`);
64
- process.exit(1);
65
- break;
66
- }
67
-
68
- }
69
-
70
- try {
71
- await main();
72
- } catch (err) {
73
- process.stderr.write('Error: ' + err.message + '\n');
74
- process.exit(1);
75
- }
76
-
77
- function help() {
78
- console.log(
79
- `Changelog Tool v${pkg.version}
80
-
81
- Manipulate your changelog file
82
-
83
- Usage:
84
-
85
- changelog init - Create a new, empty changelog.
86
- changelog add [message] - Adds a new line to the changelog.
87
- changelog release - Marks the current changelog as released.
88
- changelog show - Show the last changelog.
89
- changelog show [version] - Show the changelog of a specific version.
90
- changelog list - List all versions in the changelog.
91
-
92
- The logs this tool uses follows a specific markdown format. Currently it will
93
- only look for a file named 'changelog.md' in the current directory.
94
-
95
- To see an example of this format, you can either run 'changelog init' or
96
- check out the changelog shipped with this project:
97
-
98
- https://github.com/evert/changelog-tool
99
- `);
100
-
101
- }
102
-
103
- async function init() {
104
-
105
- if (await exists(filename)) {
106
- throw new Error(`A file named ${filename} already exists`);
107
- }
108
-
109
- const changelog = new Changelog();
110
- const version = new VersionLog(await readPackageVersion());
111
- version.add('New project!');
112
- changelog.versions.push(version);
113
-
114
- await fs.writeFile(filename, changelog.toString());
115
- console.log(`${filename} created`);
116
-
117
- }
118
-
119
- async function list() {
120
-
121
- if (!await exists(filename)) {
122
- throw new Error(`${filename} not found in current directory`);
123
- }
124
-
125
- const changelog = await parseFile(filename);
126
-
127
- for(const version of changelog.versions) {
128
- console.log(version.version);
129
- }
130
-
131
- }
1
+ export { parse, parseFile } from './parse.mjs';
2
+ export { Changelog, VersionLog, LogItem } from './changelog.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "changelog-tool",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "A CLI tool for manipulating changelogs",
5
5
  "main": "index.mjs",
6
6
  "scripts": {
@@ -15,6 +15,9 @@
15
15
  "engine": {
16
16
  "node": ">16"
17
17
  },
18
+ "bin": {
19
+ "changelog": "./cli.mjs"
20
+ },
18
21
  "devDependencies": {
19
22
  "@types/node": "^18.11.19",
20
23
  "typescript": "^4.9.5"
package/parse.mjs CHANGED
@@ -27,15 +27,37 @@ export function parse(changelogInput) {
27
27
  const changelog = new Changelog();
28
28
  changelog.title = lines[0];
29
29
 
30
+ let lastVersionLog = null;
31
+ let lastBullet = null;
32
+
30
33
  for(let idx=2; idx<lines.length; idx++) {
31
34
 
35
+ const line = lines[idx];
36
+
37
+ if (line.startsWith('* ')) {
38
+ // Found a bullet point
39
+ if (!lastVersionLog) {
40
+ throw new Error(`Parse error: found a bullet point * outside of a level 2 heading on line ${idx+1}`);
41
+ }
42
+ lastBullet = lastVersionLog.add(line.substr(1).trim());
43
+ continue;
44
+ }
45
+ if (line.startsWith(' ')) {
46
+ // Continuation of last line.
47
+ if (!lastBullet) {
48
+ throw new Error(`Parse error: unexpected indented string on line ${line+1}`);
49
+ }
50
+ lastBullet.message += ' ' + line.trim();
51
+ continue;
52
+ }
53
+
54
+ // Look to the next line for ----
32
55
  if (lines[idx+1]?.match(/^-{1,}$/)) {
33
56
  // Found a new Version
34
- const versionTitle = lines[idx];
35
- const matches = versionTitle.match(/^([0-9\.]{3,}(?:-(?:alpha|beta)\.[0-9])?) \(([0-9]{4}-[0-9]{2}-[0-9]{2}|\?\?\?\?-\?\?-\?\?)\)$/);
57
+ const matches = line.match(/^([0-9\.]{3,}(?:-(?:alpha|beta)\.[0-9])?) \(([0-9]{4}-[0-9]{2}-[0-9]{2}|\?\?\?\?-\?\?-\?\?)\)$/);
36
58
 
37
59
  if (!matches) {
38
- throw new Error(`A version title must have the format "1.0.0 (YYYY-MM-DD)" or "1.0.0 (????-??-??)" for unreleased versions. We found: "${lines[idx]}"`);
60
+ throw new Error(`A version title must have the format "1.0.0 (YYYY-MM-DD)" or "1.0.0 (????-??-??)" for unreleased versions. We found: "${line}"`);
39
61
  }
40
62
 
41
63
  const versionLog = new VersionLog(matches[1]);
@@ -44,10 +66,29 @@ export function parse(changelogInput) {
44
66
  } else {
45
67
  versionLog.date = matches[2];
46
68
  }
47
- changelog.add(versionLog);
69
+ changelog.versions.push(versionLog);
70
+ lastVersionLog = versionLog;
71
+ lastBullet = null;
72
+ idx++;
73
+ continue;
48
74
 
49
75
  }
50
76
 
77
+ if (line.trim()==='') {
78
+ continue;
79
+ }
80
+
81
+ if (!lastVersionLog) {
82
+ throw new Error(`Parse error: unexpected string on line ${line+1}`);
83
+ }
84
+ // If we got here, this is either a loose preface or postface line.
85
+ if (lastBullet) {
86
+ lastVersionLog.postface = lastVersionLog.postface ? lastVersionLog.postface + ' ' + line : line;
87
+ } else {
88
+ lastVersionLog.preface = lastVersionLog.preface ? lastVersionLog.preface + ' ' + line : line;
89
+ }
90
+
91
+
51
92
  }
52
93
 
53
94
  return changelog;
package/readme.md ADDED
@@ -0,0 +1,48 @@
1
+ Changelog tool
2
+ ==============
3
+
4
+ This repository contains a simple tool for reading and manipulating changelog
5
+ files.
6
+
7
+ This tool currently expects to work with a file named 'changelog.md' in the
8
+ current working directory. This is a markdown file that looks like this:
9
+
10
+ ```
11
+ 0.4.0 (????-??-??)
12
+ ------------------
13
+
14
+ * Feature A
15
+ * Bugfix 3
16
+
17
+ 0.3.0 (2023-02-08)
18
+ ------------------
19
+
20
+ * First public release!
21
+ ```
22
+
23
+ Questionmarks for the date indicate an unreleased version.
24
+
25
+ Installation
26
+ ------------
27
+
28
+ ```sh
29
+ npm install changelog-tool --save-dev
30
+ ```
31
+
32
+ CLI
33
+ ---
34
+
35
+ To tool can be used programmatically and with the CLI. The CLI has the
36
+ following commands:
37
+
38
+ ```
39
+ npx changelog init - Create a new, empty npx changelog.
40
+ npx changelog add [message] - Adds a new line to the npx changelog.
41
+ npx changelog release - Marks the current npx changelog as released.
42
+ npx changelog show - Show the last npx changelog.
43
+ npx changelog show [version] - Show the npx changelog of a specific version.
44
+ npx changelog list - List all versions in the npx changelog.
45
+ npx changelog format - Reformats the npx changelog in the standard format.
46
+ ```
47
+
48
+ Feature requests and bug reports are welcome.
package/test/parse.mjs CHANGED
@@ -2,7 +2,7 @@ import { test } from 'node:test';
2
2
  import { parse } from '../parse.mjs';
3
3
  import * as assert from 'node:assert';
4
4
 
5
- test('Parsing a basic changelog', async () => {
5
+ test('Parsing changelog metadata', async () => {
6
6
 
7
7
  const input = `Time for a change
8
8
  =========
@@ -31,3 +31,64 @@ test('Parsing a basic changelog', async () => {
31
31
  assert.equal('0.1.0', result.versions[1].version);
32
32
 
33
33
  });
34
+
35
+ test('Parsing changelog entries', async () => {
36
+
37
+ const input = `Time for a change
38
+ =========
39
+
40
+ 0.2.0 (????-??-??)
41
+ ------------------
42
+
43
+ * Implemented the 'list' command.
44
+ * Added testing framework.
45
+
46
+ 0.1.0 (2023-02-08)
47
+ ------------------
48
+
49
+ * Implemented the 'help' and 'init' commands.
50
+ *
51
+ `;
52
+
53
+ const result = await parse(input);
54
+
55
+ const latest = result.get('0.2.0');
56
+ assert.equal(2, latest.items.length);
57
+ assert.equal('Implemented the \'list\' command.', latest.items[0].message);
58
+
59
+
60
+ });
61
+
62
+ test('Preface and postface', async () => {
63
+
64
+ const input = `Time for a change
65
+ =========
66
+
67
+ 0.2.0 (????-??-??)
68
+ ------------------
69
+
70
+ WOW another release. How good is that?
71
+ Here's another line.
72
+
73
+ * Implemented the 'list' command.
74
+ * Added testing framework.
75
+
76
+ Well, that's all folks.
77
+
78
+ 0.1.0 (2023-02-08)
79
+ ------------------
80
+
81
+ * Implemented the 'help' and 'init' commands.
82
+ *
83
+ `;
84
+
85
+ debugger;
86
+ const result = await parse(input);
87
+
88
+ const latest = result.get('0.2.0');
89
+
90
+ assert.equal('WOW another release. How good is that? Here\'s another line.', latest.preface);
91
+ assert.equal('Well, that\'s all folks.', latest.postface);
92
+
93
+
94
+ });
package/util.mjs CHANGED
@@ -49,7 +49,7 @@ export async function readPackageVersion() {
49
49
  * @param {number} secondLineOffset
50
50
  * @param {number} lineLength
51
51
  */
52
- export function wrap(input, secondLineOffset = 0, lineLength = 80) {
52
+ export function wrap(input, secondLineOffset = 0, lineLength = 79) {
53
53
 
54
54
  const words = input.split(' ');
55
55
  const lines = [];