changelog-tool 0.2.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.
@@ -0,0 +1,45 @@
1
+ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2
+ # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3
+
4
+ name: Node.js CI
5
+
6
+ on:
7
+ push:
8
+ branches: [ main ]
9
+ pull_request:
10
+ branches: [ main ]
11
+
12
+ jobs:
13
+ node-test:
14
+ name: Node.js tests
15
+
16
+ runs-on: ubuntu-latest
17
+ timeout-minutes: 5
18
+
19
+ strategy:
20
+ matrix:
21
+ node-version: [16.x, 18.x]
22
+ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
23
+
24
+ steps:
25
+ - uses: actions/checkout@v2
26
+ - name: Use Node.js ${{ matrix.node-version }}
27
+ uses: actions/setup-node@v1
28
+ with:
29
+ node-version: ${{ matrix.node-version }}
30
+ - run: npm ci
31
+ - run: npm test
32
+
33
+ types:
34
+ name: Typescript check
35
+
36
+ runs-on: ubuntu-latest
37
+
38
+ steps:
39
+ - uses: actions/checkout@v2
40
+ - name: Use Node.js
41
+ uses: actions/setup-node@v1
42
+ with:
43
+ node-version: 18.x
44
+ - run: npm ci
45
+ - run: npx tsc
package/changelog.md ADDED
@@ -0,0 +1,14 @@
1
+ Changelog
2
+ =========
3
+
4
+ 0.2.0 (2023-02-12)
5
+ ------------------
6
+
7
+ * Implemented the 'list' command.
8
+ * Added testing framework based on node:test.
9
+
10
+
11
+ 0.1.0 (2023-02-08)
12
+ ------------------
13
+
14
+ * Implemented the 'help' and 'init' commands.
package/changelog.mjs ADDED
@@ -0,0 +1,119 @@
1
+ import { wrap } from './util.mjs';
2
+
3
+ // @ts-check
4
+ export class Changelog {
5
+
6
+ title = 'Changelog';
7
+
8
+ /**
9
+ * @type {VersionLog[]}
10
+ */
11
+ versions = [];
12
+
13
+ toString() {
14
+
15
+ return (
16
+ this.title + '\n' +
17
+ ('='.repeat(this.title.length)) + '\n' +
18
+ '\n' +
19
+ this.versions.map(version => version.toString()).join('\n\n') +
20
+ '\n'
21
+ );
22
+
23
+ }
24
+
25
+ /**
26
+ * Adds a new Version log
27
+ *
28
+ * @param version {VersionLog}
29
+ */
30
+ add(version) {
31
+
32
+ this.versions.push(version);
33
+
34
+ }
35
+
36
+ }
37
+
38
+ export class VersionLog {
39
+
40
+ /**
41
+ * @type {string}
42
+ */
43
+ version;
44
+
45
+ /**
46
+ * @type {string|null}
47
+ */
48
+ date = null;
49
+
50
+ /**
51
+ * @type {string|null}
52
+ */
53
+ preface = null;
54
+
55
+ /**
56
+ * @type {string|null}
57
+ */
58
+ postface = null;
59
+
60
+ /**
61
+ * @type {LogItem[]}
62
+ */
63
+ items = [];
64
+
65
+ /**
66
+ * @param {string} version
67
+ */
68
+ constructor(version) {
69
+ this.version = version;
70
+
71
+ }
72
+
73
+ /**
74
+ * @param {string} message
75
+ */
76
+ add(message) {
77
+ this.items.push(new LogItem(message));
78
+ }
79
+
80
+ toString() {
81
+
82
+ const title = this.version + ' (' + (this.date ?? '????-??-??') + ')';
83
+ return (
84
+ title + '\n' +
85
+ ('-'.repeat(title.length)) + '\n' +
86
+ (this.preface ? wrap(this.preface) + '\n' : '') +
87
+ '\n' +
88
+ this.items.map(version => version.toString()).join('\n') +
89
+ '\n' +
90
+ (this.postface ? wrap(this.postface) + '\n' : '')
91
+ );
92
+
93
+ }
94
+
95
+ }
96
+
97
+ export class LogItem {
98
+
99
+ /**
100
+ * @type string
101
+ */
102
+ message;
103
+
104
+ /**
105
+ * @param {string} message
106
+ */
107
+ constructor(message) {
108
+ this.message = message;
109
+ }
110
+
111
+ toString() {
112
+
113
+ return wrap('* ' + this.message, 2);
114
+
115
+ }
116
+
117
+ }
118
+
119
+
package/index.mjs ADDED
@@ -0,0 +1,131 @@
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
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "changelog-tool",
3
+ "version": "0.2.0",
4
+ "description": "A CLI tool for manipulating changelogs",
5
+ "main": "index.mjs",
6
+ "scripts": {
7
+ "test": "node --test"
8
+ },
9
+ "keywords": [
10
+ "changelog",
11
+ "markdown"
12
+ ],
13
+ "author": "Evert Pot (https://evertpot.com/)",
14
+ "license": "MIT",
15
+ "engine": {
16
+ "node": ">16"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^18.11.19",
20
+ "typescript": "^4.9.5"
21
+ }
22
+ }
package/parse.mjs ADDED
@@ -0,0 +1,56 @@
1
+ // @ts-check
2
+ import { Changelog, VersionLog } from "./changelog.mjs";
3
+ import { readFile } from 'node:fs/promises';
4
+
5
+ /**
6
+ * @param {string} filename
7
+ * @returns {Promise<Changelog>}
8
+ */
9
+ export async function parseFile(filename) {
10
+
11
+ return parse(
12
+ await readFile(filename, 'utf-8')
13
+ );
14
+
15
+ }
16
+
17
+ /**
18
+ * @param {string} changelogInput
19
+ * @returns {Changelog}
20
+ */
21
+ export function parse(changelogInput) {
22
+
23
+ const lines = changelogInput.split('\n');
24
+ if (!lines[1].match(/^={1,}$/)) {
25
+ throw new Error('Parse error: Line 1 and 2 of the changelog must be in the format "Changelog\\n=====". We did not find all equals signs on the second line.');
26
+ }
27
+ const changelog = new Changelog();
28
+ changelog.title = lines[0];
29
+
30
+ for(let idx=2; idx<lines.length; idx++) {
31
+
32
+ if (lines[idx+1]?.match(/^-{1,}$/)) {
33
+ // 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}|\?\?\?\?-\?\?-\?\?)\)$/);
36
+
37
+ 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]}"`);
39
+ }
40
+
41
+ const versionLog = new VersionLog(matches[1]);
42
+ if (matches[2] === '????-??-??') {
43
+ versionLog.date = null;
44
+ } else {
45
+ versionLog.date = matches[2];
46
+ }
47
+ changelog.add(versionLog);
48
+
49
+ }
50
+
51
+ }
52
+
53
+ return changelog;
54
+
55
+
56
+ }
package/test/parse.mjs ADDED
@@ -0,0 +1,33 @@
1
+ import { test } from 'node:test';
2
+ import { parse } from '../parse.mjs';
3
+ import * as assert from 'node:assert';
4
+
5
+ test('Parsing a basic changelog', async () => {
6
+
7
+ const input = `Time for a change
8
+ =========
9
+
10
+ 0.2.0 (????-??-??)
11
+ ------------------
12
+
13
+ * Implemented the 'list' command.
14
+ * Added testing framework.
15
+
16
+ 0.1.0 (2023-02-08)
17
+ ------------------
18
+
19
+ * Implemented the 'help' and 'init' commands.
20
+ *
21
+ `;
22
+
23
+ const result = await parse(input);
24
+
25
+ assert.equal('Time for a change', result.title);
26
+ assert.equal(2, result.versions.length);
27
+
28
+ assert.equal(null, result.versions[0].date);
29
+ assert.equal('0.2.0', result.versions[0].version);
30
+ assert.equal('2023-02-08', result.versions[1].date);
31
+ assert.equal('0.1.0', result.versions[1].version);
32
+
33
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2022",
4
+ "module": "esnext",
5
+ "rootDir": "./",
6
+ "allowJs": true,
7
+ "checkJs": true,
8
+
9
+ "moduleResolution": "node",
10
+ "resolveJsonModule": true,
11
+
12
+ "noEmit": true,
13
+ "strict": true,
14
+ "useUnknownInCatchVariables": false,
15
+
16
+ }
17
+ }
package/util.mjs ADDED
@@ -0,0 +1,76 @@
1
+ // @ts-check
2
+ import * as fs from 'node:fs/promises';
3
+
4
+ /**
5
+ * Checks if a file exists
6
+ *
7
+ * @param {string} filename
8
+ * @returns {Promise<boolean>}
9
+ */
10
+ export async function exists(filename) {
11
+
12
+ try {
13
+ await fs.stat(filename)
14
+ } catch (err) {
15
+ if (err.code === 'ENOENT') return false;
16
+ throw err;
17
+ }
18
+ return true;
19
+
20
+ }
21
+
22
+ /**
23
+ * Returns the version property of the package.json file in the current
24
+ * directory.
25
+ *
26
+ * @returns {Promise<string>}
27
+ */
28
+ export async function readPackageVersion() {
29
+
30
+ if (!await exists('package.json')) {
31
+ throw new Error('package.json does not exists in the current directory');
32
+ }
33
+
34
+ const json = JSON.parse(
35
+ await fs.readFile(
36
+ 'package.json',
37
+ 'utf-8'
38
+ )
39
+ );
40
+
41
+ return json.version;
42
+
43
+ }
44
+
45
+ /**
46
+ * Wraps a line over multiple lines.
47
+ *
48
+ * @param {string} input
49
+ * @param {number} secondLineOffset
50
+ * @param {number} lineLength
51
+ */
52
+ export function wrap(input, secondLineOffset = 0, lineLength = 80) {
53
+
54
+ const words = input.split(' ');
55
+ const lines = [];
56
+ for(const word of words) {
57
+
58
+ if (!lines.length) {
59
+ // First line
60
+ lines.push(word);
61
+ continue;
62
+ }
63
+
64
+ const maxLength = lines.length > 1 ? lineLength - secondLineOffset : lineLength;
65
+
66
+ const potentialNewLine = [lines.at(-1),word].join(' ');
67
+ if (potentialNewLine.length>maxLength) {
68
+ lines.push(word);
69
+ } else {
70
+ lines[lines.length-1] = potentialNewLine;
71
+ }
72
+
73
+ }
74
+ return lines.join('\n' + ' '.repeat(secondLineOffset));
75
+
76
+ }