changelog-tool 0.2.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/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 = [];