@imqueue/job 2.0.6 → 3.0.1

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/.oxfmtrc.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "singleQuote": true,
3
+ "tabWidth": 4,
4
+ "printWidth": 80,
5
+ "semi": true,
6
+ "trailingComma": "all",
7
+ "arrowParens": "avoid",
8
+ "bracketSpacing": true
9
+ }
package/.oxlintrc.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "categories": {
4
+ "correctness": "error"
5
+ },
6
+ "rules": {
7
+ "no-debugger": "error",
8
+ "no-console": ["warn", { "allow": ["warn", "error", "info"] }],
9
+ "no-unused-vars": "warn"
10
+ },
11
+ "ignorePatterns": [
12
+ "**/*.js",
13
+ "**/*.d.ts",
14
+ "docs/",
15
+ "coverage/",
16
+ ".nyc_output/",
17
+ ".agent-out/",
18
+ "node_modules/"
19
+ ]
20
+ }
@@ -0,0 +1,58 @@
1
+ # Contributing to @imqueue
2
+
3
+ Thanks for your interest in improving @imqueue! Contributions of all kinds are
4
+ welcome — bug fixes, features, tests, and documentation.
5
+
6
+ ## Contribution terms — please read first
7
+
8
+ @imqueue is **dual-licensed**: it is free to everyone under **GPL-3.0**, and it is
9
+ also offered under **commercial licenses** for closed-source use. To make that
10
+ sustainable, contributions are accepted under the
11
+ **[@imqueue Contribution Terms](./CONTRIBUTION-TERMS.md)**.
12
+
13
+ **By opening a pull request — or otherwise contributing — you agree to those
14
+ terms.** In short:
15
+
16
+ - You **keep the copyright** in your contribution.
17
+ - Your contribution stays available to everyone under **GPL-3.0**.
18
+ - You grant the project owner the right to **also license your contribution
19
+ commercially**, royalty-free — you will not receive a fee for it.
20
+
21
+ **If you do not agree, please do not contribute.** Read the full text in
22
+ [CONTRIBUTION-TERMS.md](./CONTRIBUTION-TERMS.md).
23
+
24
+ ## How to contribute
25
+
26
+ 1. **Open an issue first** for anything non-trivial, so we can agree on the
27
+ approach before you invest time.
28
+ 2. **Fork** the repository and create a topic branch from `master`
29
+ (e.g. `fix/redis-reconnect` or `feat/lock-timeout`).
30
+ 3. **Make your change**, following the existing code style. Keep pull requests
31
+ focused — one logical change per PR.
32
+ 4. **Add or update tests** so the change is covered, and make sure the full suite
33
+ passes locally:
34
+ ```bash
35
+ npm ci
36
+ npm test
37
+ ```
38
+ 5. **Write clear commit messages** and a descriptive PR title and summary.
39
+ 6. **Open the pull request** against `master` and fill in the PR template,
40
+ including the contribution-terms checkbox.
41
+
42
+ ## Guidelines
43
+
44
+ - Match the existing TypeScript style and formatting already used in the file you
45
+ are editing.
46
+ - Keep public API changes documented (doc-blocks / README as appropriate).
47
+ - Be respectful and constructive in reviews and discussions.
48
+
49
+ ## Reporting security issues
50
+
51
+ Please **do not** open a public issue for security vulnerabilities. Report them
52
+ privately to the maintainers (see the repository's security policy or contact
53
+ address) so they can be addressed responsibly.
54
+
55
+ ---
56
+
57
+ Questions about the terms or a larger/corporate contribution? Reach out before you
58
+ start and we'll help.
@@ -0,0 +1,79 @@
1
+ # @imqueue Contribution Terms (v1.0)
2
+
3
+ These terms apply to every contribution to any repository of the **@imqueue**
4
+ project — the packages published under the `@imqueue` npm scope and any repository
5
+ in the imqueue GitHub organisation.
6
+
7
+ **By submitting a contribution — opening a pull request, pushing a commit, sending
8
+ a patch, or otherwise offering work for inclusion — you accept these terms in
9
+ full. If you do not agree with them, do not contribute.**
10
+
11
+ In these terms, **"Owner"** means Mykhailo Stadnyk, together with his successors,
12
+ assigns and any entity he controls (including VaryLogic s.r.o.), and
13
+ **"Contribution"** means any work of authorship — code, documentation or other
14
+ material — you submit to an @imqueue repository.
15
+
16
+ ## 1. What you grant
17
+
18
+ You grant the Owner a **perpetual, worldwide, irrevocable, royalty-free,
19
+ non-exclusive, transferable and sublicensable** license to use, reproduce,
20
+ modify, adapt, publish, distribute and otherwise exploit your Contribution and
21
+ derivative works of it, **and to license and sub-license it under any terms the
22
+ Owner chooses — including commercial and proprietary (closed-source) licenses,
23
+ without any obligation to release source code.**
24
+
25
+ In plain terms: the Owner may include your Contribution in @imqueue and **sell
26
+ commercial licenses that cover it**, alongside the open-source edition.
27
+
28
+ ## 2. No royalties, fees or compensation
29
+
30
+ The rights above are granted **free of charge**. You **waive** any right to
31
+ royalties, license fees, revenue share or any other compensation arising from the
32
+ Owner's use or licensing of your Contribution, including its commercial licensing,
33
+ and you agree not to seek any such payment now or in the future.
34
+
35
+ ## 3. You keep your copyright
36
+
37
+ You **retain ownership** of the copyright in your Contribution and may use it
38
+ elsewhere for your own purposes. You are granting the Owner the rights in Section
39
+ 1 — you are not required to transfer ownership. Your Contribution also remains
40
+ available to the public under the repository's open-source license (GPL-3.0).
41
+
42
+ ## 4. Patent license
43
+
44
+ You grant the Owner and all downstream recipients a perpetual, worldwide,
45
+ royalty-free, irrevocable (except as stated) patent license to make, use, sell,
46
+ offer to sell, import and transfer your Contribution, limited to the patent claims
47
+ you can license that are necessarily infringed by your Contribution alone or in
48
+ combination with the project. If you start patent litigation alleging the project
49
+ or your Contribution infringes your patents, the patent license you received
50
+ terminates.
51
+
52
+ ## 5. Your promises
53
+
54
+ You confirm that: (a) the Contribution is your own original work, or you have the
55
+ right to submit it and to grant these rights; (b) if your employer has any rights
56
+ in your work, you have their permission to contribute, or they have waived those
57
+ rights; and (c) you have identified any third-party material in your Contribution
58
+ and its license.
59
+
60
+ ## 6. Moral rights
61
+
62
+ To the maximum extent permitted by applicable law, you waive, or agree not to
63
+ exercise, any moral rights in your Contribution in a way that would prevent the
64
+ Owner from exercising the rights in Section 1. (Certain moral rights are
65
+ inalienable under Slovak/EU law; nothing here purports to transfer those.)
66
+
67
+ ## 7. No obligation; "as is"
68
+
69
+ The Owner is under no obligation to use your Contribution. It is provided **"as
70
+ is"**, without warranty of any kind.
71
+
72
+ ## 8. Governing law
73
+
74
+ These terms are governed by the laws of the Slovak Republic, without regard to its
75
+ conflict-of-laws rules.
76
+
77
+ ---
78
+
79
+ **If you do not agree to all of the above, do not contribute to @imqueue.**
package/SECURITY.md ADDED
@@ -0,0 +1,40 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ If you believe you have found a security vulnerability in **@imqueue/job** (or any
6
+ `@imqueue/*` package), please report it **privately** — do not open a public issue,
7
+ pull request, or discussion for it.
8
+
9
+ Two private channels:
10
+
11
+ - **GitHub** — use *Security → Report a vulnerability* on this repository to open a
12
+ private advisory (preferred; it keeps the report and the fix coordinated in one
13
+ place).
14
+ - **Email** — <support@imqueue.com> with the details below.
15
+
16
+ Please include:
17
+
18
+ - the affected package and version(s);
19
+ - a description of the issue and its impact;
20
+ - steps to reproduce, or a proof of concept, where possible.
21
+
22
+ ## What to expect
23
+
24
+ - We aim to acknowledge a report within a few business days.
25
+ - We'll confirm the issue, keep you updated on progress, and coordinate a fix and a
26
+ disclosure timeline with you.
27
+ - Once a fix is released we'll credit the reporter in the advisory unless you prefer
28
+ to remain anonymous.
29
+
30
+ ## Supported versions
31
+
32
+ Security fixes land on the latest published release line of each `@imqueue/*`
33
+ package on npm. Please make sure you can reproduce an issue against the current
34
+ release before reporting.
35
+
36
+ ## Scope
37
+
38
+ The `@imqueue` framework is open source under GPL-3.0. This policy covers the code
39
+ in the `@imqueue/*` packages. Vulnerabilities in third-party dependencies should be
40
+ reported to those projects, though we're glad to help coordinate an upgrade.
package/index.d.ts CHANGED
@@ -21,7 +21,7 @@
21
21
  * purchase a proprietary commercial license. Please contact us at
