changelog-tool 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ }