@opentermsarchive/engine 0.37.1 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opentermsarchive/engine",
3
- "version": "0.37.1",
3
+ "version": "1.1.0",
4
4
  "description": "Tracks and makes visible changes to the terms of online services",
5
5
  "homepage": "https://github.com/OpenTermsArchive/engine#readme",
6
6
  "bugs": {
@@ -30,6 +30,8 @@
30
30
  ".eslintrc.yaml"
31
31
  ],
32
32
  "scripts": {
33
+ "changelog": "node scripts/changelog/index.js",
34
+ "commit-messages:lint": "commitlint --from=main --to=HEAD",
33
35
  "dataset:generate": "node bin/ota.js dataset",
34
36
  "dataset:release": "node bin/ota.js dataset --publish --remove-local-copy",
35
37
  "dataset:scheduler": "npm run dataset:release -- --schedule",
@@ -100,10 +102,13 @@
100
102
  "winston-mail": "^2.0.0"
101
103
  },
102
104
  "devDependencies": {
105
+ "@commitlint/cli": "^19.0.3",
103
106
  "dir-compare": "^4.0.0",
107
+ "keep-a-changelog": "^2.5.3",
104
108
  "nock": "^13.2.1",
105
109
  "node-stream-zip": "^1.15.0",
106
110
  "prettier": "^2.2.1",
111
+ "semver": "^7.6.0",
107
112
  "sinon": "^12.0.1",
108
113
  "sinon-chai": "^3.7.0",
109
114
  "supertest": "^6.3.3"
@@ -0,0 +1,95 @@
1
+ import { parser as keepAChangelogParser } from 'keep-a-changelog';
2
+ import semver from 'semver';
3
+
4
+ import ChangelogValidationError from './changelogValidationError.js';
5
+
6
+ export default class Changelog {
7
+ static NO_CODE_CHANGES_REGEX = /^_No code changes were made in this release(.+)_$/m;
8
+ static FUNDER_REGEX = /^> Development of this release was (?:supported|made on a volunteer basis) by (.+)\.$/m;
9
+ static UNRELEASED_REGEX = /## Unreleased[ ]+\[(major|minor|patch)\]/i;
10
+ static CHANGESET_LINK_REGEX = /^_Full changeset and discussions: (.+)._$/m;
11
+ static CHANGESET_LINK_TEMPLATE = PRNumber => `_Full changeset and discussions: [#${PRNumber}](https://github.com/OpenTermsArchive/engine/pull/${PRNumber})._`;
12
+ static CHANGELOG_INTRO = 'All changes that impact users of this module are documented in this file, in the [Common Changelog](https://common-changelog.org) format with some additional specifications defined in the CONTRIBUTING file. This codebase adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).';
13
+
14
+ constructor(rawContent) {
15
+ this.rawContent = rawContent;
16
+ this.changelog = keepAChangelogParser(this.rawContent);
17
+ this.changelog.description = Changelog.CHANGELOG_INTRO;
18
+ this.changelog.format = 'markdownlint';
19
+ this.releaseType = this.extractReleaseType();
20
+ }
21
+
22
+ extractReleaseType() {
23
+ const match = this.rawContent.match(Changelog.UNRELEASED_REGEX);
24
+
25
+ if (match && match[1]) {
26
+ return match[1].toLowerCase();
27
+ }
28
+
29
+ return null;
30
+ }
31
+
32
+ cleanUnreleased() {
33
+ const index = this.changelog.releases.findIndex(release => !release.version);
34
+
35
+ this.changelog.releases.splice(index, 1);
36
+ }
37
+
38
+ getVersionContent(version) {
39
+ const release = this.changelog.findRelease(version);
40
+
41
+ if (!release) {
42
+ throw new Error(`Version ${version} not found in changelog`);
43
+ }
44
+
45
+ return release.toString(this.changelog);
46
+ }
47
+
48
+ release(PRNumber) {
49
+ const unreleased = this.changelog.findRelease();
50
+
51
+ if (!unreleased) {
52
+ throw new Error('Missing "Unreleased" section');
53
+ }
54
+
55
+ const latestVersion = semver.maxSatisfying(this.changelog.releases.map(release => release.version), '*');
56
+ const newVersion = semver.inc(latestVersion, this.releaseType);
57
+
58
+ unreleased.setVersion(newVersion);
59
+ unreleased.date = new Date();
60
+
61
+ if (PRNumber && !Changelog.CHANGESET_LINK_REGEX.test(unreleased.description)) {
62
+ unreleased.description = `${Changelog.CHANGESET_LINK_TEMPLATE(PRNumber)}\n\n${unreleased.description}`;
63
+ }
64
+ }
65
+
66
+ validateUnreleased() {
67
+ const unreleased = this.changelog.findRelease();
68
+ const errors = [];
69
+
70
+ if (!unreleased) {
71
+ errors.push(new Error('Missing "Unreleased" section'));
72
+ throw new ChangelogValidationError(errors);
73
+ }
74
+
75
+ if (!this.releaseType) {
76
+ errors.push(new Error('Invalid or missing release type for "Unreleased" section. Please ensure the section contains a valid release type (major, minor, or patch)'));
77
+ }
78
+
79
+ if (!Changelog.FUNDER_REGEX.test(unreleased.description)) {
80
+ errors.push(new Error('Missing funder in the "Unreleased" section'));
81
+ }
82
+
83
+ if (!Changelog.NO_CODE_CHANGES_REGEX.test(unreleased.description) && (!unreleased.changes || Array.from(unreleased.changes.values()).every(change => !change.length))) {
84
+ errors.push(new Error('Missing or malformed changes in the "Unreleased" section'));
85
+ }
86
+
87
+ if (errors.length) {
88
+ throw new ChangelogValidationError(errors);
89
+ }
90
+ }
91
+
92
+ toString() {
93
+ return this.changelog.toString();
94
+ }
95
+ }
@@ -0,0 +1,144 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ import { expect } from 'chai';
6
+
7
+ import Changelog from './changelog.js';
8
+ import ChangelogValidationError from './changelogValidationError.js';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+
12
+ describe('Changelog', () => {
13
+ let changelog;
14
+
15
+ describe('#releaseType', () => {
16
+ context('with a properly formed changelog', () => {
17
+ it('returns the correct release type', async () => {
18
+ changelog = new Changelog(await fs.readFile(path.resolve(__dirname, './fixtures/changelog.md'), 'UTF-8'));
19
+ expect(changelog.releaseType).to.equal('major');
20
+ });
21
+ });
22
+
23
+ context('when "Unreleased" section does not exist', () => {
24
+ it('returns null', async () => {
25
+ changelog = new Changelog(await fs.readFile(path.resolve(__dirname, './fixtures/changelog-without-unreleased.md'), 'UTF-8'));
26
+ expect(changelog.releaseType).to.be.null;
27
+ });
28
+ });
29
+ context('when "Unreleased" section has a malformed release type', () => {
30
+ it('returns null', async () => {
31
+ changelog = new Changelog(await fs.readFile(path.resolve(__dirname, './fixtures/changelog-with-unreleased-malformed.md'), 'UTF-8'));
32
+ expect(changelog.releaseType).to.be.null;
33
+ });
34
+ });
35
+ });
36
+
37
+ describe('#getVersionContent', () => {
38
+ context('when getting an exisiting version', () => {
39
+ it('returns the content of the specified version', async () => {
40
+ changelog = new Changelog(await fs.readFile(path.resolve(__dirname, './fixtures/changelog.md'), 'UTF-8'));
41
+ const versionContent = changelog.getVersionContent('0.0.1');
42
+
43
+ expect(versionContent).to.equal(`## 0.0.1 - 2024-02-20
44
+
45
+ _Full changeset and discussions: #122._
46
+
47
+ > Development of this release was made on a volunteer basis by a contributor.
48
+
49
+ ### Added
50
+
51
+ - Initial release`);
52
+ });
53
+ });
54
+
55
+ context('when getting a non-existing version', () => {
56
+ it('throws an error', () => {
57
+ expect(() => changelog.getVersionContent('2.0.0')).to.throw(Error);
58
+ });
59
+ });
60
+ });
61
+
62
+ describe('#cleanUnreleased', () => {
63
+ context('when "Unreleased" section exists', () => {
64
+ it('removes the section', async () => {
65
+ changelog = new Changelog(await fs.readFile(path.resolve(__dirname, './fixtures/changelog-with-unreleased-no-release.md'), 'UTF-8'));
66
+ changelog.cleanUnreleased();
67
+ const updatedChangelog = changelog.toString();
68
+
69
+ expect(updatedChangelog).to.not.include('## Unreleased');
70
+ });
71
+ });
72
+
73
+ context('when "Unreleased" section does not exist', () => {
74
+ it('does not throw any error', async () => {
75
+ changelog = new Changelog(await fs.readFile(path.resolve(__dirname, './fixtures/changelog-without-unreleased.md'), 'UTF-8'));
76
+ expect(() => changelog.cleanUnreleased()).to.not.throw();
77
+ });
78
+ });
79
+ });
80
+
81
+ describe('#release', () => {
82
+ context('with a properly formed changelog', () => {
83
+ it('returns an updated version of the changelog', async () => {
84
+ changelog = new Changelog(await fs.readFile(path.resolve(__dirname, './fixtures/changelog.md'), 'UTF-8'));
85
+ changelog.release();
86
+ let expectedResult = await fs.readFile(path.resolve(__dirname, './fixtures/changelog-released.md'), 'UTF-8');
87
+
88
+ expectedResult = expectedResult.replace('<DATE_OF_THE_DAY_PLACEHOLDER>', `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}-${String(new Date().getDate()).padStart(2, '0')}`);
89
+ expect(changelog.toString()).to.equal(expectedResult);
90
+ });
91
+ });
92
+
93
+ context('when there is a validation error on the "Unreleased" section', () => {
94
+ it('throws an error', async () => {
95
+ changelog = new Changelog(await fs.readFile(path.resolve(__dirname, './fixtures/changelog-without-unreleased.md'), 'UTF-8'));
96
+ expect(() => changelog.release(124)).to.throw(Error, 'Missing "Unreleased" section');
97
+ });
98
+ });
99
+ });
100
+
101
+ describe('#validateUnreleased', () => {
102
+ context('with a properly formed "Unreleased" section', () => {
103
+ it('does not throw any error', async () => {
104
+ changelog = new Changelog(await fs.readFile(path.resolve(__dirname, './fixtures/changelog.md'), 'UTF-8'));
105
+ expect(() => changelog.validateUnreleased()).to.not.throw();
106
+ });
107
+ });
108
+
109
+ context('when "Unreleased" section is missing', () => {
110
+ it('throws a ChangelogValidationError error with proper message', async () => {
111
+ changelog = new Changelog(await fs.readFile(path.resolve(__dirname, './fixtures/changelog-without-unreleased.md'), 'UTF-8'));
112
+ expect(() => changelog.validateUnreleased()).to.throw(ChangelogValidationError, 'Missing "Unreleased" section');
113
+ });
114
+ });
115
+
116
+ context('when release type is invalid or missing', () => {
117
+ it('throws a ChangelogValidationError error with proper message', async () => {
118
+ changelog = new Changelog(await fs.readFile(path.resolve(__dirname, './fixtures/changelog-with-unreleased-malformed.md'), 'UTF-8'));
119
+ expect(() => changelog.validateUnreleased()).to.throw(ChangelogValidationError, 'Invalid or missing release type for "Unreleased" section. Please ensure the section contains a valid release type (major, minor, or patch)');
120
+ });
121
+ });
122
+
123
+ context('when funder is missing', () => {
124
+ it('throws a ChangelogValidationError error with proper message', async () => {
125
+ changelog = new Changelog(await fs.readFile(path.resolve(__dirname, './fixtures/changelog-without-funder.md'), 'UTF-8'));
126
+ expect(() => changelog.validateUnreleased()).to.throw(ChangelogValidationError, 'Missing funder in the "Unreleased" section');
127
+ });
128
+ });
129
+
130
+ context('when changes are missing', () => {
131
+ it('throws a ChangelogValidationError error with proper message', async () => {
132
+ changelog = new Changelog(await fs.readFile(path.resolve(__dirname, './fixtures/changelog-without-changes.md'), 'UTF-8'));
133
+ expect(() => changelog.validateUnreleased()).to.throw(ChangelogValidationError, 'Missing or malformed changes in the "Unreleased" section');
134
+ });
135
+ });
136
+
137
+ context('when changes are malformed', () => {
138
+ it('throws a ChangelogValidationError error with proper message', async () => {
139
+ changelog = new Changelog(await fs.readFile(path.resolve(__dirname, './fixtures/changelog-without-changes.md'), 'UTF-8'));
140
+ expect(() => changelog.validateUnreleased()).to.throw(ChangelogValidationError, 'Missing or malformed changes in the "Unreleased" section');
141
+ });
142
+ });
143
+ });
144
+ });
@@ -0,0 +1,9 @@
1
+ export default class ChangelogValidationError extends Error {
2
+ constructor(errorOrErrors) {
3
+ const errors = [].concat(errorOrErrors);
4
+
5
+ super(`Invalid Unreleased section:${`\n - ${errors.join('\n - ')}`}`);
6
+ this.name = 'ChangelogValidationError';
7
+ this.errors = errors;
8
+ }
9
+ }
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All changes that impact users of this module are documented in this file, in the [Common Changelog](https://common-changelog.org) format with some additional specifications defined in the CONTRIBUTING file. This codebase adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## 1.0.0 - <DATE_OF_THE_DAY_PLACEHOLDER>
6
+
7
+ _Full changeset and discussions: #123._
8
+
9
+ > Development of this release was supported by a funder.
10
+
11
+ ### Added
12
+
13
+ - New feature 1
14
+
15
+ ## 0.0.1 - 2024-02-20
16
+
17
+ _Full changeset and discussions: #122._
18
+
19
+ > Development of this release was made on a volunteer basis by a contributor.
20
+
21
+ ### Added
22
+
23
+ - Initial release
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ All changes that impact users of this module are documented in this file, in the [Common Changelog](https://common-changelog.org) format with some additional specifications defined in the CONTRIBUTING file. This codebase adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## Unreleased [major]
6
+
7
+ _Full changeset and discussions: #123._
8
+
9
+ > Development of this release was supported by a funder.
10
+
11
+ - New feature 1
12
+
13
+ ## 0.0.1 - 2024-02-20
14
+
15
+ _Full changeset and discussions: #122._
16
+
17
+ > Development of this release was supported by a funder.
18
+
19
+ ### Added
20
+
21
+ - Initial release
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All changes that impact users of this module are documented in this file, in the [Common Changelog](https://common-changelog.org) format with some additional specifications defined in the CONTRIBUTING file. This codebase adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## Unreleased major
6
+
7
+ _Full changeset and discussions: #123._
8
+
9
+ > Development of this release was supported by a funder.
10
+
11
+ ### Added
12
+
13
+ - New feature 1
14
+
15
+ ## 0.0.1 - 2024-02-20
16
+
17
+ _Full changeset and discussions: #122._
18
+
19
+ > Development of this release was supported by a funder.
20
+
21
+ ### Added
22
+
23
+ - Initial release
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ All changes that impact users of this module are documented in this file, in the [Common Changelog](https://common-changelog.org) format with some additional specifications defined in the CONTRIBUTING file. This codebase adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## Unreleased [no-release]
6
+
7
+ _Modifications made in this changeset do not alter its behavior or functionality from the perspective of the end user or other interacting systems. It does not add new features, do not enhance existing features, or alter the behavior of the software in a way that affects its use._
8
+
9
+ ### Added
10
+
11
+ - New feature 1
12
+
13
+ ## 0.0.1 - 2024-02-20
14
+
15
+ _Full changeset and discussions: #122._
16
+
17
+ > Development of this release was supported by a funder.
18
+
19
+ ### Added
20
+
21
+ - Initial release
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All changes that impact users of this module are documented in this file, in the [Common Changelog](https://common-changelog.org) format with some additional specifications defined in the CONTRIBUTING file. This codebase adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## Unreleased [major]
6
+
7
+ _Full changeset and discussions: #123._
8
+
9
+ > Development of this release was supported by a funder.
10
+
11
+ ## 0.0.1 - 2024-02-20
12
+
13
+ _Full changeset and discussions: #122._
14
+
15
+ > Development of this release was supported by a funder.
16
+
17
+ ### Added
18
+
19
+ - Initial release
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ All changes that impact users of this module are documented in this file, in the [Common Changelog](https://common-changelog.org) format with some additional specifications defined in the CONTRIBUTING file. This codebase adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## Unreleased [major]
6
+
7
+ _Full changeset and discussions: #123._
8
+
9
+ ### Added
10
+
11
+ - New feature 1
12
+
13
+ ## 0.0.1 - 2024-02-20
14
+
15
+ _Full changeset and discussions: #122._
16
+
17
+ > Development of this release was supported by a funder.
18
+
19
+ ### Added
20
+
21
+ - Initial release
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ All changes that impact users of this module are documented in this file, in the [Common Changelog](https://common-changelog.org) format with some additional specifications defined in the CONTRIBUTING file. This codebase adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## 0.0.1 - 2024-02-20
6
+
7
+ _Full changeset and discussions: #122._
8
+
9
+ > Development of this release was supported by a funder.
10
+
11
+ ### Added
12
+
13
+ - Initial release
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All changes that impact users of this module are documented in this file, in the [Common Changelog](https://common-changelog.org) format with some additional specifications defined in the CONTRIBUTING file. This codebase adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## Unreleased [major]
6
+
7
+ _Full changeset and discussions: #123._
8
+
9
+ > Development of this release was supported by a funder.
10
+
11
+ ### Added
12
+
13
+ - New feature 1
14
+
15
+ ## 0.0.1 - 2024-02-20
16
+
17
+ _Full changeset and discussions: #122._
18
+
19
+ > Development of this release was made on a volunteer basis by a contributor.
20
+
21
+ ### Added
22
+
23
+ - Initial release
@@ -0,0 +1,59 @@
1
+ #! /usr/bin/env node
2
+
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ import { program } from 'commander';
8
+
9
+ import Changelog from './changelog.js';
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+
13
+ program
14
+ .name('changelog')
15
+ .description('A command-line utility for managing the changelog file')
16
+ .option('--validate', 'Validate the changelog, ensuring it follows the expected format and contains required information')
17
+ .option('--release [PRNumber]', 'Convert the Unreleased section into a new release in the changelog, optionally linking to a pull request with the provided PRNumber')
18
+ .option('--clean-unreleased', 'Remove the Unreleased section')
19
+ .option('--get-release-type', 'Get the release type of the Unreleased section in the changelog')
20
+ .option('--get-version-content <version>', 'Get the content of the given version in the changelog');
21
+
22
+ const options = program.parse(process.argv).opts();
23
+
24
+ let changelog;
25
+
26
+ const CHANGELOG_PATH = path.resolve(__dirname, '../../CHANGELOG.md');
27
+ const ENCODING = 'UTF-8';
28
+
29
+ try {
30
+ const changelogContent = await fs.readFile(CHANGELOG_PATH, ENCODING);
31
+
32
+ changelog = new Changelog(changelogContent);
33
+ } catch (error) {
34
+ console.log(error.message);
35
+ }
36
+
37
+ if (options.validate) {
38
+ changelog.validateUnreleased();
39
+ }
40
+
41
+ if (options.getReleaseType) {
42
+ process.stdout.write(changelog.releaseType || 'No release type found');
43
+ }
44
+
45
+ if (options.getVersionContent) {
46
+ process.stdout.write(changelog.getVersionContent(options.getVersionContent));
47
+ }
48
+
49
+ if (options.release) {
50
+ const PRNumber = typeof options.release == 'boolean' ? null : options.release;
51
+
52
+ changelog.release(PRNumber);
53
+ await fs.writeFile(CHANGELOG_PATH, changelog.toString(), ENCODING);
54
+ }
55
+
56
+ if (options.cleanUnreleased) {
57
+ changelog.cleanUnreleased();
58
+ await fs.writeFile(CHANGELOG_PATH, changelog.toString(), ENCODING);
59
+ }
@@ -29,6 +29,7 @@ export const EVENTS = [
29
29
  'trackingCompleted',
30
30
  'inaccessibleContent',
31
31
  'error',
32
+ 'pluginError',
32
33
  ];
33
34
 
34
35
  export default class Archivist extends events.EventEmitter {
@@ -72,10 +73,6 @@ export default class Archivist extends events.EventEmitter {
72
73
  initQueue() {
73
74
  this.trackingQueue = async.queue(this.trackTermsChanges.bind(this), MAX_PARALLEL_TRACKING);
74
75
  this.trackingQueue.error(async (error, { terms }) => {
75
- if (error.toString().includes('HttpError: API rate limit exceeded for user ID')) {
76
- return; // This is an error due to SendInBlue quota, bypass
77
- }
78
-
79
76
  if (error instanceof InaccessibleContentError) {
80
77
  this.emit('inaccessibleContent', error, terms);
81
78
 
@@ -91,7 +88,13 @@ export default class Archivist extends events.EventEmitter {
91
88
  const handlerName = `on${event[0].toUpperCase()}${event.substring(1)}`;
92
89
 
93
90
  if (listener[handlerName]) {
94
- this.on(event, listener[handlerName].bind(listener));
91
+ this.on(event, async (...params) => {
92
+ try {
93
+ await listener[handlerName](...params); // Prefer try...catch over .catch() for handling errors to account for both synchronous and asynchronous functions, as .catch() cannot be applied to synchronous functions
94
+ } catch (error) {
95
+ this.emit('pluginError', error, listener.constructor.name);
96
+ }
97
+ });
95
98
  }
96
99
  });
97
100
  }
@@ -245,6 +245,80 @@ describe('Archivist', function () {
245
245
  });
246
246
  });
247
247
 
248
+ describe('Plugin system', () => {
249
+ const plugin = {};
250
+
251
+ describe('#attach', () => {
252
+ before(async () => {
253
+ app = new Archivist({
254
+ recorderConfig: config.get('recorder'),
255
+ fetcherConfig: config.get('fetcher'),
256
+ });
257
+ await app.initialize();
258
+
259
+ EVENTS.forEach(event => {
260
+ const handlerName = `on${event[0].toUpperCase()}${event.substring(1)}`;
261
+
262
+ plugin[handlerName] = sinon.spy();
263
+ });
264
+
265
+ app.removeAllListeners('error');
266
+ expect(app.eventNames()).to.be.empty;
267
+ app.attach(plugin);
268
+ });
269
+
270
+ EVENTS.forEach(event => {
271
+ const handlerName = `on${event[0].toUpperCase()}${event.substring(1)}`;
272
+
273
+ it(`attaches plugin "${event}" handler`, () => {
274
+ app.emit(event);
275
+ expect(plugin[handlerName].calledOnce).to.be.true;
276
+ });
277
+ });
278
+ });
279
+
280
+ context('when errors occur within a plugin', () => {
281
+ let error;
282
+ let listeners;
283
+ let plugin;
284
+
285
+ before(async () => {
286
+ nock.cleanAll();
287
+ nock('https://www.servicea.example').get('/tos').reply(200, serviceASnapshotExpectedContent, { 'Content-Type': 'text/html' });
288
+
289
+ app = new Archivist({
290
+ recorderConfig: config.get('recorder'),
291
+ fetcherConfig: config.get('fetcher'),
292
+ });
293
+ await app.initialize();
294
+
295
+ plugin = { onFirstVersionRecorded: () => { throw new Error('Plugin error'); } };
296
+
297
+ listeners = process.listeners('unhandledRejection'); // back up listeners
298
+ process.removeAllListeners('unhandledRejection'); // remove all listeners to avoid exit the process
299
+
300
+ process.on('unhandledRejection', reason => { error = reason; });
301
+
302
+ app.attach(plugin);
303
+ });
304
+
305
+ after(async () => {
306
+ process.removeAllListeners('unhandledRejection');
307
+ listeners.forEach(listener => process.on('unhandledRejection', listener));
308
+ await resetGitRepositories();
309
+ });
310
+
311
+ it('does not crash the tracking process', done => {
312
+ app.track({ services: [services[0]] }).then(() => {
313
+ if (error) {
314
+ return done(error);
315
+ }
316
+ done();
317
+ });
318
+ });
319
+ });
320
+ });
321
+
248
322
  describe('events', () => {
249
323
  const spies = {};
250
324
 
package/src/index.js CHANGED
@@ -39,17 +39,25 @@ export default async function track({ services, types, extractOnly, schedule })
39
39
  }
40
40
 
41
41
  if (process.env.SENDINBLUE_API_KEY) {
42
- archivist.attach(new Notifier(archivist.services));
42
+ try {
43
+ archivist.attach(new Notifier(archivist.services));
44
+ } catch (error) {
45
+ logger.error('Cannot instantiate the Notifier module; it will be ignored:', error);
46
+ }
43
47
  } else {
44
48
  logger.warn('Environment variable "SENDINBLUE_API_KEY" was not found; the Notifier module will be ignored');
45
49
  }
46
50
 
47
51
  if (process.env.GITHUB_TOKEN) {
48
52
  if (config.has('reporter.githubIssues.repositories.declarations')) {
49
- const reporter = new Reporter(config.get('reporter'));
53
+ try {
54
+ const reporter = new Reporter(config.get('reporter'));
50
55
 
51
- await reporter.initialize();
52
- archivist.attach(reporter);
56
+ await reporter.initialize();
57
+ archivist.attach(reporter);
58
+ } catch (error) {
59
+ logger.error('Cannot instantiate the Reporter module; it will be ignored:', error);
60
+ }
53
61
  } else {
54
62
  logger.warn('Configuration key "reporter.githubIssues.repositories.declarations" was not found; issues on the declarations repository cannot be created');
55
63
  }
@@ -136,4 +136,8 @@ logger.onError = (error, terms) => {
136
136
  logger.error({ message: error.stack, serviceId: terms.serviceId, termsType: terms.type });
137
137
  };
138
138
 
139
+ logger.onPluginError = (error, pluginName) => {
140
+ logger.error({ message: `Error in "${pluginName}" plugin: ${error.stack}` });
141
+ };
142
+
139
143
  export default logger;