@imqueue/job 2.0.6 → 3.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.
- package/.oxfmtrc.json +9 -0
- package/.oxlintrc.json +20 -0
- package/index.d.ts +2 -2
- package/index.js +25 -20
- package/package.json +29 -64
- package/scripts/strip-comment-coverage.mjs +472 -0
- package/eslint.config.mjs +0 -132
package/.oxfmtrc.json
ADDED
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
|
+
}
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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": "
|
|
3
|
+
"version": "3.0.0",
|
|
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": "
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
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
|
|
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",
|
|
@@ -36,66 +39,28 @@
|
|
|
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.
|
|
42
|
+
"@imqueue/core": "^3.2.1"
|
|
40
43
|
},
|
|
41
44
|
"devDependencies": {
|
|
42
|
-
"@
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
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
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"
|
|
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
|
-
"
|
|
83
|
-
"
|
|
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
|
-
}];
|