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.
- package/.github/workflows/node.yml +45 -0
- package/changelog.md +14 -0
- package/changelog.mjs +119 -0
- package/index.mjs +131 -0
- package/package.json +22 -0
- package/parse.mjs +56 -0
- package/test/parse.mjs +33 -0
- package/tsconfig.json +17 -0
- package/util.mjs +76 -0
@@ -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
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
|
+
}
|