22
22
  * <support@imqueue.com> to get commercial licensing options.
23
23
  */
24
- import { ILogger, IMessageQueue } from '@imqueue/core';
24
+ import { type ILogger, type IMessageQueue } from '@imqueue/core';
25
25
  /**
26
26
  * Job queues options
27
27
  */
@@ -184,7 +184,7 @@ export interface AnyJobQueuePublisher<T, U> {
184
184
  export declare abstract class BaseJobQueue<T, U> implements AnyJobQueue<T> {
185
185
  protected options: JobQueueOptions;
186
186
  protected imq: IMessageQueue;
187
- protected handler: JobQueuePopHandler<U>;
187
+ protected handler?: JobQueuePopHandler<U>;
188
188
  readonly logger: ILogger;
189
189
  protected constructor(options: JobQueueOptions);
190
190
  /**
package/index.js CHANGED
@@ -1,6 +1,3 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.JobQueueWorker = exports.JobQueuePublisher = exports.BaseJobQueue = void 0;
4
1
  /*!
5
2
  * Job Queue for @imqueue framework
6
3
  *
@@ -24,11 +21,15 @@ exports.JobQueueWorker = exports.JobQueuePublisher = exports.BaseJobQueue = void
24
21
  * purchase a proprietary commercial license. Please contact us at
25
22
  * <support@imqueue.com> to get commercial licensing options.
26
23
  */
27
- const core_1 = require("@imqueue/core");
24
+ import IMQ, { IMQMode, } from '@imqueue/core';
28
25
  /**
29
26
  * Abstract job queue, handles base implementations of AnyJobQueue interface.
30
27
  */
31
- class BaseJobQueue {
28
+ export class BaseJobQueue {
29
+ options;
30
+ imq;
31
+ handler;
32
+ logger;
32
33
  constructor(options) {
33
34
  this.options = options;
34
35
  this.logger = options.logger || console;
@@ -68,7 +69,6 @@ class BaseJobQueue {
68
69
  await this.imq.destroy();
69
70
  }
70
71
  }
71
- exports.BaseJobQueue = BaseJobQueue;
72
72
  /**
73
73
  * Creates and returns IMQOptions derived from a given JobQueueOptions
74
74
  *
@@ -83,10 +83,10 @@ function toIMQOptions(options, logger) {
83
83
  username: options.username,
84
84
  password: options.password,
85
85
  cleanup: false,
86
- safeDelivery: typeof options.safe === 'undefined'
87
- ? true : options.safe,
86
+ safeDelivery: typeof options.safe === 'undefined' ? true : options.safe,
88
87
  safeDeliveryTtl: typeof options.safeLockTtl === 'undefined'
89
- ? 10000 : options.safeLockTtl,
88
+ ? 10000
89
+ : options.safeLockTtl,
90
90
  prefix: options.prefix || 'imq-job',
91
91
  verbose: options.verbose,
92
92
  verboseExtended: options.verboseExtended,
@@ -98,7 +98,7 @@ function toIMQOptions(options, logger) {
98
98
  * Implements simple scheduled job queue publisher. Job queue publisher is only
99
99
  * responsible for pushing queue messages.
100
100
  */
101
- class JobQueuePublisher extends BaseJobQueue {
101
+ export class JobQueuePublisher extends BaseJobQueue {
102
102
  /**
103
103
  * Constructor. Instantiates new JobQueue instance.
104
104
  *
@@ -107,7 +107,7 @@ class JobQueuePublisher extends BaseJobQueue {
107
107
  */
108
108
  constructor(options) {
109
109
  super(options);
110
- this.imq = core_1.default.create(options.name, toIMQOptions(options, this.logger), core_1.IMQMode.PUBLISHER);
110
+ this.imq = IMQ.create(options.name, toIMQOptions(options, this.logger), IMQMode.PUBLISHER);
111
111
  }
112
112
  /**
113
113
  * Pushes new job to this queue
@@ -118,17 +118,24 @@ class JobQueuePublisher extends BaseJobQueue {
118
118
  */
119
119
  push(job, options) {
120
120
  options = options || {};
121
- this.imq.send(this.name, Object.assign(Object.assign({ job: job }, (options.ttl ? { expire: Date.now() + options.ttl } : {})), (options.delay ? { delay: options.delay } : {})), options.delay).catch(err => this.logger.log('[JobQueue] push error:', err));
121
+ this.imq
122
+ .send(this.name, {
123
+ job: job,
124
+ ...(options.ttl
125
+ ? { expire: Date.now() + options.ttl }
126
+ : {}),
127
+ ...(options.delay ? { delay: options.delay } : {}),
128
+ }, options.delay)
129
+ .catch(err => this.logger.log('[JobQueue] push error:', err));
122
130
  return this;
123
131
  }
124
132
  }
125
- exports.JobQueuePublisher = JobQueuePublisher;
126
133
  // noinspection JSUnusedGlobalSymbols
127
134
  /**
128
135
  * Implements simple scheduled job queue worker. Job queue worker is only
129
136
  * responsible for processing queue messages.
130
137
  */
131
- class JobQueueWorker extends BaseJobQueue {
138
+ export class JobQueueWorker extends BaseJobQueue {
132
139
  /**
133
140
  * Constructor. Instantiates new JobQueue instance.
134
141
  *
@@ -137,7 +144,7 @@ class JobQueueWorker extends BaseJobQueue {
137
144
  */
138
145
  constructor(options) {
139
146
  super(options);
140
- this.imq = core_1.default.create(options.name, toIMQOptions(options, this.logger), core_1.IMQMode.WORKER);
147
+ this.imq = IMQ.create(options.name, toIMQOptions(options, this.logger), IMQMode.WORKER);
141
148
  }
142
149
  /**
143
150
  * Sets up job handler, which is called when the job is popped from this
@@ -156,7 +163,7 @@ class JobQueueWorker extends BaseJobQueue {
156
163
  const { job, expire, delay } = message;
157
164
  let rescheduleDelay;
158
165
  try {
159
- rescheduleDelay = this.handler(job);
166
+ rescheduleDelay = this.handler?.(job);
160
167
  if (rescheduleDelay &&
161
168
  typeof rescheduleDelay === 'object' &&
162
169
  rescheduleDelay &&
@@ -179,7 +186,6 @@ class JobQueueWorker extends BaseJobQueue {
179
186
  return this;
180
187
  }
181
188
  }
182
- exports.JobQueueWorker = JobQueueWorker;
183
189
  // noinspection JSUnusedGlobalSymbols
184
190
  /**
185
191
  * Implements simple scheduled job queue. Job scheduling is optional. It may
@@ -188,7 +194,7 @@ exports.JobQueueWorker = JobQueueWorker;
188
194
  * the job is removed from a queue.
189
195
  * Supports graceful shutdown, if TERM or SIGINT is sent to the process.
190
196
  */
191
- class JobQueue extends BaseJobQueue {
197
+ export default class JobQueue extends BaseJobQueue {
192
198
  /**
193
199
  * Constructor. Instantiates new JobQueue instance.
194
200
  *
@@ -197,7 +203,7 @@ class JobQueue extends BaseJobQueue {
197
203
  */
198
204
  constructor(options) {
199
205
  super(options);
200
- this.imq = core_1.default.create(options.name, toIMQOptions(options, this.logger));
206
+ this.imq = IMQ.create(options.name, toIMQOptions(options, this.logger));
201
207
  }
202
208
  /**
203
209
  * Starts processing job queue. Throws if handler is not set before start.
@@ -236,5 +242,4 @@ class JobQueue extends BaseJobQueue {
236
242
  return JobQueueWorker.prototype.onPop.call(this, handler);
237
243
  }
238
244
  }
239
- exports.default = JobQueue;
240
245
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imqueue/job",
3
- "version": "2.0.6",
3
+ "version": "3.0.1",
4
4
  "description": "Simple job queue",
5
5
  "keywords": [
6
6
  "message-queue",
@@ -13,17 +13,20 @@
13
13
  "json-message-queue"
14
14
  ],
15
15
  "scripts": {
16
- "prepare": "./node_modules/.bin/tsc",
17
- "show:test": "/usr/bin/env node -e \"import('open').then(open => open.default('file://`pwd`/coverage/index.html',{wait:false}));\"",
18
- "test": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && npm run show:test",
19
- "clean-typedefs": "find . -name '*.d.ts' -not -wholename '*node_modules*' -not -wholename '*generator*' -type f -delete",
20
- "clean-maps": "find . -name '*.js.map' -not -wholename '*node_modules*' -not -wholename '*generator*' -type f -delete",
21
- "clean-js": "find . -name '*.js' -not -wholename '*node_modules*' -not -wholename '*generator*' -type f -delete",
16
+ "prepare": "npm run build",
17
+ "build": "npm run clean-compiled && tsc",
18
+ "lint": "oxlint",
19
+ "format": "oxfmt \"index.ts\" \"test/**/*.ts\"",
20
+ "format:check": "oxfmt --check \"index.ts\" \"test/**/*.ts\"",
21
+ "test": "npm run build && node --experimental-test-module-mocks --import ./test/mocks/index.js --test --test-timeout=15000 $(find test -name '*.spec.js')",
22
+ "test-coverage": "npm run build && node --experimental-test-module-mocks --enable-source-maps --import ./test/mocks/index.js --test --experimental-test-coverage --test-timeout=15000 $(find test -name '*.spec.js')",
23
+ "test-lcov": "npm run build && mkdir -p coverage && node --experimental-test-module-mocks --enable-source-maps --import ./test/mocks/index.js --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info --test-timeout=15000 $(find test -name '*.spec.js'); node scripts/strip-comment-coverage.mjs coverage/lcov.info",
24
+ "clean-typedefs": "find . -name '*.d.ts' -not -wholename '*node_modules*' -type f -delete",
25
+ "clean-maps": "find . -name '*.js.map' -not -wholename '*node_modules*' -type f -delete",
26
+ "clean-js": "find . -name '*.js' -not -wholename '*node_modules*' -not -wholename '*scripts*' -type f -delete",
27
+ "clean-compiled": "npm run clean-js && npm run clean-typedefs && npm run clean-maps",
22
28
  "clean-tests": "rm -rf .nyc_output coverage",
23
- "clean-doc": "rm -rf docs",
24
- "clean-benchmark": "rm -rf benchmark-result",
25
- "clean": "npm run clean-tests && npm run clean-typedefs && npm run clean-maps && npm run clean-js && npm run clean-doc && npm run clean-benchmark",
26
- "doc": "rm -rf docs && typedoc --excludePrivate --excludeExternals --hideGenerator --exclude \"**/+(test|node_modules|docs|coverage|benchmark|.nyc_output)/**/*\" --out ./docs . && /usr/bin/env node -e \"import('open').then(open => open.default('file://`pwd`/docs/index.html',{wait:false}))\""
29
+ "clean": "npm run clean-tests && npm run clean-compiled"
27
30
  },
28
31
  "repository": {
29
32
  "type": "git",
@@ -32,70 +35,32 @@
32
35
  "bugs": {
33
36
  "url": "https://github.com/imqueue/job/issues"
34
37
  },
35
- "homepage": "https://imqueue.com/",
38
+ "homepage": "https://imqueue.org/",
36
39
  "author": "imqueue.com <support@imqueue.com> (https://imqueue.com)",
37
40
  "license": "GPL-3.0-only",
38
41
  "dependencies": {
39
- "@imqueue/core": "^2.0.18"
42
+ "@imqueue/core": "^3.2.2"
40
43
  },
41
44
  "devDependencies": {
42
- "@eslint/eslintrc": "^3.3.1",
43
- "@eslint/js": "^9.35.0",
44
- "@types/chai": "^5.2.2",
45
- "@types/mocha": "^10.0.10",
46
- "@types/mock-require": "^3.0.0",
47
- "@types/node": "^24.9.2",
48
- "@types/sinon": "^17.0.4",
49
- "@typescript-eslint/eslint-plugin": "^8.43.0",
50
- "@typescript-eslint/parser": "^8.43.0",
51
- "@typescript-eslint/typescript-estree": "^8.43.0",
52
- "chai": "^6.0.1",
53
- "eslint": "^9.35.0",
54
- "globals": "^16.4.0",
55
- "mocha": "^11.7.2",
56
- "mocha-lcov-reporter": "^1.3.0",
57
- "mock-require": "^3.0.3",
58
- "npm-scripts-help": "^0.8.0",
59
- "nyc": "^17.1.0",
60
- "open": "^10.2.0",
61
- "reflect-metadata": "^0.2.2",
62
- "sinon": "^21.0.0",
63
- "source-map-support": "^0.5.21",
64
- "ts-node": "^10.9.2",
65
- "typedoc": "^0.28.12",
66
- "typescript": "^5.9.2",
67
- "yargs": "^18.0.0"
45
+ "@types/node": "^24.9.1",
46
+ "oxfmt": "0.57.0",
47
+ "oxlint": "1.72.0",
48
+ "typescript": "^7.0.2"
68
49
  },
69
50
  "main": "index.js",
70
51
  "typescript": {
71
52
  "definitions": "index.d.ts"
72
53
  },
73
- "mocha": {
74
- "require": [
75
- "ts-node/register",
76
- "source-map-support/register"
77
- ],
78
- "recursive": true,
79
- "bail": true,
80
- "full-trace": true
54
+ "type": "module",
55
+ "types": "index.d.ts",
56
+ "exports": {
57
+ ".": {
58
+ "types": "./index.d.ts",
59
+ "default": "./index.js"
60
+ },
61
+ "./package.json": "./package.json"
81
62
  },
82
- "nyc": {
83
- "check-coverage": false,
84
- "extension": [
85
- ".ts"
86
- ],
87
- "exclude": [
88
- "**/*.d.ts",
89
- "**/test/**"
90
- ],
91
- "require": [
92
- "ts-node/register"
93
- ],
94
- "reporter": [
95
- "html",
96
- "text",
97
- "text-summary",
98
- "lcovonly"
99
- ]
63
+ "engines": {
64
+ "node": ">=22.12.0"
100
65
  }
101
66
  }
@@ -0,0 +1,472 @@
1
+ /*!
2
+ * Coverage post-processor
3
+ *
4
+ * I'm Queue Software Project
5
+ * Copyright (C) 2025 imqueue.com <support@imqueue.com>
6
+ *
7
+ * This program is free software: you can redistribute it and/or modify
8
+ * it under the terms of the GNU General Public License as published by
9
+ * the Free Software Foundation, either version 3 of the License, or
10
+ * (at your option) any later version.
11
+ *
12
+ * This program is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ * GNU General Public License for more details.
16
+ *
17
+ * You should have received a copy of the GNU General Public License
18
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
19
+ *
20
+ * If you want to use this code in a closed source (commercial) project, you can
21
+ * purchase a proprietary commercial license. Please contact us at
22
+ * <support@imqueue.com> to get commercial licensing options.
23
+ */
24
+
25
+ /**
26
+ * Node's built-in coverage (`--experimental-test-coverage --enable-source-maps`)
27
+ * reports every non-code line of a `.ts` source as uncovered, because comment
28
+ * and blank lines have no source-map mappings and therefore fall outside every
29
+ * covered V8 byte-range. That paints the license header and each JSDoc block
30
+ * red in the HTML report and pushes per-file line% far below reality.
31
+ *
32
+ * Node's reporter also emits records that the (strict) genhtml LCOV 2.x tool
33
+ * rejects — function entries with `undefined` line numbers or synthetic,
34
+ * duplicated names (`<instance_members_initializer>`, and decorated methods all
35
+ * mis-named `has`/`get`), plus `BRDA:undefined,...` branch records. Left in,
36
+ * they produce a wall of warnings and a fatal "unexpected category" error.
37
+ *
38
+ * This script rewrites an lcov file in place: it drops the `DA`/`BRDA` records
39
+ * for lines that contain no executable code (comments and blank lines), drops
40
+ * all function records and malformed branch records, then recomputes the
41
+ * `LF`/`LH`/`BRF`/`BRH` totals. It uses the TypeScript scanner (already a dev
42
+ * dependency) to locate comments reliably — comment markers inside strings,
43
+ * template literals and regexes are not misdetected. The result renders in
44
+ * genhtml with accurate line + branch coverage and no warnings or errors.
45
+ */
46
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
47
+ // TypeScript 7 (native port) no longer exposes the classic compiler API from
48
+ // the package root — the lightweight scanner/AST primitives live under the
49
+ // `typescript/unstable/ast` entry point (createSourceFile is gone entirely, so
50
+ // import spans are located with the scanner rather than a parsed AST).
51
+ import * as ts from 'typescript/unstable/ast';
52
+
53
+ const lcovPath = process.argv[2] || 'coverage/lcov.info';
54
+
55
+ if (!existsSync(lcovPath)) {
56
+ console.warn(`strip-comment-coverage: ${lcovPath} not found, skipping`);
57
+ process.exit(0);
58
+ }
59
+
60
+ /**
61
+ * Returns the set of 1-based line numbers in the given source that hold no
62
+ * executable code — i.e. blank lines and lines whose only content is a comment.
63
+ *
64
+ * @param {string} text
65
+ * @returns {Set<number>}
66
+ */
67
+ function nonCodeLines(text) {
68
+ const commentChars = new Uint8Array(text.length);
69
+ const scanner = ts.createScanner(
70
+ ts.ScriptTarget.Latest,
71
+ /* skipTrivia */ false,
72
+ ts.LanguageVariant.Standard,
73
+ );
74
+
75
+ scanner.setText(text);
76
+
77
+ let token = scanner.scan();
78
+
79
+ while (token !== ts.SyntaxKind.EndOfFile) {
80
+ if (
81
+ token === ts.SyntaxKind.SingleLineCommentTrivia ||
82
+ token === ts.SyntaxKind.MultiLineCommentTrivia
83
+ ) {
84
+ const start = scanner.getTokenStart();
85
+ const end = scanner.getTokenEnd();
86
+
87
+ for (let i = start; i < end; i++) {
88
+ commentChars[i] = 1;
89
+ }
90
+ }
91
+
92
+ token = scanner.scan();
93
+ }
94
+
95
+ const lines = text.split('\n');
96
+ const result = new Set();
97
+ let pos = 0;
98
+
99
+ for (let ln = 0; ln < lines.length; ln++) {
100
+ const line = lines[ln];
101
+ let hasCode = false;
102
+
103
+ for (let i = 0; i < line.length; i++) {
104
+ const ch = line[i];
105
+
106
+ if (ch === ' ' || ch === '\t' || ch === '\r') {
107
+ continue;
108
+ }
109
+
110
+ if (!commentChars[pos + i]) {
111
+ hasCode = true;
112
+ break;
113
+ }
114
+ }
115
+
116
+ if (!hasCode) {
117
+ result.add(ln + 1);
118
+ }
119
+
120
+ pos += line.length + 1; // account for the split '\n'
121
+ }
122
+
123
+ return result;
124
+ }
125
+
126
+ /**
127
+ * Returns the set of 1-based lines occupied by top-level import statements.
128
+ * Imports compile to `require(...)` that runs on module load, so they are
129
+ * always executed, yet Node's source-mapped coverage frequently mis-reports
130
+ * them as uncovered (the mapping lands outside the covered byte-range). Treating
131
+ * them as non-coverable corrects that artifact rather than hiding real logic.
132
+ *
133
+ * @param {string} text
134
+ * @returns {Set<number>}
135
+ */
136
+ function importLines(text) {
137
+ // TypeScript 7 dropped `createSourceFile`, so top-level import statements are
138
+ // located with the scanner: a top-level `import` keyword (depth 0, not the
139
+ // dynamic `import(...)` call nor `import.meta`) begins a declaration whose
140
+ // span reaches the terminating semicolon — both `import ... from '...'` and
141
+ // `import x = require('...')` end that way in this codebase.
142
+ const scanner = ts.createScanner(
143
+ ts.ScriptTarget.Latest,
144
+ /* skipTrivia */ true,
145
+ ts.LanguageVariant.Standard,
146
+ );
147
+
148
+ scanner.setText(text);
149
+
150
+ const lineStarts = ts.computeLineStarts(text);
151
+ // 0-based line holding `pos`: largest index with lineStarts[i] <= pos
152
+ const lineOf = pos => {
153
+ let lo = 0;
154
+ let hi = lineStarts.length - 1;
155
+
156
+ while (lo < hi) {
157
+ const mid = (lo + hi + 1) >> 1;
158
+
159
+ if (lineStarts[mid] <= pos) {
160
+ lo = mid;
161
+ } else {
162
+ hi = mid - 1;
163
+ }
164
+ }
165
+
166
+ return lo;
167
+ };
168
+
169
+ const K = ts.SyntaxKind;
170
+ const result = new Set();
171
+ let depth = 0;
172
+ let token = scanner.scan();
173
+
174
+ while (token !== K.EndOfFile) {
175
+ if (
176
+ token === K.OpenBraceToken ||
177
+ token === K.OpenParenToken ||
178
+ token === K.OpenBracketToken
179
+ ) {
180
+ depth++;
181
+ } else if (
182
+ token === K.CloseBraceToken ||
183
+ token === K.CloseParenToken ||
184
+ token === K.CloseBracketToken
185
+ ) {
186
+ depth--;
187
+ } else if (depth === 0 && token === K.ImportKeyword) {
188
+ const start = scanner.getTokenStart();
189
+ let next = scanner.scan();
190
+
191
+ // `import(...)` dynamic call or `import.meta` — not a declaration
192
+ if (next === K.OpenParenToken || next === K.DotToken) {
193
+ token = next;
194
+ continue;
195
+ }
196
+
197
+ // consume to the terminating semicolon at bracket depth 0
198
+ let inner = 0;
199
+ let end = scanner.getTokenEnd();
200
+
201
+ while (next !== K.EndOfFile) {
202
+ if (next === K.OpenBraceToken || next === K.OpenParenToken) {
203
+ inner++;
204
+ } else if (
205
+ next === K.CloseBraceToken ||
206
+ next === K.CloseParenToken
207
+ ) {
208
+ inner--;
209
+ } else if (next === K.SemicolonToken && inner === 0) {
210
+ end = scanner.getTokenEnd();
211
+ break;
212
+ }
213
+
214
+ end = scanner.getTokenEnd();
215
+ next = scanner.scan();
216
+ }
217
+
218
+ for (let line = lineOf(start); line <= lineOf(end); line++) {
219
+ result.add(line + 1);
220
+ }
221
+
222
+ token = scanner.scan();
223
+ continue;
224
+ }
225
+
226
+ token = scanner.scan();
227
+ }
228
+
229
+ return result;
230
+ }
231
+
232
+ const B64 =
233
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
234
+ const B64MAP = new Map([...B64].map((c, i) => [c, i]));
235
+
236
+ /**
237
+ * Decodes one base64 VLQ source-map segment into its integer fields.
238
+ *
239
+ * @param {string} seg
240
+ * @returns {number[]}
241
+ */
242
+ function decodeVLQ(seg) {
243
+ const out = [];
244
+ let shift = 0;
245
+ let value = 0;
246
+
247
+ for (const ch of seg) {
248
+ const digit = B64MAP.get(ch);
249
+
250
+ if (digit === undefined) {
251
+ break;
252
+ }
253
+
254
+ value += (digit & 31) << shift;
255
+
256
+ if (digit & 32) {
257
+ shift += 5;
258
+ } else {
259
+ out.push(value & 1 ? -(value >> 1) : value >> 1);
260
+ value = 0;
261
+ shift = 0;
262
+ }
263
+ }
264
+
265
+ return out;
266
+ }
267
+
268
+ /**
269
+ * Returns the set of 1-based source lines that produced generated JavaScript,
270
+ * read from the file's `.js.map`. These are exactly the coverable lines — every
271
+ * other line (comments, blanks, type-only declarations, interfaces) emits no
272
+ * code and can never be executed. Returns null when no source map is present.
273
+ *
274
+ * @param {string} sourceFile - path of the .ts source (e.g. src/RedisQueue.ts)
275
+ * @returns {Set<number> | null}
276
+ */
277
+ function emittedLines(sourceFile) {
278
+ const mapPath = sourceFile.replace(/\.ts$/, '.js.map');
279
+
280
+ if (!existsSync(mapPath)) {
281
+ return null;
282
+ }
283
+
284
+ let map;
285
+
286
+ try {
287
+ map = JSON.parse(readFileSync(mapPath, 'utf8'));
288
+ } catch {
289
+ return null;
290
+ }
291
+
292
+ if (typeof map.mappings !== 'string') {
293
+ return null;
294
+ }
295
+
296
+ const lines = new Set();
297
+ let srcIndex = 0;
298
+ let srcLine = 0;
299
+
300
+ for (const group of map.mappings.split(';')) {
301
+ for (const seg of group.split(',')) {
302
+ if (!seg) {
303
+ continue;
304
+ }
305
+
306
+ const fields = decodeVLQ(seg);
307
+
308
+ // [genCol, srcIndex, srcLine, srcCol, nameIndex]; <4 fields = no
309
+ // source position for this segment
310
+ if (fields.length >= 4) {
311
+ srcIndex += fields[1];
312
+ srcLine += fields[2];
313
+
314
+ if (srcIndex === 0) {
315
+ lines.add(srcLine + 1); // map lines are 0-based
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ return lines;
322
+ }
323
+
324
+ const coverableCache = new Map();
325
+
326
+ /**
327
+ * Resolves how to filter a source file's line records:
328
+ * - `{ mode: 'keep', set }` — keep only lines in `set` (source-map driven);
329
+ * - `{ mode: 'drop', set }` — drop lines in `set` (comment-scanner fallback).
330
+ *
331
+ * @param {string} sourceFile
332
+ * @returns {{ mode: 'keep' | 'drop', set: Set<number> }}
333
+ */
334
+ function coverableInfo(sourceFile) {
335
+ if (coverableCache.has(sourceFile)) {
336
+ return coverableCache.get(sourceFile);
337
+ }
338
+
339
+ const emitted = sourceFile ? emittedLines(sourceFile) : null;
340
+ let info;
341
+
342
+ if (emitted && existsSync(sourceFile)) {
343
+ // A line is coverable only if it emitted JS (has a source-map mapping)
344
+ // AND is not a comment/blank AND is not an import statement. The comment
345
+ // check is required because `removeComments: false` keeps license/JSDoc
346
+ // comments in the output, so they carry mappings too; the mapping check
347
+ // drops type-only lines (interfaces, type aliases) which emit nothing;
348
+ // the import check drops the mis-mapped import artifact.
349
+ const text = readFileSync(sourceFile, 'utf8');
350
+ const nonCode = nonCodeLines(text);
351
+ const imports = importLines(text);
352
+ const keep = new Set(
353
+ [...emitted].filter(
354
+ line => !nonCode.has(line) && !imports.has(line),
355
+ ),
356
+ );
357
+ info = { mode: 'keep', set: keep };
358
+ } else if (sourceFile && existsSync(sourceFile)) {
359
+ const text = readFileSync(sourceFile, 'utf8');
360
+ const skip = nonCodeLines(text);
361
+
362
+ for (const line of importLines(text)) {
363
+ skip.add(line);
364
+ }
365
+
366
+ info = { mode: 'drop', set: skip };
367
+ } else {
368
+ info = { mode: 'drop', set: new Set() };
369
+ }
370
+
371
+ coverableCache.set(sourceFile, info);
372
+
373
+ return info;
374
+ }
375
+
376
+ let strippedLines = 0;
377
+
378
+ /**
379
+ * Rewrites a single lcov record so that genhtml accepts it without warnings or
380
+ * errors. It:
381
+ * - drops DA/BRDA entries on non-code (comment/blank) lines;
382
+ * - drops all function records (FN/FNDA/FNF/FNH) — Node's source-mapped
383
+ * function detection is unreliable here (undefined line numbers, mis-named
384
+ * and duplicated entries), which genhtml rejects, so line + branch coverage
385
+ * is kept instead;
386
+ * - drops malformed branch records such as `BRDA:undefined,...`;
387
+ * - recomputes the LF/LH/BRF/BRH totals.
388
+ *
389
+ * @param {string[]} recordLines - record body, excluding the end_of_record line
390
+ * @returns {string}
391
+ */
392
+ function processRecord(recordLines) {
393
+ const sfLine = recordLines.find(line => line.startsWith('SF:'));
394
+ const sourceFile = sfLine ? sfLine.slice(3).trim() : '';
395
+ const info = coverableInfo(sourceFile);
396
+
397
+ // true when a source line produces no executable code and must be dropped
398
+ const drop = n => (info.mode === 'keep' ? !info.set.has(n) : info.set.has(n));
399
+
400
+ let lf = 0;
401
+ let lh = 0;
402
+ let brf = 0;
403
+ let brh = 0;
404
+
405
+ const kept = recordLines.filter(line => {
406
+ // drop all function records — unreliable under source maps
407
+ if (/^(FN:|FNDA:|FNF:|FNH:)/.test(line)) {
408
+ return false;
409
+ }
410
+
411
+ const da = line.match(/^DA:(\d+),(\d+)/);
412
+
413
+ if (da) {
414
+ if (drop(+da[1])) {
415
+ strippedLines++;
416
+
417
+ return false;
418
+ }
419
+
420
+ lf++;
421
+
422
+ if (+da[2] > 0) {
423
+ lh++;
424
+ }
425
+
426
+ return true;
427
+ }
428
+
429
+ if (line.startsWith('BRDA:')) {
430
+ const brda = line.match(/^BRDA:(\d+),\d+,\d+,(\d+|-)$/);
431
+
432
+ // drop malformed (e.g. BRDA:undefined,...) or non-code branches
433
+ if (!brda || drop(+brda[1])) {
434
+ return false;
435
+ }
436
+
437
+ brf++;
438
+
439
+ if (brda[2] !== '-' && +brda[2] > 0) {
440
+ brh++;
441
+ }
442
+
443
+ return true;
444
+ }
445
+
446
+ // drop the old totals — they are recomputed below
447
+ return !/^(LF|LH|BRF|BRH):/.test(line);
448
+ });
449
+
450
+ return (
451
+ `${kept.join('\n')}\n` +
452
+ `LF:${lf}\nLH:${lh}\nBRF:${brf}\nBRH:${brh}\nend_of_record\n`
453
+ );
454
+ }
455
+
456
+ const output = [];
457
+ let record = [];
458
+
459
+ for (const line of readFileSync(lcovPath, 'utf8').split('\n')) {
460
+ if (line === 'end_of_record') {
461
+ output.push(processRecord(record));
462
+ record = [];
463
+ } else if (line !== '') {
464
+ record.push(line);
465
+ }
466
+ }
467
+
468
+ writeFileSync(lcovPath, output.join(''));
469
+ console.info(
470
+ `strip-comment-coverage: removed ${strippedLines} comment/blank line ` +
471
+ `entries from ${lcovPath}`,
472
+ );
package/eslint.config.mjs DELETED
@@ -1,132 +0,0 @@
1
- import typescriptEslint from "@typescript-eslint/eslint-plugin";
2
- import globals from "globals";
3
- import tsParser from "@typescript-eslint/parser";
4
- import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import js from "@eslint/js";
7
- import { FlatCompat } from "@eslint/eslintrc";
8
-
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = path.dirname(__filename);
11
- const compat = new FlatCompat({
12
- baseDirectory: __dirname,
13
- recommendedConfig: js.configs.recommended,
14
- allConfig: js.configs.all
15
- });
16
-
17
- export default [...compat.extends(
18
- "plugin:@typescript-eslint/recommended",
19
- "plugin:@typescript-eslint/recommended-requiring-type-checking",
20
- ), {
21
- plugins: {
22
- "@typescript-eslint": typescriptEslint,
23
- },
24
-
25
- languageOptions: {
26
- globals: {
27
- ...globals.browser,
28
- ...globals.node,
29
- },
30
-
31
- parser: tsParser,
32
- ecmaVersion: 5,
33
- sourceType: "module",
34
-
35
- parserOptions: {
36
- project: "tsconfig.json",
37
- },
38
- },
39
-
40
- rules: {
41
- "@typescript-eslint/adjacent-overload-signatures": "error",
42
- "@typescript-eslint/array-type": "error",
43
- "@typescript-eslint/class-name-casing": "off",
44
- "@typescript-eslint/consistent-type-assertions": "error",
45
- "@typescript-eslint/interface-name-prefix": "off",
46
- "@typescript-eslint/no-unsafe-assignment": "off",
47
- "@typescript-eslint/no-unsafe-member-access": "off",
48
- "@typescript-eslint/explicit-module-boundary-types": "off",
49
- "@typescript-eslint/no-unsafe-return": "off",
50
- "@typescript-eslint/await-thenable": "off",
51
- "@typescript-eslint/no-misused-promises": "off",
52
- "@typescript-eslint/no-empty-function": "error",
53
- "@typescript-eslint/no-empty-interface": "error",
54
- "@typescript-eslint/no-explicit-any": "off",
55
- "@typescript-eslint/no-misused-new": "error",
56
- "@typescript-eslint/no-namespace": "error",
57
- "@typescript-eslint/no-parameter-properties": "off",
58
- "@typescript-eslint/no-use-before-define": "off",
59
- "@typescript-eslint/no-var-requires": "off",
60
- "@typescript-eslint/prefer-for-of": "error",
61
- "@typescript-eslint/prefer-function-type": "error",
62
- "@typescript-eslint/prefer-namespace-keyword": "error",
63
- "@typescript-eslint/unbound-method": "off",
64
- "@typescript-eslint/no-shadow": ["error"],
65
-
66
- "quotes": ["error", "single", {
67
- avoidEscape: true,
68
- }],
69
-
70
- "semi": "error",
71
- "@typescript-eslint/triple-slash-reference": "error",
72
- "@typescript-eslint/unified-signatures": "error",
73
- "arrow-parens": ["off", "as-needed"],
74
- camelcase: "error",
75
- "comma-dangle": "off",
76
- complexity: "off",
77
- "constructor-super": "error",
78
- "dot-notation": "error",
79
- eqeqeq: ["error", "smart"],
80
- "guard-for-in": "error",
81
-
82
- "id-blacklist": [
83
- "error",
84
- "any",
85
- "Number",
86
- "number",
87
- "String",
88
- "string",
89
- "Boolean",
90
- "boolean",
91
- "Undefined",
92
- "undefined",
93
- ],
94
-
95
- "id-match": "error",
96
- "max-classes-per-file": "off",
97
-
98
- "max-len": ["error", {
99
- code: 80,
100
- }],
101
-
102
- "new-parens": "error",
103
- "no-bitwise": "off",
104
- "no-caller": "error",
105
- "no-cond-assign": "error",
106
- "no-console": "off",
107
- "no-debugger": "error",
108
- "no-empty": "error",
109
- "no-eval": "error",
110
- "no-fallthrough": "off",
111
- "no-invalid-this": "off",
112
- "no-multiple-empty-lines": "off",
113
- "no-new-wrappers": "error",
114
- "no-shadow": "off",
115
- "no-throw-literal": "error",
116
- "no-trailing-spaces": "error",
117
- "no-undef-init": "error",
118
- "no-underscore-dangle": "error",
119
- "no-unsafe-finally": "error",
120
- "no-unused-expressions": "off",
121
- "no-unused-labels": "error",
122
- "no-var": "error",
123
- "object-shorthand": "error",
124
- "one-var": ["error", "never"],
125
- "prefer-arrow/prefer-arrow-functions": "off",
126
- "prefer-const": "error",
127
- radix: "error",
128
- "spaced-comment": "off",
129
- "use-isnan": "error",
130
- "valid-typeof": "off",
131
- },
132
- }];