diff --git a/benchmark/index.ts b/benchmark/index.ts index f2c49dd..6eb4fb4 100644 --- a/benchmark/index.ts +++ b/benchmark/index.ts @@ -26,11 +26,11 @@ import { execSync as exec } from 'node:child_process'; import { arch, cpus, freemem, platform, totalmem } from 'node:os'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { parseArgs } from 'node:util'; -import { run } from './redis-test'; +import { run } from './redis-test.js'; import { resolve } from 'node:path'; import { randomUUID as uuid } from 'node:crypto'; -import { AnyJson } from '..'; -import { setAffinity } from './affinity'; +import { type AnyJson } from '../index.js'; +import { setAffinity } from './affinity.js'; /** * Command line options: canonical long name -> parseArgs config and description diff --git a/benchmark/redis-test.ts b/benchmark/redis-test.ts index 77991cb..f7011f0 100644 --- a/benchmark/redis-test.ts +++ b/benchmark/redis-test.ts @@ -22,8 +22,12 @@ * to get commercial licensing options. */ import { randomUUID as uuid } from 'node:crypto'; -import IMQ, { IMQOptions, JsonObject, AnyJson } from '../index'; -import { pack } from '../src/helpers'; +import IMQ, { + type IMQOptions, + type JsonObject, + type AnyJson, +} from '../index.js'; +import { pack } from '../src/helpers/index.js'; /** * Sample message used within tests diff --git a/index.ts b/index.ts index 755ab2c..4be32bd 100644 --- a/index.ts +++ b/index.ts @@ -22,13 +22,24 @@ * to get commercial licensing options. */ import { - IMessageQueue, - IMessageQueueConstructor, + type IMessageQueue, + type IMessageQueueConstructor, IMQMode, - IMQOptions, -} from './src'; + type IMQOptions, +} from './src/index.js'; +import { ClusteredRedisQueue } from './src/index.js'; +import { RedisQueue } from './src/index.js'; -export * from './src'; +export * from './src/index.js'; + +// ES modules provide no synchronous dynamic loading, so the queue adapters +// are registered statically; the map key matches the previously used +// `${vendor}Queue` / `Clustered${vendor}Queue` file naming convention +const ADAPTERS: { [name: string]: IMessageQueueConstructor } = { + RedisQueue: RedisQueue as unknown as IMessageQueueConstructor, + ClusteredRedisQueue: + ClusteredRedisQueue as unknown as IMessageQueueConstructor, +}; /** * Message Queue Factory @@ -64,9 +75,14 @@ export default class IMQ { ClassName = `Clustered${ClassName}`; } - const Adapter: IMessageQueueConstructor = require( - `${__dirname}/src/${ClassName}.js`, - )[ClassName]; + const Adapter: IMessageQueueConstructor | undefined = + ADAPTERS[ClassName]; + + if (!Adapter) { + throw new TypeError( + `IMQ: unknown queue vendor requested: ${options.vendor}`, + ); + } return new Adapter(name, options, mode); } diff --git a/package-lock.json b/package-lock.json index 0084998..c65eafc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/core", - "version": "3.1.1", + "version": "3.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/core", - "version": "3.1.1", + "version": "3.2.0", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.11.1" diff --git a/package.json b/package.json index ad13c22..bf887e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/core", - "version": "3.1.1", + "version": "3.2.0", "description": "Simple JSON-based messaging queue for inter service communication", "keywords": [ "message-queue", @@ -18,9 +18,9 @@ "lint": "oxlint", "format": "oxfmt \"index.ts\" \"src/**/*.ts\" \"test/**/*.ts\" \"benchmark/**/*.ts\"", "format:check": "oxfmt --check \"index.ts\" \"src/**/*.ts\" \"test/**/*.ts\" \"benchmark/**/*.ts\"", - "test": "npm run build && node --experimental-test-module-mocks --test --test-timeout=15000 $(find test -name '*.spec.js')", - "test-coverage": "npm run build && node --experimental-test-module-mocks --enable-source-maps --test --experimental-test-coverage --test-timeout=15000 $(find test -name '*.spec.js')", - "test-lcov": "npm run build && mkdir -p coverage && node --experimental-test-module-mocks --enable-source-maps --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", + "test": "npm run build && node --experimental-test-module-mocks --import ./test/mocks/index.js --test --test-timeout=15000 $(find test -name '*.spec.js')", + "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')", + "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", "test-coverage-html": "npm run test-lcov; genhtml coverage/lcov.info --output-directory coverage/html --ignore-errors inconsistent,corrupt,format,mismatch && echo \"Coverage report: file://$(pwd)/coverage/html/index.html\"", "test-dev": "npm run test && npm run clean-js && npm run clean-typedefs && npm run clean-maps", "clean-typedefs": "find . -name '*.d.ts' -not -wholename '*node_modules*' -not -wholename '*generator*' -type f -delete", @@ -50,5 +50,20 @@ "typescript": "^7.0.2" }, "main": "index.js", - "types": "index.d.ts" + "types": "index.d.ts", + "type": "module", + "engines": { + "node": ">=22.12.0" + }, + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + }, + "./debug": { + "types": "./debug.d.ts", + "default": "./debug.js" + }, + "./package.json": "./package.json" + } } diff --git a/src/ClusterManager.ts b/src/ClusterManager.ts index eeff007..ebe2044 100644 --- a/src/ClusterManager.ts +++ b/src/ClusterManager.ts @@ -21,8 +21,11 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { IMessageQueueConnection, IServerInput } from './IMessageQueue'; import { randomUUID } from 'node:crypto'; +import { + type IMessageQueueConnection, + type IServerInput, +} from './IMessageQueue.js'; export interface ICluster { add: (server: IServerInput) => IMessageQueueConnection; diff --git a/src/ClusteredRedisQueue.ts b/src/ClusteredRedisQueue.ts index 52a7cc6..3c8732e 100644 --- a/src/ClusteredRedisQueue.ts +++ b/src/ClusteredRedisQueue.ts @@ -22,20 +22,20 @@ * to get commercial licensing options. */ import { EventEmitter } from 'node:events'; -import { InitializedCluster } from './ClusterManager'; -import { buildOptions, copyEventEmitter } from './helpers'; +import { type InitializedCluster } from './ClusterManager.js'; +import { buildOptions, copyEventEmitter } from './helpers/index.js'; import { DEFAULT_IMQ_OPTIONS, - EventMap, - ILogger, - IMessageQueue, - IMessageQueueConnection, + type EventMap, + type ILogger, + type IMessageQueue, + type IMessageQueueConnection, IMQMode, - IMQOptions, - IServerInput, - JsonObject, + type IMQOptions, + type IServerInput, + type JsonObject, RedisQueue, -} from '.'; +} from './index.js'; /** * Time (ms) send() waits for the first cluster server to become available diff --git a/src/IMessageQueue.ts b/src/IMessageQueue.ts index 6b8aa81..b746524 100644 --- a/src/IMessageQueue.ts +++ b/src/IMessageQueue.ts @@ -22,8 +22,8 @@ * to get commercial licensing options. */ import { EventEmitter } from 'node:events'; -import { IMQMode } from './IMQMode'; -import { ClusterManager } from './ClusterManager'; +import { IMQMode } from './IMQMode.js'; +import { ClusterManager } from './ClusterManager.js'; export { EventEmitter } from 'node:events'; diff --git a/src/RedisQueue.ts b/src/RedisQueue.ts index 4170a34..904d4e0 100644 --- a/src/RedisQueue.ts +++ b/src/RedisQueue.ts @@ -25,16 +25,16 @@ import { EventEmitter } from 'node:events'; import { randomUUID } from 'node:crypto'; import { hostname } from 'node:os'; import { - IMessageQueue, - IRedisClient, - JsonObject, - IMQOptions, - IMessage, - ILogger, + type IMessageQueue, + type IRedisClient, + type JsonObject, + type IMQOptions, + type IMessage, + type ILogger, IMQMode, - EventMap, + type EventMap, profile, -} from '.'; +} from './index.js'; import { buildOptions, escapeRegExp, @@ -43,8 +43,8 @@ import { sha1, unpack, envInt, -} from './helpers'; -import Redis from './redis'; +} from './helpers/index.js'; +import Redis from './redis.js'; const RX_CLIENT_NAME = /name=(\S+)/g; const RX_CLIENT_TEST = /:(reader|writer|watcher)/; diff --git a/src/UDPClusterManager.ts b/src/UDPClusterManager.ts index b67f222..e77ff43 100644 --- a/src/UDPClusterManager.ts +++ b/src/UDPClusterManager.ts @@ -23,8 +23,8 @@ */ import { join } from 'node:path'; import { Worker } from 'node:worker_threads'; -import { ClusterManager, ICluster } from './ClusterManager'; -import { ILogger, IServerInput } from './IMessageQueue'; +import { ClusterManager, type ICluster } from './ClusterManager.js'; +import { type ILogger, type IServerInput } from './IMessageQueue.js'; /** Shape of a message posted from the UDP worker thread */ interface WorkerMessage { @@ -270,7 +270,7 @@ export class UDPClusterManager extends ClusterManager { aliveTimeoutCorrection: this.options.aliveTimeoutCorrection, useAliveCheck: this.options.useAliveCheck, }; - const worker = new Worker(join(__dirname, './UDPWorker.js'), { + const worker = new Worker(join(import.meta.dirname, './UDPWorker.js'), { workerData, }); const workerKey = this.workerKey; diff --git a/src/UDPWorker.ts b/src/UDPWorker.ts index 13a7644..1658d6c 100644 --- a/src/UDPWorker.ts +++ b/src/UDPWorker.ts @@ -28,10 +28,10 @@ import { workerData, MessagePort, } from 'node:worker_threads'; -import { createSocket, Socket } from 'node:dgram'; +import { createSocket, type Socket } from 'node:dgram'; import { networkInterfaces } from 'node:os'; import { randomUUID } from 'node:crypto'; -import { UDPWorkerOptions } from './UDPClusterManager'; +import { type UDPWorkerOptions } from './UDPClusterManager.js'; enum MessageType { Up = 'up', diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 9b6fb2f..b9008a8 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -21,11 +21,11 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -export * from './buildOptions'; -export * from './sha1'; -export * from './randomInt'; -export * from './pack'; -export * from './unpack'; -export * from './escapeRegExp'; -export * from './copyEventEmitter'; -export * from './envInt'; +export * from './buildOptions.js'; +export * from './sha1.js'; +export * from './randomInt.js'; +export * from './pack.js'; +export * from './unpack.js'; +export * from './escapeRegExp.js'; +export * from './copyEventEmitter.js'; +export * from './envInt.js'; diff --git a/src/index.ts b/src/index.ts index c4f6406..24cf69a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,10 +19,10 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -export * from './profile'; -export * from './redis'; -export * from './IMQMode'; -export * from './IMessageQueue'; -export * from './RedisQueue'; -export * from './ClusteredRedisQueue'; -export * from './UDPClusterManager'; +export * from './profile.js'; +export * from './redis.js'; +export * from './IMQMode.js'; +export * from './IMessageQueue.js'; +export * from './RedisQueue.js'; +export * from './ClusteredRedisQueue.js'; +export * from './UDPClusterManager.js'; diff --git a/src/profile.ts b/src/profile.ts index ae4eb69..47a3a9c 100644 --- a/src/profile.ts +++ b/src/profile.ts @@ -21,7 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { ILogger } from '.'; +import { type ILogger } from './index.js'; export enum LogLevel { LOG = 'log', diff --git a/src/redis.ts b/src/redis.ts index 89db614..8ad0943 100644 --- a/src/redis.ts +++ b/src/redis.ts @@ -21,7 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import Redis from 'ioredis'; +import { Redis } from 'ioredis'; /** * Extends the default Redis type to allow dynamic properties access on it diff --git a/test/helpers/index.ts b/test/helpers/index.ts index 941666d..604e5ff 100644 --- a/test/helpers/index.ts +++ b/test/helpers/index.ts @@ -21,4 +21,4 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -export * from './makeLogger'; +export * from './makeLogger.js'; diff --git a/test/helpers/makeLogger.ts b/test/helpers/makeLogger.ts index bbbb5bb..49f937c 100644 --- a/test/helpers/makeLogger.ts +++ b/test/helpers/makeLogger.ts @@ -21,7 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { ILogger } from '../../src'; +import { type ILogger } from '../../src/index.js'; /** * Builds a fresh no-op logger suitable for spying on in unit tests. diff --git a/test/mocks/dgram.ts b/test/mocks/dgram.ts index 207fdb1..922b5f9 100644 --- a/test/mocks/dgram.ts +++ b/test/mocks/dgram.ts @@ -22,7 +22,7 @@ * to get commercial licensing options. */ import { EventEmitter } from 'node:events'; -import { mockBuiltin } from './mockBuiltin'; +import { mockBuiltin } from './mockBuiltin.js'; class Socket extends EventEmitter { public bindArgs: any[] = []; diff --git a/test/mocks/index.ts b/test/mocks/index.ts index 9bb7bdf..4b64221 100644 --- a/test/mocks/index.ts +++ b/test/mocks/index.ts @@ -21,8 +21,8 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -export * from './mockBuiltin'; -export * from './redis'; -export * from './logger'; -export * from './dgram'; -export * from './os'; +export * from './mockBuiltin.js'; +export * from './redis.js'; +export * from './logger.js'; +export * from './dgram.js'; +export * from './os.js'; diff --git a/test/mocks/mockBuiltin.ts b/test/mocks/mockBuiltin.ts index 65c9cdc..e39c68e 100644 --- a/test/mocks/mockBuiltin.ts +++ b/test/mocks/mockBuiltin.ts @@ -21,18 +21,26 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { mock, MockModuleOptions } from 'node:test'; +import { mock, type MockModuleOptions } from 'node:test'; /** - * Builds the `mock.module()` options for the running Node version. Node 24+ - * takes the mock's exports through the `exports` option (a `default` key is - * the default export; for CJS consumers `module.exports` becomes that value), - * and deprecated the earlier `defaultExport`/`namedExports` pair. Node 22 only - * understands the earlier pair, so the same exports shape is translated there. + * Builds the `mock.module()` options from a single exports shape (`default` + * key being the default export). The long-standing `defaultExport` / + * `namedExports` pair is used on every Node version: newer 24.x lines emit a + * deprecation warning in favor of an `exports` option, but that option's + * behavior has already shifted between 24.x releases (module mocking is + * experimental), while the pair keeps working consistently on 22.x and 24.x. * - * `cache: true` keeps mock-require's semantics: every `require()` of the - * mocked specifier returns the same module instance, so a test may patch a - * property in place and the code under test observes the change. + * Note: named exports are merged onto the default export for CJS consumers, + * which requires the default export to be a plain object (a bare class as + * the default with named exports alongside is rejected by the runtime). + * + * `cache: false` is required: as of the Node 24.17/24.18 module-loader + * changes, `cache: true` mock modules expose their export names with the + * values never bound (importing `{ Redis }` yields undefined). Instance + * caching is not load-bearing for these mocks anyway — every piece of + * shared state lives on the exported references themselves (for example + * RedisClientMock statics), so each re-evaluation serves the same objects. * * @param {Record} exports - mock exports, `default` key being * the default export @@ -41,13 +49,8 @@ import { mock, MockModuleOptions } from 'node:test'; export function moduleMockOptions( exports: Record, ): MockModuleOptions { - if (+process.versions.node.split('.')[0] >= 24) { - // typings (@types/node 24.x) lag the runtime's `exports` option - return { cache: true, exports } as MockModuleOptions; - } - const { default: defaultExport, ...namedExports } = exports; - const options: MockModuleOptions = { cache: true }; + const options: MockModuleOptions = { cache: false }; if (defaultExport !== undefined) { options.defaultExport = defaultExport; diff --git a/test/mocks/os.ts b/test/mocks/os.ts index 4360aff..d3484f2 100644 --- a/test/mocks/os.ts +++ b/test/mocks/os.ts @@ -20,7 +20,7 @@ * to get commercial licensing options. */ import os from 'node:os'; -import { mockBuiltin } from './mockBuiltin'; +import { mockBuiltin } from './mockBuiltin.js'; export const networkInterfaces = () => { return { diff --git a/test/mocks/redis.ts b/test/mocks/redis.ts index a3a9fb7..f27d976 100644 --- a/test/mocks/redis.ts +++ b/test/mocks/redis.ts @@ -24,7 +24,7 @@ import { mock } from 'node:test'; import { EventEmitter } from 'node:events'; import { createHash, Hash } from 'node:crypto'; -import { moduleMockOptions } from './mockBuiltin'; +import { moduleMockOptions } from './mockBuiltin.js'; function sha1(str: string) { let sha: Hash = createHash('sha1'); @@ -376,10 +376,9 @@ export class RedisClientMock extends EventEmitter { } } -// Native module mocking cannot merge named exports onto a class default -// export, so the whole transpiled-ESM-shaped namespace object is registered -// as the module's default export (for CJS consumers it IS what -// `require('ioredis')` returns) — exactly the object mock-require served here. +// The mock must serve both worlds: ESM sources bind the named `Redis` +// export directly, while CommonJS-style consumers read properties off the +// default (`module.exports`) object — so both shapes are registered. mock.module( 'ioredis', moduleMockOptions({ @@ -388,6 +387,7 @@ mock.module( default: RedisClientMock, Redis: RedisClientMock, }, + Redis: RedisClientMock, }), ); diff --git a/test/unit/ClusterManager.spec.ts b/test/unit/ClusterManager.spec.ts index e3f96c1..ab623ba 100644 --- a/test/unit/ClusterManager.spec.ts +++ b/test/unit/ClusterManager.spec.ts @@ -4,10 +4,13 @@ * I'm Queue Software Project * Copyright (C) 2025 imqueue.com */ -import '../mocks'; +import '../mocks/index.js'; import { describe, it, afterEach, mock } from 'node:test'; import assert from 'node:assert/strict'; -import { ClusterManager, InitializedCluster } from '../../src/ClusterManager'; +import { + ClusterManager, + type InitializedCluster, +} from '../../src/ClusterManager.js'; class TestClusterManager extends ClusterManager { public destroyed = false; diff --git a/test/unit/ClusteredRedisQueue.spec.ts b/test/unit/ClusteredRedisQueue.spec.ts index 8266318..52359d8 100644 --- a/test/unit/ClusteredRedisQueue.spec.ts +++ b/test/unit/ClusteredRedisQueue.spec.ts @@ -22,11 +22,11 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { logger } from '../mocks'; -import { describe, it, afterEach, mock, Mock } from 'node:test'; +import { logger } from '../mocks/index.js'; +import { describe, it, afterEach, mock, type Mock } from 'node:test'; import assert from 'node:assert/strict'; -import { ClusteredRedisQueue, RedisQueue } from '../../src'; -import { ClusterManager } from '../../src/ClusterManager'; +import { ClusteredRedisQueue, RedisQueue } from '../../src/index.js'; +import { ClusterManager } from '../../src/ClusterManager.js'; process.setMaxListeners(100); diff --git a/test/unit/IMessageQueue.spec.ts b/test/unit/IMessageQueue.spec.ts index b592ecd..df1758b 100644 --- a/test/unit/IMessageQueue.spec.ts +++ b/test/unit/IMessageQueue.spec.ts @@ -19,10 +19,10 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import '../mocks'; +import '../mocks/index.js'; import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { EventEmitter as IMQEventEmitter } from '../../src'; +import { EventEmitter as IMQEventEmitter } from '../../src/index.js'; import { EventEmitter as NodeEventEmitter } from 'node:events'; describe('IMessageQueue EventEmitter re-export', () => { diff --git a/test/unit/RedisQueue.spec.ts b/test/unit/RedisQueue.spec.ts index 3f4e53e..b468b97 100644 --- a/test/unit/RedisQueue.spec.ts +++ b/test/unit/RedisQueue.spec.ts @@ -25,15 +25,22 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import '../mocks'; +import '../mocks/index.js'; import assert from 'node:assert/strict'; import { randomUUID as uuid } from 'node:crypto'; -import { describe, it, beforeEach, afterEach, mock, Mock } from 'node:test'; -import Redis from 'ioredis'; -import { RedisQueue, IMQMode } from '../../src'; -import { escapeRegExp, sha1 } from '../../src/helpers'; -import { makeLogger } from '../helpers'; -import { logger, RedisClientMock } from '../mocks'; +import { + describe, + it, + beforeEach, + afterEach, + mock, + type Mock, +} from 'node:test'; +import { Redis } from 'ioredis'; +import { RedisQueue, IMQMode } from '../../src/index.js'; +import { escapeRegExp, sha1 } from '../../src/helpers/index.js'; +import { makeLogger } from '../helpers/index.js'; +import { logger, RedisClientMock } from '../mocks/index.js'; process.setMaxListeners(100); diff --git a/test/unit/UDPClusterManager.spec.ts b/test/unit/UDPClusterManager.spec.ts index 19c9461..94abfe6 100644 --- a/test/unit/UDPClusterManager.spec.ts +++ b/test/unit/UDPClusterManager.spec.ts @@ -24,10 +24,10 @@ * UDPClusterManager unit tests (merged: main suite, missing branches coverage * and destroyWorker() behavior). */ -import '../mocks'; -import { describe, it, afterEach, mock, Mock } from 'node:test'; +import '../mocks/index.js'; +import { describe, it, afterEach, mock, type Mock } from 'node:test'; import assert from 'node:assert/strict'; -import { UDPClusterManager } from '../../src'; +import { UDPClusterManager } from '../../src/index.js'; const testMessageUp = { name: 'IMQUnitTest', diff --git a/test/unit/UDPWorker.spec.ts b/test/unit/UDPWorker.spec.ts index 79a13fc..e18365d 100644 --- a/test/unit/UDPWorker.spec.ts +++ b/test/unit/UDPWorker.spec.ts @@ -21,7 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import '../mocks'; +import '../mocks/index.js'; import { describe, it, mock } from 'node:test'; import assert from 'node:assert/strict'; import { EventEmitter } from 'node:events'; @@ -29,7 +29,7 @@ import { EventEmitter } from 'node:events'; // `import * as`, which esModuleInterop turns into a copy), so patching // os.networkInterfaces below is observed by the code under test import os = require('node:os'); -import { UDPWorker } from '../../src/UDPWorker'; +import { UDPWorker } from '../../src/UDPWorker.js'; const OPTIONS: any = { port: 63999, diff --git a/test/unit/helpers/buildOptions.spec.ts b/test/unit/helpers/buildOptions.spec.ts index c097631..eadb53a 100644 --- a/test/unit/helpers/buildOptions.spec.ts +++ b/test/unit/helpers/buildOptions.spec.ts @@ -21,7 +21,7 @@ */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { buildOptions } from '../../../src/helpers'; +import { buildOptions } from '../../../src/helpers/index.js'; interface Options { host: string; diff --git a/test/unit/helpers/copyEventEmitter.spec.ts b/test/unit/helpers/copyEventEmitter.spec.ts index b1e151b..3f702df 100644 --- a/test/unit/helpers/copyEventEmitter.spec.ts +++ b/test/unit/helpers/copyEventEmitter.spec.ts @@ -19,7 +19,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import '../../mocks'; +import '../../mocks/index.js'; import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { EventEmitter } from 'node:events'; @@ -27,7 +27,8 @@ import { EventEmitter } from 'node:events'; // which esModuleInterop turns into a copy), so patching util.inspect below // is observed by the code under test import util = require('node:util'); -import { copyEventEmitter } from '../../../src/helpers'; +import { syncBuiltinESMExports } from 'node:module'; +import { copyEventEmitter } from '../../../src/helpers/index.js'; describe('copyEventEmitter()', () => { const eventName = 'test'; @@ -103,11 +104,13 @@ describe('copyEventEmitter()', () => { } return originalInspect(obj); }; + syncBuiltinESMExports(); copyEventEmitter(source, target); // Restore original inspect (util as any).inspect = originalInspect; + syncBuiltinESMExports(); assert.equal(target.listenerCount(eventName), 1); }); @@ -150,12 +153,14 @@ describe('copyEventEmitter()', () => { } return originalInspect(obj); }; + syncBuiltinESMExports(); source.on(eventName, mockListener as any); copyEventEmitter(source, target); // Restore original inspect (util as any).inspect = originalInspect; + syncBuiltinESMExports(); assert.equal(target.listenerCount(eventName), 1); }); @@ -173,12 +178,14 @@ describe('copyEventEmitter()', () => { } return originalInspect(obj); }; + syncBuiltinESMExports(); source.on(eventName, mockListener as any); copyEventEmitter(source, target); // Restore original inspect (util as any).inspect = originalInspect; + syncBuiltinESMExports(); assert.equal(target.listenerCount(eventName), 1); }); @@ -202,12 +209,14 @@ describe('copyEventEmitter()', () => { } return originalInspect(obj); }; + syncBuiltinESMExports(); source.on(eventName, mockListener as any); copyEventEmitter(source, target); // Restore original inspect (util as any).inspect = originalInspect; + syncBuiltinESMExports(); // Ensure the listener was attached via once() and is callable exactly once assert.equal(target.listenerCount(eventName), 1); @@ -237,11 +246,13 @@ describe('copyEventEmitter()', () => { } return originalInspect(obj); }; + syncBuiltinESMExports(); copyEventEmitter(source as any, target as any); // Restore original inspect (util as any).inspect = originalInspect; + syncBuiltinESMExports(); assert.equal(onceCalls.length, 1); assert.equal(onceCalls[0][0], eventName); diff --git a/test/unit/helpers/envInt.spec.ts b/test/unit/helpers/envInt.spec.ts index ba3c768..af1c1d1 100644 --- a/test/unit/helpers/envInt.spec.ts +++ b/test/unit/helpers/envInt.spec.ts @@ -21,7 +21,7 @@ */ import { afterEach, describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { envInt } from '../../../src/helpers'; +import { envInt } from '../../../src/helpers/index.js'; const VAR = 'IMQ_ENV_INT_TEST'; diff --git a/test/unit/helpers/escapeRegExp.spec.ts b/test/unit/helpers/escapeRegExp.spec.ts index bd103bc..882f25c 100644 --- a/test/unit/helpers/escapeRegExp.spec.ts +++ b/test/unit/helpers/escapeRegExp.spec.ts @@ -21,7 +21,7 @@ */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { escapeRegExp } from '../../../src/helpers'; +import { escapeRegExp } from '../../../src/helpers/index.js'; describe('escapeRegExp()', () => { it('should leave plain alphanumeric strings unchanged', () => { diff --git a/test/unit/helpers/pack.spec.ts b/test/unit/helpers/pack.spec.ts index 58bea3a..69cba4d 100644 --- a/test/unit/helpers/pack.spec.ts +++ b/test/unit/helpers/pack.spec.ts @@ -22,7 +22,7 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { gunzipSync } from 'node:zlib'; -import { pack } from '../../../src/helpers'; +import { pack } from '../../../src/helpers/index.js'; describe('pack()', () => { it('should return a string', () => { diff --git a/test/unit/helpers/randomInt.spec.ts b/test/unit/helpers/randomInt.spec.ts index 1b9e703..8cdd8b2 100644 --- a/test/unit/helpers/randomInt.spec.ts +++ b/test/unit/helpers/randomInt.spec.ts @@ -21,7 +21,7 @@ */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { randomInt } from '../../../src/helpers'; +import { randomInt } from '../../../src/helpers/index.js'; describe('randomInt()', () => { it('should always return an integer within the inclusive range', () => { diff --git a/test/unit/helpers/sha1.spec.ts b/test/unit/helpers/sha1.spec.ts index d28e94f..3a7445e 100644 --- a/test/unit/helpers/sha1.spec.ts +++ b/test/unit/helpers/sha1.spec.ts @@ -21,7 +21,7 @@ */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { sha1 } from '../../../src/helpers'; +import { sha1 } from '../../../src/helpers/index.js'; describe('sha1()', () => { it('should return a 40-character lowercase hex string', () => { diff --git a/test/unit/helpers/unpack.spec.ts b/test/unit/helpers/unpack.spec.ts index f90c240..5bc1ded 100644 --- a/test/unit/helpers/unpack.spec.ts +++ b/test/unit/helpers/unpack.spec.ts @@ -21,7 +21,7 @@ */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { pack, unpack } from '../../../src/helpers'; +import { pack, unpack } from '../../../src/helpers/index.js'; describe('unpack()', () => { it('should reverse pack() for objects', () => { diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts index 8123ae2..a68c823 100644 --- a/test/unit/index.spec.ts +++ b/test/unit/index.spec.ts @@ -21,10 +21,10 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import '../mocks'; +import '../mocks/index.js'; import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import IMQ, { RedisQueue, ClusteredRedisQueue } from '../..'; +import IMQ, { RedisQueue, ClusteredRedisQueue } from '../../index.js'; describe('IMQ', () => { it('should be a class', () => { diff --git a/test/unit/profile.spec.ts b/test/unit/profile.spec.ts index 8e6d364..bf18b6f 100644 --- a/test/unit/profile.spec.ts +++ b/test/unit/profile.spec.ts @@ -22,32 +22,39 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import '../mocks'; -import { describe, it, beforeEach, afterEach, mock, Mock } from 'node:test'; +import '../mocks/index.js'; +import { + describe, + it, + beforeEach, + afterEach, + mock, + type Mock, +} from 'node:test'; import assert from 'node:assert/strict'; import { profile, - ILogger, + type ILogger, verifyLogLevel, LogLevel, - DebugInfoOptions, -} from '../..'; -import { logger } from '../mocks'; + type DebugInfoOptions, +} from '../../index.js'; +import { logger } from '../mocks/index.js'; + +let reloadCounter = 0; /** * Loads a fresh, uncached copy of a module so that its module-scope state - * (e.g. env-derived constants) is re-evaluated. Replaces mock-require's - * reRequire(). + * (e.g. env-derived constants) is re-evaluated. The ES module registry is + * immutable, so freshness is achieved with a unique query string per load. * - * @param {string} id - module id, relative to this file - * @returns {any} - the freshly loaded module + * @param {string} id - module id, relative to this file, without extension + * @returns {Promise} - the freshly loaded module namespace */ -function reRequire(id: string): any { - const resolved = require.resolve(id); - - delete require.cache[resolved]; +function reRequire(id: string): Promise { + const href = new URL(`${id}.js`, import.meta.url).href; - return require(resolved); + return import(`${href}?reload=${++reloadCounter}`); } const BIG_INT_SUPPORT = (() => { @@ -131,32 +138,32 @@ describe('profile()', () => { delete process.env.IMQ_LOG_TIME_FORMAT; }); - it('should be a function', () => { + it('should be a function', async () => { assert.equal(typeof profile, 'function'); }); - it('should be decorator factory', () => { + it('should be decorator factory', async () => { assert.equal(typeof profile(), 'function'); }); - it('should pass decorated method args with no change', () => { + it('should pass decorated method args with no change', async () => { assert.deepEqual( new ProfiledClass().decoratedMethod(1, 2, 3), [1, 2, 3], ); }); - it('should log time if enabled', () => { + it('should log time if enabled', async () => { new ProfiledClassTimed().decoratedMethod(); assert.equal(log.mock.callCount(), 1); }); - it('should log args if enabled', () => { + it('should log args if enabled', async () => { new ProfiledClassArgued().decoratedMethod(); assert.equal(log.mock.callCount(), 1); }); - it('should log time and args if both enabled', () => { + it('should log time and args if both enabled', async () => { new ProfiledClassTimedAndArgued().decoratedMethod(); assert.equal(log.mock.callCount(), 2); }); @@ -167,14 +174,14 @@ describe('profile()', () => { }); describe('verifyLogLevel()', () => { - it('should return valid log levels as is', () => { + it('should return valid log levels as is', async () => { assert.equal(verifyLogLevel(LogLevel.LOG), LogLevel.LOG); assert.equal(verifyLogLevel(LogLevel.INFO), LogLevel.INFO); assert.equal(verifyLogLevel(LogLevel.WARN), LogLevel.WARN); assert.equal(verifyLogLevel(LogLevel.ERROR), LogLevel.ERROR); }); - it('should return default log level on invalid value', () => { + it('should return default log level on invalid value', async () => { assert.equal(verifyLogLevel('invalid'), LogLevel.INFO); }); }); @@ -192,28 +199,28 @@ describe('profile()', () => { logLevel: LogLevel.LOG, }; - it('should log time in microseconds by default', () => { - const { logDebugInfo } = reRequire('../../src/profile'); + it('should log time in microseconds by default', async () => { + const { logDebugInfo } = await reRequire('../../src/profile'); logDebugInfo(baseOptions); assert.equal(calledWithMatch(log, /μs/), true); }); - it('should log time in milliseconds', () => { + it('should log time in milliseconds', async () => { process.env.IMQ_LOG_TIME_FORMAT = 'milliseconds'; - const { logDebugInfo } = reRequire('../../src/profile'); + const { logDebugInfo } = await reRequire('../../src/profile'); logDebugInfo(baseOptions); assert.equal(calledWithMatch(log, /ms/), true); }); - it('should log time in seconds', () => { + it('should log time in seconds', async () => { process.env.IMQ_LOG_TIME_FORMAT = 'seconds'; - const { logDebugInfo } = reRequire('../../src/profile'); + const { logDebugInfo } = await reRequire('../../src/profile'); logDebugInfo(baseOptions); assert.equal(calledWithMatch(log, /sec/), true); }); - it('should handle circular references in args', () => { - const { logDebugInfo } = reRequire('../../src/profile'); + it('should handle circular references in args', async () => { + const { logDebugInfo } = await reRequire('../../src/profile'); const a: any = { b: 1 }; const b = { a }; a.b = b; @@ -221,8 +228,8 @@ describe('profile()', () => { assert.equal(error.mock.callCount(), 0); }); - it('should not log when logger method is missing', () => { - const { logDebugInfo } = reRequire('../../src/profile'); + it('should not log when logger method is missing', async () => { + const { logDebugInfo } = await reRequire('../../src/profile'); const dummyLogger: any = { error: logger.error.bind(logger) }; logDebugInfo({ ...baseOptions, @@ -232,8 +239,8 @@ describe('profile()', () => { assert.equal(log.mock.callCount(), 0); }); - it('should handle JSON.stringify errors', () => { - const { logDebugInfo } = reRequire('../../src/profile'); + it('should handle JSON.stringify errors', async () => { + const { logDebugInfo } = await reRequire('../../src/profile'); const badJson = { toJSON: () => { throw new Error('bad json'); @@ -251,8 +258,8 @@ describe('profile.ts additional branches', () => { delete (process as any).env.IMQ_LOG_TIME_FORMAT; }); - it('logDebugInfo: should not attempt to call missing log method (no-op path)', () => { - const { logDebugInfo, LogLevel } = reRequire('../../src/profile'); + it('logDebugInfo: should not attempt to call missing log method (no-op path)', async () => { + const { logDebugInfo, LogLevel } = await reRequire('../../src/profile'); const fakeLogger: any = { // intentionally no 'log' or 'info' method for selected level error: mock.fn(), @@ -272,8 +279,8 @@ describe('profile.ts additional branches', () => { assert.equal(fakeLogger.error.mock.callCount() > 0, false); }); - it('logDebugInfo: should call logger.error on JSON.stringify error (BigInt arg)', () => { - const { logDebugInfo, LogLevel } = reRequire('../../src/profile'); + it('logDebugInfo: should call logger.error on JSON.stringify error (BigInt arg)', async () => { + const { logDebugInfo, LogLevel } = await reRequire('../../src/profile'); const fakeLogger: any = { error: mock.fn(), }; @@ -294,7 +301,7 @@ describe('profile.ts additional branches', () => { }); describe('profile decorator extra branches', () => { - it('should return early via original.apply(target, ...) when both debug flags are false and this is undefined', () => { + it('should return early via original.apply(target, ...) when both debug flags are false and this is undefined', async () => { class T1 { @profile({ enableDebugTime: false, @@ -311,7 +318,7 @@ describe('profile decorator extra branches', () => { assert.deepEqual(res, [1, 2, 3]); }); - it('should execute debug path with (this || target) picking target and logLevel fallback to IMQ_LOG_LEVEL', () => { + it('should execute debug path with (this || target) picking target and logLevel fallback to IMQ_LOG_LEVEL', async () => { class T2 { // no logger on prototype; calling with undefined this picks target @profile({ @@ -356,7 +363,7 @@ describe('profile() async rejection path', () => { }); describe('profile() legacy decorator signature', () => { - it('wraps via the legacy (target, key, descriptor) form', () => { + it('wraps via the legacy (target, key, descriptor) form', async () => { const decorator = profile({ enableDebugTime: true }); const original = function (): number { return 42; @@ -370,7 +377,7 @@ describe('profile() legacy decorator signature', () => { assert.equal(descriptor.value(), 42); }); - it('resolves the class name from a function target', () => { + it('resolves the class name from a function target', async () => { const decorator = profile({ enableDebugTime: true }); const descriptor: any = { value: function (): number { diff --git a/tsconfig.json b/tsconfig.json index e3bd55a..8b0e4ff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,14 @@ { "compilerOptions": { - "target": "es2023", - "lib": ["es2023"], + "target": "es2024", + "lib": ["es2024"], "moduleDetection": "force", "module": "nodenext", "moduleResolution": "nodenext", "resolveJsonModule": true, "esModuleInterop": true, + "verbatimModuleSyntax": true, "types": ["node"], @@ -19,6 +20,8 @@ "removeComments": false, "newLine": "lf", + "skipLibCheck": true, + "strict": true, "noImplicitOverride": true, "noFallthroughCasesInSwitch": true