@release-it/keep-a-changelog 5.0.0 → 7.0.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,21 @@
1
+ name: Publish through pkr.pr.new
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - '**'
7
+ tags:
8
+ - '!**'
9
+ pull_request:
10
+ workflow_dispatch:
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: actions/setup-node@v4
19
+ with:
20
+ node-version: 20
21
+ - run: npx --yes pkg-pr-new publish --compact --comment=update
@@ -13,6 +13,7 @@ jobs:
13
13
  node:
14
14
  - 18
15
15
  - 20
16
+ - 22
16
17
 
17
18
  runs-on: ${{ matrix.os }}
18
19
  name: ${{ matrix.os }} (Node v${{ matrix.node }})
package/README.md CHANGED
@@ -31,7 +31,6 @@ In [release-it](https://github.com/release-it/release-it) config:
31
31
  | option | default value | description |
32
32
  | ----------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
33
33
  | filename | `'CHANGELOG.md'` | File with changelogs. |
34
- | strictLatest | `true` | Entry of latest version must be present in order to get correct changelog. Set this option to `false` if you expect latest version without logs. |
35
34
  | addUnreleased | `false` | It leaves "Unreleased" title row if set to `true`. |
36
35
  | keepUnreleased | `false` | It leaves "Unreleased" title row unchanged if set to `true`. |
37
36
  | addVersionUrl | `false` | Links the version to the according changeset. Uses GitHub-compatible URLs by default, see other options to configure the URL format. |
package/index.js CHANGED
@@ -3,6 +3,7 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { detectNewline } from 'detect-newline';
5
5
  import format from 'string-template';
6
+ import semver from "semver";
6
7
 
7
8
  const pad = num => ('0' + num).slice(-2);
8
9
 
@@ -28,10 +29,9 @@ const defaultVersionUrlFormats = {
28
29
  class KeepAChangelog extends Plugin {
29
30
  async init() {
30
31
  await super.init();
31
- const { filename, strictLatest, addUnreleased, keepUnreleased, addVersionUrl, versionUrlFormats, head } = this.options;
32
+ const { filename, addUnreleased, keepUnreleased, addVersionUrl, versionUrlFormats, head } = this.options;
32
33
 
33
34
  this.filename = filename || 'CHANGELOG.md';
34
- this.strictLatest = strictLatest === undefined ? true : Boolean(strictLatest);
35
35
  this.addUnreleased = addUnreleased === undefined ? false : Boolean(addUnreleased);
36
36
  this.keepUnreleased = keepUnreleased === undefined ? false : Boolean(keepUnreleased);
37
37
  this.addVersionUrl = addVersionUrl === undefined ? false : Boolean(addVersionUrl);
@@ -56,15 +56,41 @@ class KeepAChangelog extends Plugin {
56
56
  const { changelog } = this.getContext();
57
57
  if (changelog) return changelog;
58
58
 
59
- const { filename, strictLatest } = this;
60
- const previousReleaseTitle = strictLatest ? `## [${latestVersion}]` : `## [`;
61
- const hasPreviousReleaseSection = this.changelogContent.includes(previousReleaseTitle);
62
- if (strictLatest && !hasPreviousReleaseSection) {
63
- throw Error(`Missing section for previous release ("${latestVersion}") in ${filename}.`);
59
+ const unreleasedTitleRawList = Array.from(this.changelogContent.matchAll(/(?<=## \[)Unreleased(?=])/g)).map(ver => ver?.[0])
60
+ if(unreleasedTitleRawList.length > 1) {
61
+ throw new Error(`Too many "Unreleased" sections in ${this.filename}: ${unreleasedTitleRawList.length}.`)
62
+ }
63
+ const versionTitleRawList = Array.from(this.changelogContent.matchAll(/(?<=## \[)((\d+\.){2}\d+[^\]]*)/g)).map(ver => ver?.[0])
64
+ const badTitleRawList = versionTitleRawList.filter(ver => {
65
+ // invalid: ver > latestVersion
66
+ return !semver.valid(ver) || semver.gt(ver, latestVersion);
67
+ })
68
+ if(badTitleRawList.length > 0) {
69
+ throw new Error(`Invalid versions in ${this.filename}: ${badTitleRawList.join(', ')}. Current: ${latestVersion}.`)
70
+ }
71
+ const unorderedTitleRawList = versionTitleRawList.filter((ver, veri) => {
72
+ const previous = versionTitleRawList[veri - 1];
73
+ const next = versionTitleRawList[veri + 1];
74
+
75
+ if(previous) {
76
+ // bad order: ver > previous
77
+ return semver.gt(ver, previous)
78
+ }
79
+
80
+ if(next) {
81
+ // bad order: ver < next
82
+ return semver.lt(ver, next)
83
+ }
84
+
85
+ return false
86
+ })
87
+ if(unorderedTitleRawList.length > 0) {
88
+ throw new Error(`Invalid sections order in ${this.filename}: ${unorderedTitleRawList.join(', ')}.`)
64
89
  }
65
90
 
66
91
  const { isIncrement } = this.config;
67
- const titleToFind = isIncrement ? this.unreleasedTitleRaw : latestVersion;
92
+ // use unreleased if initial release
93
+ const titleToFind = isIncrement || versionTitleRawList.length === 0 ? this.unreleasedTitleRaw : latestVersion;
68
94
  const changelogContent = this.getChangelogEntryContent(titleToFind);
69
95
 
70
96
  this.setContext({ changelog: changelogContent });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@release-it/keep-a-changelog",
3
- "version": "5.0.0",
3
+ "version": "7.0.0",
4
4
  "description": "Keep-a-changelog plugin for release-it",
5
5
  "type": "module",
6
6
  "exports": "./index.js",
@@ -31,19 +31,21 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "detect-newline": "^4.0.1",
34
+ "semver": "^7.7.1",
34
35
  "string-template": "^1.0.0"
35
36
  },
36
37
  "devDependencies": {
37
38
  "bron": "^2.0.3",
38
- "mock-fs": "^5.1.4",
39
- "release-it": "^17.0.0",
40
- "sinon": "^17.0.1"
39
+ "fs-monkey": "^1.0.6",
40
+ "memfs": "^4.17.0",
41
+ "release-it": "^19.0.0",
42
+ "sinon": "^20.0.0"
41
43
  },
42
44
  "peerDependencies": {
43
- "release-it": "^17.0.0"
45
+ "release-it": "^18.0.0 || ^19.0.0"
44
46
  },
45
47
  "engines": {
46
- "node": ">=18"
48
+ "node": "^20.9.0 || >=22.0.0"
47
49
  },
48
50
  "release-it": {
49
51
  "github": {
package/test.js CHANGED
@@ -1,7 +1,8 @@
1
- import { strict as assert } from 'assert';
1
+ import { strict as assert } from 'node:assert';
2
2
  import test from 'bron';
3
- import fs from 'fs';
4
- import mock from 'mock-fs';
3
+ import fs from 'node:fs';
4
+ import { vol } from 'memfs';
5
+ import { patchFs } from 'fs-monkey';
5
6
  import { factory, runTasks } from 'release-it/test/util/index.js';
6
7
  import sinon from 'sinon';
7
8
  import Plugin from './index.js';
@@ -9,13 +10,14 @@ import Plugin from './index.js';
9
10
  const initialDryRunFileContents =
10
11
  '\n\n## [Unreleased]\n\n* Item A\n* Item B\n\n## [1.0.0] - 2020-05-02\n\n* Item C\n* Item D';
11
12
 
12
- mock({
13
+ vol.fromJSON({
13
14
  './CHANGELOG-FOO.md': '## [FOO]\n\n* Item A\n* Item B',
14
- './CHANGELOG-MISSING.md': '## [Unreleased]\n\n* Item A\n* Item B',
15
+ './CHANGELOG-MULTIPLE_UNRELEASED.md': '## [Unreleased]\n* Item A\n\n## [Unreleased]* Item B\nB\n* Item C\n* Item D',
16
+ './CHANGELOG-INVALID_ORDER.md':
17
+ '## [Unreleased]\n\n## [0.2.0]\n* Item A\n\n## [0.3.0]* Item B\nB\n\n## [0.1.0]\n* Item C\n* Item D',
15
18
  './CHANGELOG-EMPTY.md': '## [Unreleased]\n\n\n\n## [1.0.0]\n\n* Item A\n* Item B',
16
19
  './CHANGELOG-FULL.md':
17
20
  '# Changelog\n\n## [Unreleased]\n\n* Item A\n* Item B\n\n## [1.0.0] - 2020-05-02\n\n* Item C\n* Item D',
18
- './CHANGELOG-NO-STRICT.md': '## [Unreleased]\n\n* Item A\n* Item B\n\n## [1.0.0] - 2020-05-02\n\n* Item C\n* Item D',
19
21
  './CHANGELOG-DRYRUN.md': initialDryRunFileContents,
20
22
  './CHANGELOG-LESS_NEW_LINES.md': '## [Unreleased]\n* Item A\n* Item B\n## [1.0.0] - 2020-05-02\n* Item C\n* Item D',
21
23
  './CHANGELOG-EOL.md':
@@ -34,70 +36,71 @@ mock({
34
36
  './CHANGELOG-VERSION_URL_NEW.md': '## [Unreleased]\n\n* Item A\n* Item B\n'
35
37
  });
36
38
 
39
+ patchFs(vol);
40
+
37
41
  const readFile = file => fs.readFileSync(file).toString();
38
42
 
39
43
  const namespace = 'keep-a-changelog';
40
44
 
41
45
  test('should throw for missing changelog file', async t => {
42
46
  const options = { [namespace]: {} };
43
- const plugin = factory(Plugin, { namespace, options });
47
+ const plugin = await factory(Plugin, { namespace, options });
44
48
  await assert.rejects(runTasks(plugin), /ENOENT: no such file or directory/);
45
49
  });
46
50
 
47
51
  test('should throw for missing "unreleased" section', async t => {
48
52
  const options = { [namespace]: { filename: 'CHANGELOG-FOO.md' } };
49
- const plugin = factory(Plugin, { namespace, options });
53
+ const plugin = await factory(Plugin, { namespace, options });
50
54
  await assert.rejects(runTasks(plugin), /Missing "Unreleased" section in CHANGELOG-FOO.md/);
51
55
  });
52
56
 
53
57
  test('should throw for empty "unreleased" section', async t => {
54
58
  const options = { [namespace]: { filename: 'CHANGELOG-EMPTY.md' } };
55
- const plugin = factory(Plugin, { namespace, options });
59
+ const plugin = await factory(Plugin, { namespace, options });
56
60
  await assert.rejects(runTasks(plugin), /There are no entries under "Unreleased" section in CHANGELOG-EMPTY\.md/);
57
61
  });
58
62
 
59
63
  test('should throw for missing "unreleased" section when no-increment flag is set if changelog is misformatted', async t => {
60
64
  const options = { increment: false, [namespace]: { filename: 'CHANGELOG-FOO.md' } };
61
- const plugin = factory(Plugin, { namespace, options });
65
+ const plugin = await factory(Plugin, { namespace, options });
62
66
  await assert.rejects(runTasks(plugin), /Missing "Unreleased" section in CHANGELOG-FOO.md/);
63
67
  });
64
68
 
65
- test('should throw for missing "1.0.0" section when no-increment flag is set', async t => {
66
- const options = { increment: false, [namespace]: { filename: 'CHANGELOG-MISSING.md' } };
67
- const plugin = factory(Plugin, { namespace, options });
68
- await assert.rejects(runTasks(plugin), /Missing section for previous release \("1\.0\.0"\) in CHANGELOG-MISSING\.md/);
69
+ test('should throw for multiple "unreleased" sections', async t => {
70
+ const options = { [namespace]: { filename: 'CHANGELOG-MULTIPLE_UNRELEASED.md' } };
71
+ const plugin = await factory(Plugin, { namespace, options });
72
+ await assert.rejects(runTasks(plugin), /Too many "Unreleased" sections in CHANGELOG-MULTIPLE_UNRELEASED.md: \d+./);
73
+ });
74
+ test('should throw for invalid order', async t => {
75
+ const options = { [namespace]: { filename: 'CHANGELOG-INVALID_ORDER.md' } };
76
+ const plugin = await factory(Plugin, { namespace, options });
77
+ await assert.rejects(runTasks(plugin), /Invalid sections order in CHANGELOG-INVALID_ORDER.md: 0.2.0, 0.3.0./);
69
78
  });
70
79
 
71
80
  test('should find "1.0.0" section when no-increment flag is set when items under version', async t => {
72
81
  const options = { increment: false, [namespace]: { filename: 'CHANGELOG-EMPTY.md' } };
73
- const plugin = factory(Plugin, { namespace, options });
82
+ const plugin = await factory(Plugin, { namespace, options });
74
83
  await runTasks(plugin);
75
84
  assert.equal(plugin.getChangelog('1.0.0'), '* Item A\n* Item B');
76
85
  });
77
86
 
78
87
  test('should find "1.0.0" section when no-increment flag is set when items under unreleased and version', async t => {
79
88
  const options = { increment: false, [namespace]: { filename: 'CHANGELOG-FULL.md' } };
80
- const plugin = factory(Plugin, { namespace, options });
89
+ const plugin = await factory(Plugin, { namespace, options });
81
90
  await runTasks(plugin);
82
91
  assert.equal(plugin.getChangelog('1.0.0'), '* Item C\n* Item D');
83
92
  });
84
93
 
85
- test('should throw for missing section for previous release', async t => {
86
- const options = { [namespace]: { filename: 'CHANGELOG-MISSING.md' } };
87
- const plugin = factory(Plugin, { namespace, options });
88
- await assert.rejects(runTasks(plugin), /Missing section for previous release \("1\.0\.0"\) in CHANGELOG-MISSING\.md/);
89
- });
90
-
91
94
  test('should find very first changelog with disabled strict latest option', async t => {
92
95
  const options = { [namespace]: { filename: 'CHANGELOG-UNRELEASED.md', strictLatest: false } };
93
- const plugin = factory(Plugin, { namespace, options });
96
+ const plugin = await factory(Plugin, { namespace, options });
94
97
  await runTasks(plugin);
95
98
  assert.equal(plugin.getChangelog(), '* Item A\n* Item B');
96
99
  });
97
100
 
98
101
  test('should write changelog', async t => {
99
102
  const options = { [namespace]: { filename: 'CHANGELOG-FULL.md' } };
100
- const plugin = factory(Plugin, { namespace, options });
103
+ const plugin = await factory(Plugin, { namespace, options });
101
104
  await runTasks(plugin);
102
105
  assert.equal(plugin.getChangelog(), '* Item A\n* Item B');
103
106
  assert.match(
@@ -106,41 +109,30 @@ test('should write changelog', async t => {
106
109
  );
107
110
  });
108
111
 
109
- test('should write changelog even with `strictLatest: false`', async t => {
110
- const options = { [namespace]: { filename: 'CHANGELOG-NO-STRICT.md', strictLatest: false } };
111
- const plugin = factory(Plugin, { namespace, options });
112
- await runTasks(plugin);
113
- assert.equal(plugin.getChangelog(), '* Item A\n* Item B');
114
- assert.match(
115
- readFile('./CHANGELOG-NO-STRICT.md'),
116
- /## \[1\.0\.1] - [0-9]{4}-[0-9]{2}-[0-9]{2}\n\n\* Item A\n\* Item B\n\n## \[1\.0\.0] - 2020-05-02\n\n\* Item C\n*\* Item D/
117
- );
118
- });
119
-
120
112
  test('should not write changelog in dry run', async t => {
121
113
  const options = { 'dry-run': true, [namespace]: { filename: 'CHANGELOG-DRYRUN.md' } };
122
- const plugin = factory(Plugin, { namespace, options });
114
+ const plugin = await factory(Plugin, { namespace, options });
123
115
  await runTasks(plugin);
124
116
  assert.equal(readFile('./CHANGELOG-DRYRUN.md'), initialDryRunFileContents);
125
117
  });
126
118
 
127
119
  test('should not write changelog with keep unreleased option', async t => {
128
120
  const options = { [namespace]: { filename: 'CHANGELOG-DRYRUN.md', keepUnreleased: true } };
129
- const plugin = factory(Plugin, { namespace, options });
121
+ const plugin = await factory(Plugin, { namespace, options });
130
122
  await runTasks(plugin);
131
123
  assert.equal(readFile('./CHANGELOG-DRYRUN.md'), initialDryRunFileContents);
132
124
  });
133
125
 
134
126
  test('should find changelog even if less new lines is used', async t => {
135
127
  const options = { [namespace]: { filename: 'CHANGELOG-LESS_NEW_LINES.md' } };
136
- const plugin = factory(Plugin, { namespace, options });
128
+ const plugin = await factory(Plugin, { namespace, options });
137
129
  await runTasks(plugin);
138
130
  assert.equal(plugin.getChangelog(), '* Item A\n* Item B');
139
131
  });
140
132
 
141
133
  test('should write changelog with different EOL', async t => {
142
134
  const options = { [namespace]: { filename: 'CHANGELOG-EOL.md' } };
143
- const plugin = factory(Plugin, { namespace, options });
135
+ const plugin = await factory(Plugin, { namespace, options });
144
136
  await runTasks(plugin);
145
137
  assert.equal(plugin.getChangelog(), '* Item A\r\n* Item B');
146
138
  assert.match(
@@ -151,7 +143,7 @@ test('should write changelog with different EOL', async t => {
151
143
 
152
144
  test('should write changelog and add unreleased section', async t => {
153
145
  const options = { [namespace]: { filename: 'CHANGELOG-UNRELEASED.md', addUnreleased: true } };
154
- const plugin = factory(Plugin, { namespace, options });
146
+ const plugin = await factory(Plugin, { namespace, options });
155
147
  await runTasks(plugin);
156
148
  assert.equal(plugin.getChangelog(), '* Item A\n* Item B');
157
149
  assert.match(
@@ -162,7 +154,7 @@ test('should write changelog and add unreleased section', async t => {
162
154
 
163
155
  test('should add links to the end of the file', async t => {
164
156
  const options = { [namespace]: { filename: 'CHANGELOG-VERSION_URL.md', addVersionUrl: true } };
165
- const plugin = factory(Plugin, { namespace, options });
157
+ const plugin = await factory(Plugin, { namespace, options });
166
158
  plugin.config.setContext({
167
159
  latestTag: '1.0.0',
168
160
  repo: {
@@ -190,7 +182,7 @@ test('should add links with custom URL formats to the end of the file', async t
190
182
  }
191
183
  }
192
184
  };
193
- const plugin = factory(Plugin, { namespace, options });
185
+ const plugin = await factory(Plugin, { namespace, options });
194
186
  plugin.config.setContext({
195
187
  latestTag: '1.0.0',
196
188
  repo: {
@@ -208,7 +200,7 @@ test('should add links with custom URL formats to the end of the file', async t
208
200
 
209
201
  test('should add links with custom head to the end of the file', async t => {
210
202
  const options = { [namespace]: { filename: 'CHANGELOG-VERSION_URL_HEAD.md', addVersionUrl: true, head: 'main' } };
211
- const plugin = factory(Plugin, { namespace, options });
203
+ const plugin = await factory(Plugin, { namespace, options });
212
204
  plugin.config.setContext({
213
205
  latestTag: '1.0.0',
214
206
  repo: {
@@ -228,7 +220,7 @@ test('should add unreleased section and links to the end of the file', async t =
228
220
  const options = {
229
221
  [namespace]: { filename: 'CHANGELOG-VERSION_URL_UNRELEASED.md', addVersionUrl: true, addUnreleased: true }
230
222
  };
231
- const plugin = factory(Plugin, { namespace, options });
223
+ const plugin = await factory(Plugin, { namespace, options });
232
224
  plugin.config.setContext({
233
225
  latestTag: '1.0.0',
234
226
  repo: {
@@ -248,7 +240,7 @@ test('should match an existing unreleased link in title case', async t => {
248
240
  const options = {
249
241
  [namespace]: { filename: 'CHANGELOG-VERSION_URL_UNRELEASED_TITLE_CASE.md', addVersionUrl: true, addUnreleased: true }
250
242
  };
251
- const plugin = factory(Plugin, { namespace, options });
243
+ const plugin = await factory(Plugin, { namespace, options });
252
244
  plugin.config.setContext({
253
245
  latestTag: '1.0.0',
254
246
  repo: {
@@ -268,7 +260,7 @@ test('should add links to the end of a new changelog', async t => {
268
260
  const options = {
269
261
  [namespace]: { filename: 'CHANGELOG-VERSION_URL_NEW.md', addVersionUrl: true, strictLatest: false }
270
262
  };
271
- const plugin = factory(Plugin, { namespace, options });
263
+ const plugin = await factory(Plugin, { namespace, options });
272
264
  sinon.stub(plugin, 'getLatestVersion').returns('0.0.0');
273
265
  plugin.config.setContext({
274
266
  latestTag: undefined,