From 59c8b1e98e5dc6e9a51f98414ba102fbde35724e Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 9 Jul 2026 19:21:26 +0200 Subject: [PATCH 1/5] feat!: migrate to native ES modules - package.json: type=module, exports map, engines node>=22.12, --experimental-test-module-mocks + --import mock preload in test scripts - explicit .js extensions on all relative imports - IMQClient: tsconfig.json read via import.meta.url; typescript compiler resolved and generated CommonJS clients loaded via createRequire() (generated clients still transpile to CJS for the vm sandbox) - IMQCache: static built-in adapter registry replaces synchronous dynamic require; custom adapters still registrable by class/instance - decorators/cache: imports from defining modules to break a circular-import TDZ that CJS evaluation order used to tolerate - tests: ioredis/node:fs mocks expose named exports for ESM bindings; builtin method patches published via syncBuiltinESMExports(); fresh-module reloads via query-busted dynamic import; ESM-invalid module-scope this removed; ioredis added as explicit devDependency - @imqueue/core consumed as vendored ESM tarball (vendor/) for the experiment; replace with the published ^4.0.0 when core ships ESM BREAKING CHANGE: package is ESM-only; CJS consumers require Node >=22.12 (require(esm)) and TypeScript >=5.8 to type-check --- index.ts | 2 +- package-lock.json | 15 +++-- package.json | 20 +++++-- src/IMQCache.ts | 22 ++++++-- src/IMQClient.ts | 24 ++++---- src/IMQLock.ts | 2 +- src/IMQRPCOptions.ts | 8 +-- src/IMQRPCRequest.ts | 2 +- src/IMQRPCResponse.ts | 2 +- src/IMQRequestContext.ts | 4 +- src/IMQService.ts | 10 ++-- src/cache/RedisCache.ts | 4 +- src/cache/index.ts | 4 +- src/decorators/cache.ts | 7 ++- src/decorators/classType.ts | 2 +- src/decorators/expose.ts | 6 +- src/decorators/index.ts | 18 +++--- src/decorators/indexed.ts | 4 +- src/decorators/lock.ts | 4 +- src/decorators/property.ts | 2 +- src/helpers/index.ts | 8 +-- src/index.ts | 28 +++++----- test/IMQCache.spec.ts | 4 +- test/IMQClient.callTimeout.spec.ts | 6 +- test/IMQClient.console.logger.spec.ts | 4 +- test/IMQClient.extra.spec.ts | 6 +- .../IMQClient.generator.trailing.args.spec.ts | 8 ++- test/IMQClient.generator.types.spec.ts | 2 +- test/IMQClient.methods.spec.ts | 6 +- test/IMQClient.singleQueue.spec.ts | 6 +- test/IMQClient.spec.ts | 4 +- test/IMQClient.transpile.error.spec.ts | 14 ++++- test/IMQDelay.spec.ts | 4 +- test/IMQLock.spec.ts | 6 +- test/IMQLock.stringify.metadata.spec.ts | 2 +- test/IMQMetadata.spec.ts | 4 +- test/IMQService.afterCall.spec.ts | 9 ++- test/IMQService.console.logger.spec.ts | 2 +- test/IMQService.hooks.spec.ts | 9 ++- test/IMQService.logger.fallback.spec.ts | 2 +- test/IMQService.replyFailure.spec.ts | 4 +- test/IMQService.signal.publish.spec.ts | 2 +- test/IMQService.spec.ts | 7 ++- test/cache/RedisCache.errors.spec.ts | 2 +- test/cache/RedisCache.purge.promise.spec.ts | 2 +- test/cache/RedisCache.race.spec.ts | 8 +-- test/cache/RedisCache.spec.ts | 4 +- test/coverage-extra.spec.ts | 6 +- test/decorators/cache.spec.ts | 4 +- test/decorators/expose.spec.ts | 4 +- test/decorators/indexed.spec.ts | 4 +- test/decorators/lock.skipArgs.spec.ts | 4 +- test/decorators/lock.spec.ts | 4 +- test/decorators/logged.spec.ts | 2 +- test/decorators/property.spec.ts | 4 +- test/decorators/remote.spec.ts | 4 +- test/helpers/fs.spec.ts | 23 +++++--- test/helpers/os-uuid.spec.ts | 55 ++++++++++++------- test/helpers/pid.spec.ts | 4 +- test/helpers/signature.spec.ts | 4 +- test/mocks/index.ts | 6 +- test/mocks/redis.ts | 30 +++++----- test/signals.cleanup.spec.ts | 6 +- 63 files changed, 291 insertions(+), 198 deletions(-) diff --git a/index.ts b/index.ts index 2cc6eab..702a5d7 100644 --- a/index.ts +++ b/index.ts @@ -19,5 +19,5 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -export * from './src'; +export * from './src/index.js'; export * from '@imqueue/core'; diff --git a/package-lock.json b/package-lock.json index 29737f8..70877fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,23 +9,30 @@ "version": "3.1.1", "license": "GPL-3.0-only", "dependencies": { - "@imqueue/core": "^3.1.0", + "@imqueue/core": "file:vendor/imqueue-core-3.1.1.tgz", "acorn": "^8.17.0", "typescript": "^7.0.2" }, "devDependencies": { "@types/node": "^24.9.1", + "ioredis": "^5.11.1", "oxfmt": "0.57.0", "oxlint": "1.72.0" + }, + "engines": { + "node": ">=22.12.0" } }, "node_modules/@imqueue/core": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@imqueue/core/-/core-3.1.0.tgz", - "integrity": "sha512-lz8AQU9RgRW48AXfAKthYXO3KqU5nvhjhbqQwu34muY7TiJilIkIVGsBSQp58lI4HjOgk3wLUrnbH9rJNJI4bQ==", + "version": "3.1.1", + "resolved": "file:vendor/imqueue-core-3.1.1.tgz", + "integrity": "sha512-lrjgDaIh4m0iASgQSKk8Y6Pp6OLDFPW255f9FR3Ne0qwoksOLNdQpfPX/vGNddEbbEfsRZ8MiGYJ60C8Rjju7g==", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.11.1" + }, + "engines": { + "node": ">=22.12.0" } }, "node_modules/@ioredis/commands": { diff --git a/package.json b/package.json index a2f9d2b..88f39b6 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "lint": "oxlint", "format": "oxfmt \"index.ts\" \"debug.ts\" \"src/**/*.ts\" \"test/**/*.ts\"", "format:check": "oxfmt --check \"index.ts\" \"debug.ts\" \"src/**/*.ts\" \"test/**/*.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", @@ -43,12 +43,13 @@ "author": "imqueue.com (https://imqueue.com)", "license": "GPL-3.0-only", "dependencies": { - "@imqueue/core": "^3.1.0", + "@imqueue/core": "file:vendor/imqueue-core-3.1.1.tgz", "acorn": "^8.17.0", "typescript": "^7.0.2" }, "devDependencies": { "@types/node": "^24.9.1", + "ioredis": "^5.11.1", "oxfmt": "0.57.0", "oxlint": "1.72.0" }, @@ -58,6 +59,15 @@ ".": { "types": "./index.d.ts", "default": "./index.js" - } + }, + "./debug": { + "types": "./debug.d.ts", + "default": "./debug.js" + }, + "./package.json": "./package.json" + }, + "type": "module", + "engines": { + "node": ">=22.12.0" } } diff --git a/src/IMQCache.ts b/src/IMQCache.ts index 792b1ad..451ce5c 100644 --- a/src/IMQCache.ts +++ b/src/IMQCache.ts @@ -21,7 +21,15 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { ICache, ICacheAdapter, ICacheConstructor } from '.'; +import { RedisCache } from './cache/index.js'; +import { ICache, ICacheAdapter, ICacheConstructor } from './index.js'; + +// ES modules provide no synchronous dynamic loading, so the built-in cache +// adapters registrable by name are enumerated statically; custom adapters +// are still registrable by class or instance +const BUILT_IN_ADAPTERS: { [name: string]: ICacheConstructor } = { + RedisCache: RedisCache as unknown as ICacheConstructor, +}; /** * Generic cache registry @@ -46,9 +54,15 @@ export class IMQCache { if (typeof adapter === 'string') { if (!self.adapters[adapter]) { - self.adapters[adapter] = ( - new (require(`${__dirname}/cache/${adapter}.js`)[adapter])() - ); + const Adapter = BUILT_IN_ADAPTERS[adapter]; + + if (!Adapter) { + throw new TypeError( + `IMQCache: unknown cache adapter requested: ${adapter}`, + ); + } + + self.adapters[adapter] = new Adapter() as unknown as ICache; } } else { if (!self.adapters[(adapter as ICacheConstructor).name]) { diff --git a/src/IMQClient.ts b/src/IMQClient.ts index 0d98e10..46d9c0a 100644 --- a/src/IMQClient.ts +++ b/src/IMQClient.ts @@ -40,7 +40,7 @@ import { IMQMetadata, BEFORE_HOOK_ERROR, AFTER_HOOK_ERROR, -} from '.'; +} from './index.js'; import { pid, forgetPid, @@ -49,7 +49,7 @@ import { mkdir, writeFile, SIGNALS, -} from './helpers'; +} from './helpers/index.js'; import { EventEmitter } from 'node:events'; import { Script } from 'node:vm'; import { spawnSync } from 'node:child_process'; @@ -60,16 +60,20 @@ import { existsSync, rmSync, } from 'node:fs'; +import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; import { join, dirname } from 'node:path'; -import { IMQBeforeCall, IMQAfterCall } from './IMQRPCOptions'; +import { IMQBeforeCall, IMQAfterCall } from './IMQRPCOptions.js'; -// Loaded via require (not a static import) so that consumers which type-check +// CommonJS require scoped to this module: resolves the typescript package +// and loads generated CommonJS client modules from the ESM host +const cjsRequire = createRequire(import.meta.url); + +// Read as a file (not a static import) so that consumers which type-check // this source through a file: link do not need `resolveJsonModule` enabled. -const tsOptions = require('../tsconfig.json').compilerOptions as Record< - string, - unknown ->; +const tsOptions = JSON.parse( + readFileSync(new URL('../tsconfig.json', import.meta.url), 'utf8'), +).compilerOptions as Record; const RX_SEMICOLON: RegExp = /;+$/g; /** @@ -815,7 +819,7 @@ function transpileClient(src: string): string { // blocks `typescript/lib/*`, so build a raw path that Node runs // directly) const tscJs = join( - dirname(require.resolve('typescript/package.json')), + dirname(cjsRequire.resolve('typescript/package.json')), 'lib', 'tsc.js', ); @@ -865,7 +869,7 @@ async function compile( if (options.compile) { const script = new Script(js); - const context = { exports: {}, require }; + const context = { exports: {}, require: cjsRequire }; script.runInNewContext(context, { filename: jsFile }); diff --git a/src/IMQLock.ts b/src/IMQLock.ts index 693b372..842e69e 100644 --- a/src/IMQLock.ts +++ b/src/IMQLock.ts @@ -41,7 +41,7 @@ export interface IMQLockMetadata { * * @example * ~~~typescript - * import { IMQLock, AcquiredLock } from '.'; + * import { IMQLock, AcquiredLock } from './index.js'; * * async function doSomething(): Promise> { * const lock: AcquiredLock = await IMQLock.acquire('doSomething'); diff --git a/src/IMQRPCOptions.ts b/src/IMQRPCOptions.ts index 082f132..f238721 100644 --- a/src/IMQRPCOptions.ts +++ b/src/IMQRPCOptions.ts @@ -22,10 +22,10 @@ * to get commercial licensing options. */ import { DEFAULT_IMQ_OPTIONS, IMQOptions } from '@imqueue/core'; -import { IMQRPCRequest } from './IMQRPCRequest'; -import { IMQRPCResponse } from './IMQRPCResponse'; -import { IMQService } from './IMQService'; -import { IMQClient } from './IMQClient'; +import { IMQRPCRequest } from './IMQRPCRequest.js'; +import { IMQRPCResponse } from './IMQRPCResponse.js'; +import { IMQService } from './IMQService.js'; +import { IMQClient } from './IMQClient.js'; /** * Hook invoked before a service method call is dispatched. diff --git a/src/IMQRPCRequest.ts b/src/IMQRPCRequest.ts index 9562fee..bf97702 100644 --- a/src/IMQRPCRequest.ts +++ b/src/IMQRPCRequest.ts @@ -22,7 +22,7 @@ * to get commercial licensing options. */ import { JsonObject } from '@imqueue/core'; -import { IMQMetadata } from './IMQMetadata'; +import { IMQMetadata } from './IMQMetadata.js'; /** * Request message data structure to be handled by a service. diff --git a/src/IMQRPCResponse.ts b/src/IMQRPCResponse.ts index 3ab28e5..10d2770 100644 --- a/src/IMQRPCResponse.ts +++ b/src/IMQRPCResponse.ts @@ -22,7 +22,7 @@ * to get commercial licensing options. */ import { JsonObject } from '@imqueue/core'; -import { IMQRPCError, IMQRPCRequest } from '.'; +import { IMQRPCError, IMQRPCRequest } from './index.js'; /** * Response message data structure that a service replies with to handled diff --git a/src/IMQRequestContext.ts b/src/IMQRequestContext.ts index 1c9d28e..bbcfa7e 100644 --- a/src/IMQRequestContext.ts +++ b/src/IMQRequestContext.ts @@ -22,8 +22,8 @@ * to get commercial licensing options. */ import { AsyncLocalStorage } from 'node:async_hooks'; -import { IMQMetadata } from './IMQMetadata'; -import { IMQRPCRequest } from './IMQRPCRequest'; +import { IMQMetadata } from './IMQMetadata.js'; +import { IMQRPCRequest } from './IMQRPCRequest.js'; const storage = new AsyncLocalStorage(); diff --git a/src/IMQService.ts b/src/IMQService.ts index f8d8602..c37bf8a 100644 --- a/src/IMQService.ts +++ b/src/IMQService.ts @@ -43,13 +43,13 @@ import { AFTER_HOOK_ERROR, BEFORE_HOOK_ERROR, DEFAULT_IMQ_METRICS_SERVER_OPTIONS, -} from '.'; -import { SIGNALS } from './helpers'; +} from './index.js'; +import { SIGNALS } from './helpers/index.js'; import { cpus } from 'node:os'; import cluster, { type Worker } from 'node:cluster'; -import { ArgDescription } from './IMQRPCDescription'; -import { IMQBeforeCall, IMQAfterCall } from './IMQRPCOptions'; -import { runWithRequest } from './IMQRequestContext'; +import { ArgDescription } from './IMQRPCDescription.js'; +import { IMQBeforeCall, IMQAfterCall } from './IMQRPCOptions.js'; +import { runWithRequest } from './IMQRequestContext.js'; import { createServer, type Server } from 'node:http'; export class Description { diff --git a/src/cache/RedisCache.ts b/src/cache/RedisCache.ts index 51a7815..a18cf9f 100644 --- a/src/cache/RedisCache.ts +++ b/src/cache/RedisCache.ts @@ -29,13 +29,13 @@ import { Redis, } from '@imqueue/core'; import { hostname } from 'node:os'; -import { ICache } from '.'; +import { ICache } from './index.js'; export interface IRedisCacheOptions extends Partial { conn?: IRedisClient; } -export const DEFAULT_REDIS_CACHE_OPTIONS = { +export const DEFAULT_REDIS_CACHE_OPTIONS: IMQOptions = { ...DEFAULT_IMQ_OPTIONS, prefix: 'imq-cache', }; diff --git a/src/cache/index.ts b/src/cache/index.ts index 03505d3..71d559f 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -21,5 +21,5 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -export * from './ICache'; -export * from './RedisCache'; +export * from './ICache.js'; +export * from './RedisCache.js'; diff --git a/src/decorators/cache.ts b/src/decorators/cache.ts index 7b2cfb1..a14c81f 100644 --- a/src/decorators/cache.ts +++ b/src/decorators/cache.ts @@ -21,8 +21,11 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { IMQCache, ICache, RedisCache, ICacheConstructor } from '..'; -import { signature } from '../helpers'; +// cache classes are imported from their defining modules (not the package +// barrel) to avoid a circular-import TDZ on the module-scope default options +import { ICache, ICacheConstructor, RedisCache } from '../cache/index.js'; +import { IMQCache } from '../IMQCache.js'; +import { signature } from '../helpers/index.js'; export interface CacheDecoratorOptions { adapter?: string | ICache | ICacheConstructor; diff --git a/src/decorators/classType.ts b/src/decorators/classType.ts index 7be7328..8041da5 100644 --- a/src/decorators/classType.ts +++ b/src/decorators/classType.ts @@ -21,7 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { registerType } from './property'; +import { registerType } from './property.js'; /** * Implements the '@classType' decorator factory. diff --git a/src/decorators/expose.ts b/src/decorators/expose.ts index 8360ed9..6969f45 100644 --- a/src/decorators/expose.ts +++ b/src/decorators/expose.ts @@ -22,7 +22,11 @@ * to get commercial licensing options. */ import { parse, type Comment, type Options } from 'acorn'; -import { ArgDescription, ReturnValueDescription, IMQRPCDescription } from '..'; +import { + ArgDescription, + ReturnValueDescription, + IMQRPCDescription, +} from '../index.js'; type CommentMetadata = { [key: string]: string | ArgDescription[] | ReturnValueDescription; diff --git a/src/decorators/index.ts b/src/decorators/index.ts index 88eaa4a..ba4f9b5 100644 --- a/src/decorators/index.ts +++ b/src/decorators/index.ts @@ -23,13 +23,13 @@ */ // must load first: installs the Symbol.metadata polyfill required by the // standard-decorator metadata used across the decorators below -import './metadata'; +import './metadata.js'; -export * from './expose'; -export * from './classType'; -export * from './remote'; -export * from './lock'; -export * from './cache'; -export * from './property'; -export * from './indexed'; -export * from './logged'; +export * from './expose.js'; +export * from './classType.js'; +export * from './remote.js'; +export * from './lock.js'; +export * from './cache.js'; +export * from './property.js'; +export * from './indexed.js'; +export * from './logged.js'; diff --git a/src/decorators/indexed.ts b/src/decorators/indexed.ts index d386b23..3155a2d 100644 --- a/src/decorators/indexed.ts +++ b/src/decorators/indexed.ts @@ -21,8 +21,8 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { Thunk, IMQRPCDescription } from '..'; -import { registerType } from './property'; +import { Thunk, IMQRPCDescription } from '../index.js'; +import { registerType } from './property.js'; /** * Implements '@indexed' decorator factory diff --git a/src/decorators/lock.ts b/src/decorators/lock.ts index 2b99bcc..cd5ef38 100644 --- a/src/decorators/lock.ts +++ b/src/decorators/lock.ts @@ -21,8 +21,8 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { IMQLock, AcquiredLock } from '..'; -import { signature } from '../helpers'; +import { IMQLock, AcquiredLock } from '../index.js'; +import { signature } from '../helpers/index.js'; export interface LockOptions { disabled?: boolean; diff --git a/src/decorators/property.ts b/src/decorators/property.ts index 051b9d7..c2be047 100644 --- a/src/decorators/property.ts +++ b/src/decorators/property.ts @@ -21,7 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { IMQRPCDescription } from '..'; +import { IMQRPCDescription } from '../index.js'; export interface Thunk { (): any; diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 1172057..a3fa473 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -20,7 +20,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -export * from './signature'; -export * from './os-uuid'; -export * from './pid'; -export * from './fs'; +export * from './signature.js'; +export * from './os-uuid.js'; +export * from './pid.js'; +export * from './fs.js'; diff --git a/src/index.ts b/src/index.ts index 37247d8..8fb2c57 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,17 +19,17 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -export * from './decorators'; -export * from './cache'; -export * from './IMQDelay'; -export * from './IMQCache'; -export * from './IMQRPCOptions'; -export * from './IMQRPCDescription'; -export * from './IMQRPCError'; -export * from './IMQRPCRequest'; -export * from './IMQRPCResponse'; -export * from './IMQLock'; -export * from './IMQService'; -export * from './IMQClient'; -export * from './IMQMetadata'; -export * from './IMQRequestContext'; +export * from './decorators/index.js'; +export * from './cache/index.js'; +export * from './IMQDelay.js'; +export * from './IMQCache.js'; +export * from './IMQRPCOptions.js'; +export * from './IMQRPCDescription.js'; +export * from './IMQRPCError.js'; +export * from './IMQRPCRequest.js'; +export * from './IMQRPCResponse.js'; +export * from './IMQLock.js'; +export * from './IMQService.js'; +export * from './IMQClient.js'; +export * from './IMQMetadata.js'; +export * from './IMQRequestContext.js'; diff --git a/test/IMQCache.spec.ts b/test/IMQCache.spec.ts index 34342c9..ce18b7a 100644 --- a/test/IMQCache.spec.ts +++ b/test/IMQCache.spec.ts @@ -21,10 +21,10 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { logger } from './mocks'; +import { logger } from './mocks/index.js'; import { describe, it, afterEach, mock } from 'node:test'; import assert from 'node:assert/strict'; -import { IMQCache, RedisCache } from '..'; +import { IMQCache, RedisCache } from '../index.js'; describe('IMQCache', () => { IMQCache.adapters = {}; diff --git a/test/IMQClient.callTimeout.spec.ts b/test/IMQClient.callTimeout.spec.ts index b37c32d..1623536 100644 --- a/test/IMQClient.callTimeout.spec.ts +++ b/test/IMQClient.callTimeout.spec.ts @@ -1,11 +1,11 @@ /*! * IMQClient per-call timeout tests */ -import './mocks'; +import './mocks/index.js'; import { describe, it, afterEach, mock } from 'node:test'; import assert from 'node:assert/strict'; -import { IMQClient, IMQDelay, remote } from '..'; -import { logger } from './mocks'; +import { IMQClient, IMQDelay, remote } from '../index.js'; +import { logger } from './mocks/index.js'; class TimeoutClient extends IMQClient { @remote() diff --git a/test/IMQClient.console.logger.spec.ts b/test/IMQClient.console.logger.spec.ts index 06d153d..68e1894 100644 --- a/test/IMQClient.console.logger.spec.ts +++ b/test/IMQClient.console.logger.spec.ts @@ -1,7 +1,7 @@ /*! * IMQClient console logger fallback branches coverage */ -import './mocks'; +import './mocks/index.js'; import { describe, it, afterEach, mock } from 'node:test'; import assert from 'node:assert/strict'; import { @@ -11,7 +11,7 @@ import { remote, AFTER_HOOK_ERROR, BEFORE_HOOK_ERROR, -} from '..'; +} from '../index.js'; class ConsoleClient extends IMQClient { @remote() diff --git a/test/IMQClient.extra.spec.ts b/test/IMQClient.extra.spec.ts index e253056..656f7b0 100644 --- a/test/IMQClient.extra.spec.ts +++ b/test/IMQClient.extra.spec.ts @@ -1,10 +1,10 @@ /*! * IMQClient Extra Unit Tests (non-RPC, using send stubs) */ -import './mocks'; +import './mocks/index.js'; import { describe, it, afterEach, mock } from 'node:test'; import assert from 'node:assert/strict'; -import { logger } from './mocks'; +import { logger } from './mocks/index.js'; import { IMQClient, IMQDelay, @@ -12,7 +12,7 @@ import { remote, AFTER_HOOK_ERROR, BEFORE_HOOK_ERROR, -} from '..'; +} from '../index.js'; class ExtraClient extends IMQClient { @remote() diff --git a/test/IMQClient.generator.trailing.args.spec.ts b/test/IMQClient.generator.trailing.args.spec.ts index a5e61fd..7bb07c2 100644 --- a/test/IMQClient.generator.trailing.args.spec.ts +++ b/test/IMQClient.generator.trailing.args.spec.ts @@ -4,7 +4,13 @@ import * as fs from 'node:fs'; import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; -import { IMQService, IMQClient, IMQDelay, IMQMetadata, expose } from '..'; +import { + IMQService, + IMQClient, + IMQDelay, + IMQMetadata, + expose, +} from '../index.js'; // The generated client imports from '@imqueue/rpc'; the package.json `exports` // field lets that specifier self-resolve to this in-tree build, so no module diff --git a/test/IMQClient.generator.types.spec.ts b/test/IMQClient.generator.types.spec.ts index 705e225..a013df9 100644 --- a/test/IMQClient.generator.types.spec.ts +++ b/test/IMQClient.generator.types.spec.ts @@ -32,7 +32,7 @@ import { property, classType, indexed, -} from '..'; +} from '../index.js'; // The generated client imports from '@imqueue/rpc'; the package.json `exports` // field lets that specifier self-resolve to this in-tree build, so no module diff --git a/test/IMQClient.methods.spec.ts b/test/IMQClient.methods.spec.ts index 85aa6b1..46dde1a 100644 --- a/test/IMQClient.methods.spec.ts +++ b/test/IMQClient.methods.spec.ts @@ -1,11 +1,11 @@ /*! * IMQClient methods Unit Tests (subscribe/unsubscribe/broadcast + signals) */ -import './mocks'; +import './mocks/index.js'; import { describe, it, afterEach, mock } from 'node:test'; import assert from 'node:assert/strict'; -import { IMQClient, remote } from '..'; -import { logger } from './mocks'; +import { IMQClient, remote } from '../index.js'; +import { logger } from './mocks/index.js'; class MethodsClient extends IMQClient { // ensure there is at least one remote method (not used directly here) diff --git a/test/IMQClient.singleQueue.spec.ts b/test/IMQClient.singleQueue.spec.ts index f8a626e..210fccb 100644 --- a/test/IMQClient.singleQueue.spec.ts +++ b/test/IMQClient.singleQueue.spec.ts @@ -1,11 +1,11 @@ /*! * IMQClient singleQueue lifecycle tests */ -import './mocks'; +import './mocks/index.js'; import { describe, it, afterEach, mock } from 'node:test'; import assert from 'node:assert/strict'; -import { IMQClient, remote } from '..'; -import { logger } from './mocks'; +import { IMQClient, remote } from '../index.js'; +import { logger } from './mocks/index.js'; class SingleQueueClient extends IMQClient { @remote() diff --git a/test/IMQClient.spec.ts b/test/IMQClient.spec.ts index 92cd52b..c75f71f 100644 --- a/test/IMQClient.spec.ts +++ b/test/IMQClient.spec.ts @@ -23,7 +23,7 @@ */ import { describe, it, before, after, mock } from 'node:test'; import assert from 'node:assert/strict'; -import { logger } from './mocks'; +import { logger } from './mocks/index.js'; import { IMQClient, IMQService, @@ -31,7 +31,7 @@ import { Description, expose, remote, -} from '..'; +} from '../index.js'; import * as fs from 'node:fs'; // The generated client imports from '@imqueue/rpc'; the package.json `exports` diff --git a/test/IMQClient.transpile.error.spec.ts b/test/IMQClient.transpile.error.spec.ts index 7425998..7a2c106 100644 --- a/test/IMQClient.transpile.error.spec.ts +++ b/test/IMQClient.transpile.error.spec.ts @@ -21,13 +21,15 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import './mocks'; +import './mocks/index.js'; import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; // live module object so mock.method() patches the same spawnSync the client -// transpiler reads at call time +// transpiler reads at call time; under ESM the transpiler's named-import +// binding only picks the patch up after syncBuiltinESMExports() import childProcess = require('node:child_process'); -import { IMQService, IMQClient, expose } from '..'; +import { syncBuiltinESMExports } from 'node:module'; +import { IMQService, IMQClient, expose } from '../index.js'; // Own spec file (hence own process): generating a client makes exactly one // getDescription() round-trip; a second create() in the same process would hang @@ -59,6 +61,12 @@ describe('IMQClient client transpiler failure', () => { stderr: 'boom-err', status: 1, })) as any); + syncBuiltinESMExports(); + t.after(() => { + // t.mock auto-restores the CJS object; the ESM bindings must be + // re-synced separately or the stub would leak into other tests + setImmediate(() => syncBuiltinESMExports()); + }); await assert.rejects( IMQClient.create('TranspileErrService', { diff --git a/test/IMQDelay.spec.ts b/test/IMQDelay.spec.ts index a2f002a..fa1e4c9 100644 --- a/test/IMQDelay.spec.ts +++ b/test/IMQDelay.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 { IMQDelay } from '..'; +import { IMQDelay } from '../index.js'; describe('IMQDelay', () => { it('should be a class', () => { diff --git a/test/IMQLock.spec.ts b/test/IMQLock.spec.ts index 66d0f49..f1f35bd 100644 --- a/test/IMQLock.spec.ts +++ b/test/IMQLock.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, before, after } from 'node:test'; import assert from 'node:assert/strict'; -import { IMQLock, AcquiredLock } from '..'; +import { IMQLock, AcquiredLock } from '../index.js'; const LOCK_TIMEOUT = 100; const ORIGINAL_LOCK_TIMEOUT = IMQLock.deadlockTimeout; @@ -54,8 +54,6 @@ async function deadLocked() { } describe('IMQLock', () => { - (this as any).timeout = 30000; - before(() => { IMQLock.deadlockTimeout = LOCK_TIMEOUT; }); diff --git a/test/IMQLock.stringify.metadata.spec.ts b/test/IMQLock.stringify.metadata.spec.ts index f1c9fdb..cbeb223 100644 --- a/test/IMQLock.stringify.metadata.spec.ts +++ b/test/IMQLock.stringify.metadata.spec.ts @@ -3,7 +3,7 @@ */ import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; -import { IMQLock } from '..'; +import { IMQLock } from '../index.js'; describe('IMQLock acquire() timeout with unstringifiable metadata', () => { const KEY = 'circular-metadata-key'; diff --git a/test/IMQMetadata.spec.ts b/test/IMQMetadata.spec.ts index 69a482c..e32b7df 100644 --- a/test/IMQMetadata.spec.ts +++ b/test/IMQMetadata.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 { IMQMetadata } from '..'; +import { IMQMetadata } from '../index.js'; describe('IMQMetadata', () => { it('should be a class', () => { diff --git a/test/IMQService.afterCall.spec.ts b/test/IMQService.afterCall.spec.ts index 708e8c0..b4faa6d 100644 --- a/test/IMQService.afterCall.spec.ts +++ b/test/IMQService.afterCall.spec.ts @@ -24,8 +24,13 @@ import { describe, it, mock } from 'node:test'; import assert from 'node:assert/strict'; import { randomUUID as uuid } from 'node:crypto'; -import { logger } from './mocks'; -import { IMQService, IMQRPCRequest, expose, AFTER_HOOK_ERROR } from '..'; +import { logger } from './mocks/index.js'; +import { + IMQService, + IMQRPCRequest, + expose, + AFTER_HOOK_ERROR, +} from '../index.js'; class AfterHookService extends IMQService { @expose() diff --git a/test/IMQService.console.logger.spec.ts b/test/IMQService.console.logger.spec.ts index b1272bb..0f7d7cf 100644 --- a/test/IMQService.console.logger.spec.ts +++ b/test/IMQService.console.logger.spec.ts @@ -10,7 +10,7 @@ import { expose, BEFORE_HOOK_ERROR, AFTER_HOOK_ERROR, -} from '..'; +} from '../index.js'; class ConsoleLoggerService extends IMQService { @expose() diff --git a/test/IMQService.hooks.spec.ts b/test/IMQService.hooks.spec.ts index 6420d8d..495be80 100644 --- a/test/IMQService.hooks.spec.ts +++ b/test/IMQService.hooks.spec.ts @@ -24,8 +24,13 @@ import { describe, it, mock } from 'node:test'; import assert from 'node:assert/strict'; import { randomUUID as uuid } from 'node:crypto'; -import { logger } from './mocks'; -import { IMQService, IMQRPCRequest, expose, BEFORE_HOOK_ERROR } from '..'; +import { logger } from './mocks/index.js'; +import { + IMQService, + IMQRPCRequest, + expose, + BEFORE_HOOK_ERROR, +} from '../index.js'; class HookTestService extends IMQService { @expose() diff --git a/test/IMQService.logger.fallback.spec.ts b/test/IMQService.logger.fallback.spec.ts index cf5d200..8bd1189 100644 --- a/test/IMQService.logger.fallback.spec.ts +++ b/test/IMQService.logger.fallback.spec.ts @@ -10,7 +10,7 @@ import { expose, BEFORE_HOOK_ERROR, AFTER_HOOK_ERROR, -} from '..'; +} from '../index.js'; class FallbackLoggerService extends IMQService { @expose() diff --git a/test/IMQService.replyFailure.spec.ts b/test/IMQService.replyFailure.spec.ts index 62318d3..2f61ebd 100644 --- a/test/IMQService.replyFailure.spec.ts +++ b/test/IMQService.replyFailure.spec.ts @@ -1,11 +1,11 @@ /*! * IMQService reply-publish failure handling tests */ -import './mocks'; +import './mocks/index.js'; import { describe, it, afterEach, mock } from 'node:test'; import assert from 'node:assert/strict'; import { randomUUID as uuid } from 'node:crypto'; -import { IMQService, IMQRPCRequest, expose } from '..'; +import { IMQService, IMQRPCRequest, expose } from '../index.js'; class ReplyFailService extends IMQService { @expose() diff --git a/test/IMQService.signal.publish.spec.ts b/test/IMQService.signal.publish.spec.ts index ba8689d..8bafc00 100644 --- a/test/IMQService.signal.publish.spec.ts +++ b/test/IMQService.signal.publish.spec.ts @@ -4,7 +4,7 @@ import { describe, it, afterEach, mock } from 'node:test'; import assert from 'node:assert/strict'; import { randomUUID as uuid } from 'node:crypto'; -import { IMQService, expose } from '..'; +import { IMQService, expose } from '../index.js'; class SignalTestService extends IMQService { @expose() diff --git a/test/IMQService.spec.ts b/test/IMQService.spec.ts index cf2e6b6..773f26e 100644 --- a/test/IMQService.spec.ts +++ b/test/IMQService.spec.ts @@ -21,17 +21,18 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { logger } from './mocks'; +import { logger } from './mocks/index.js'; import { describe, it, mock } from 'node:test'; import assert from 'node:assert/strict'; -import { IMQService, IMQRPCRequest, Description, expose } from '..'; +import { IMQService, IMQRPCRequest, Description, expose } from '../index.js'; import { randomUUID as uuid } from 'node:crypto'; // require (not import): at runtime node:cluster's module.exports is the Cluster // instance the code under test shares, but its module *type* omits those // instance members (isMaster/listeners/…), so `any` via require avoids casting // every mocked access -const cluster: any = require('node:cluster'); +import clusterModule = require('node:cluster'); +const cluster: any = clusterModule; class TestService extends IMQService { @expose() diff --git a/test/cache/RedisCache.errors.spec.ts b/test/cache/RedisCache.errors.spec.ts index 05f9d03..a66c3a3 100644 --- a/test/cache/RedisCache.errors.spec.ts +++ b/test/cache/RedisCache.errors.spec.ts @@ -1,7 +1,7 @@ /*! * RedisCache error branches: methods before init should throw */ -import { RedisCache, REDIS_CLIENT_INIT_ERROR } from '../..'; +import { RedisCache, REDIS_CLIENT_INIT_ERROR } from '../../index.js'; import { describe, it, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; diff --git a/test/cache/RedisCache.purge.promise.spec.ts b/test/cache/RedisCache.purge.promise.spec.ts index 08a5ed2..4557f00 100644 --- a/test/cache/RedisCache.purge.promise.spec.ts +++ b/test/cache/RedisCache.purge.promise.spec.ts @@ -1,7 +1,7 @@ /*! * RedisCache additional coverage: purge() and set() with Promise */ -import { RedisCache } from '../..'; +import { RedisCache } from '../../index.js'; import { describe, it, afterEach } from 'node:test'; import assert from 'node:assert/strict'; diff --git a/test/cache/RedisCache.race.spec.ts b/test/cache/RedisCache.race.spec.ts index bd9ebdf..075a428 100644 --- a/test/cache/RedisCache.race.spec.ts +++ b/test/cache/RedisCache.race.spec.ts @@ -1,12 +1,12 @@ /*! * RedisCache concurrent-initialization tests */ -import '../mocks'; +import '../mocks/index.js'; import { describe, it, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; -import { RedisCache } from '../..'; -import { RedisClientMock } from '../mocks'; -import { logger } from '../mocks'; +import { RedisCache } from '../../index.js'; +import { RedisClientMock } from '../mocks/index.js'; +import { logger } from '../mocks/index.js'; describe('cache/RedisCache concurrent init', () => { beforeEach(async () => { diff --git a/test/cache/RedisCache.spec.ts b/test/cache/RedisCache.spec.ts index d4db73f..8961f3c 100644 --- a/test/cache/RedisCache.spec.ts +++ b/test/cache/RedisCache.spec.ts @@ -21,10 +21,10 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { Redis, logger } from '../mocks'; +import { Redis, logger } from '../mocks/index.js'; import { describe, it, before, after, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; -import { RedisCache } from '../..'; +import { RedisCache } from '../../index.js'; import { IRedisClient } from '@imqueue/core'; import { randomUUID as uuid } from 'node:crypto'; diff --git a/test/coverage-extra.spec.ts b/test/coverage-extra.spec.ts index 695940b..b6dd79d 100644 --- a/test/coverage-extra.spec.ts +++ b/test/coverage-extra.spec.ts @@ -22,7 +22,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import '../test/mocks'; +import '../test/mocks/index.js'; import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { @@ -37,8 +37,8 @@ import { currentMetadata, runWithRequest, IMQRPCDescription, -} from '..'; -import { signature } from '../src/helpers'; +} from '../index.js'; +import { signature } from '../src/helpers/index.js'; describe('legacy decorator signatures', () => { // legacy form: (target, propertyKey, descriptor) with no TC39 context diff --git a/test/decorators/cache.spec.ts b/test/decorators/cache.spec.ts index acefc7d..3270b48 100644 --- a/test/decorators/cache.spec.ts +++ b/test/decorators/cache.spec.ts @@ -21,10 +21,10 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { logger } from '../mocks'; +import { logger } from '../mocks/index.js'; import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; -import { RedisCache, cache, IMQCache } from '../..'; +import { RedisCache, cache, IMQCache } from '../../index.js'; import { ILogger } from '@imqueue/core'; class CacheTestClass { diff --git a/test/decorators/expose.spec.ts b/test/decorators/expose.spec.ts index cbbdf5f..2c7be8d 100644 --- a/test/decorators/expose.spec.ts +++ b/test/decorators/expose.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 { expose, IMQRPCDescription } from '../..'; +import { expose, IMQRPCDescription } from '../../index.js'; const description = IMQRPCDescription.serviceDescription; diff --git a/test/decorators/indexed.spec.ts b/test/decorators/indexed.spec.ts index 0892358..5135a6a 100644 --- a/test/decorators/indexed.spec.ts +++ b/test/decorators/indexed.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 { indexed, property, IMQRPCDescription } from '../..'; +import { indexed, property, IMQRPCDescription } from '../../index.js'; @indexed('[fieldName: string]: SchemaField') class Schema { diff --git a/test/decorators/lock.skipArgs.spec.ts b/test/decorators/lock.skipArgs.spec.ts index a1b2196..ee573e3 100644 --- a/test/decorators/lock.skipArgs.spec.ts +++ b/test/decorators/lock.skipArgs.spec.ts @@ -1,7 +1,7 @@ -import '../mocks'; +import '../mocks/index.js'; import { describe, it, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; -import { lock } from '../..'; +import { lock } from '../../index.js'; class SkipArgsClass { public calls: number = 0; diff --git a/test/decorators/lock.spec.ts b/test/decorators/lock.spec.ts index 3a83387..769e5d5 100644 --- a/test/decorators/lock.spec.ts +++ b/test/decorators/lock.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 { lock } from '../..'; +import { lock } from '../../index.js'; class TestLockClass { @lock() diff --git a/test/decorators/logged.spec.ts b/test/decorators/logged.spec.ts index 326dadb..264cb3d 100644 --- a/test/decorators/logged.spec.ts +++ b/test/decorators/logged.spec.ts @@ -1,6 +1,6 @@ import { describe, it, mock } from 'node:test'; import assert from 'node:assert/strict'; -import { logged } from '../../src/decorators/logged'; +import { logged } from '../../src/decorators/logged.js'; describe('decorators/logged()', () => { it('should be a function and return decorator function', () => { diff --git a/test/decorators/property.spec.ts b/test/decorators/property.spec.ts index 3818a2f..ac34976 100644 --- a/test/decorators/property.spec.ts +++ b/test/decorators/property.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 { property, classType, IMQRPCDescription } from '../..'; +import { property, classType, IMQRPCDescription } from '../../index.js'; @classType() class TestPointType { diff --git a/test/decorators/remote.spec.ts b/test/decorators/remote.spec.ts index 9fd2dcf..8565e2f 100644 --- a/test/decorators/remote.spec.ts +++ b/test/decorators/remote.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 { remote } from '../..'; +import { remote } from '../../index.js'; describe('decorators/remote()', () => { it('should be a function', () => { diff --git a/test/helpers/fs.spec.ts b/test/helpers/fs.spec.ts index bad4ad6..efca5f7 100644 --- a/test/helpers/fs.spec.ts +++ b/test/helpers/fs.spec.ts @@ -21,8 +21,8 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { mockModule } from '../mocks'; -import { describe, it, before, after } from 'node:test'; +import { moduleMockOptions } from '../mocks/index.js'; +import { describe, it, before, after, mock } from 'node:test'; import assert from 'node:assert/strict'; const fsMock = { @@ -64,17 +64,26 @@ let writeFile: (path: string, content: string) => Promise; describe('fs helpers', () => { let fsCtl: { restore(): void }; - before(function before() { - fsCtl = mockModule('node:fs', fsMock); - delete require.cache[require.resolve('../../src/helpers/fs')]; - const fs = require('../../src/helpers/fs'); + before(async function before() { + // both named exports (for ESM `import { access } from 'node:fs'`) + // and the default namespace (for CJS-style consumers) are mocked + fsCtl = mock.module( + 'node:fs', + moduleMockOptions({ ...fsMock, default: fsMock }), + ); + + // a fresh, query-busted copy links its 'node:fs' import against the + // module mock registered above (the ES module registry is immutable, + // so the copy already imported by the package cannot be re-bound) + const href = new URL('../../src/helpers/fs.js', import.meta.url).href; + const fs = await import(`${href}?fsmock=1`); + fileExists = fs.fileExists; mkdir = fs.mkdir; writeFile = fs.writeFile; }); after(() => { fsCtl.restore(); - delete require.cache[require.resolve('../../src/helpers/fs')]; }); it('should check file existance', () => diff --git a/test/helpers/os-uuid.spec.ts b/test/helpers/os-uuid.spec.ts index 9d80d5a..0912bfd 100644 --- a/test/helpers/os-uuid.spec.ts +++ b/test/helpers/os-uuid.spec.ts @@ -21,25 +21,31 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import '../mocks'; +import '../mocks/index.js'; import { describe, it, afterEach, mock } from 'node:test'; import assert from 'node:assert/strict'; -import { osUuid } from '../../src/helpers'; +import { osUuid } from '../../src/helpers/index.js'; // The helper under test reads the live `require('node:child_process')` object at // call time, so we mock that same object via import-equals (an `import * as` // namespace copy under esModuleInterop would not be the object it reads). import childProcess = require('node:child_process'); +import { syncBuiltinESMExports } from 'node:module'; // osUuid memoizes its result at module level, so the per-platform parsing // tests below load a FRESH copy of the module after mocking the platform and // execSync — otherwise the first test's cached id would leak into the rest. -const OS_UUID_PATH = require.resolve('../../src/helpers/os-uuid'); +// The ES module registry is immutable, so freshness comes from a unique +// query string; syncBuiltinESMExports() publishes the execSync stub to the +// named-import binding the fresh copy links against. +let osUuidReload = 0; -function freshOsUuid(): () => string { - delete require.cache[OS_UUID_PATH]; +async function freshOsUuid(): Promise<() => string> { + syncBuiltinESMExports(); - return require(OS_UUID_PATH).osUuid; + const href = new URL('../../src/helpers/os-uuid.js', import.meta.url).href; + + return (await import(`${href}?reload=${++osUuidReload}`)).osUuid; } describe('helpers/osUuid()', () => { @@ -48,6 +54,7 @@ describe('helpers/osUuid()', () => { afterEach(() => { mock.restoreAll(); + syncBuiltinESMExports(); Object.defineProperty(process, 'platform', { value: realPlatform }); Object.defineProperty(process, 'arch', { value: realArch }); delete process.env.PROCESSOR_ARCHITEW6432; @@ -69,13 +76,13 @@ describe('helpers/osUuid()', () => { assert.equal(id, id.toLowerCase()); }); - it('should memoize the machine id and shell out only once', () => { + it('should memoize the machine id and shell out only once', async () => { const spy = mock.method( childProcess, 'execSync', () => 'ab12cd34ef567890abcdef1234567890\n', ); - const uuid = freshOsUuid(); + const uuid = await freshOsUuid(); assert.equal(uuid(), uuid()); assert.equal( @@ -85,7 +92,7 @@ describe('helpers/osUuid()', () => { ); }); - it('should read and parse the darwin IOPlatformUUID', () => { + it('should read and parse the darwin IOPlatformUUID', async () => { Object.defineProperty(process, 'platform', { value: 'darwin' }); const spy = mock.method( childProcess, @@ -94,14 +101,17 @@ describe('helpers/osUuid()', () => { ' "IOPlatformUUID" = "AB12CD34-0000-1111-2222-33445566EE77"\n', ); - assert.equal(freshOsUuid()(), 'ab12cd34-0000-1111-2222-33445566ee77'); + assert.equal( + (await freshOsUuid())(), + 'ab12cd34-0000-1111-2222-33445566ee77', + ); assert.match( spy.mock.calls[0].arguments[0] as string, /ioreg -rd1 -c IOPlatformExpertDevice/, ); }); - it('should read and parse the win32 MachineGuid', () => { + it('should read and parse the win32 MachineGuid', async () => { Object.defineProperty(process, 'platform', { value: 'win32' }); const spy = mock.method( childProcess, @@ -111,14 +121,14 @@ describe('helpers/osUuid()', () => { ' MachineGuid REG_SZ AB12CD34-DEAD-BEEF\r\n', ); - assert.equal(freshOsUuid()(), 'ab12cd34-dead-beef'); + assert.equal((await freshOsUuid())(), 'ab12cd34-dead-beef'); assert.match( spy.mock.calls[0].arguments[0] as string, /REG\.exe QUERY .*Cryptography \/v MachineGuid/, ); }); - it('should read and parse the linux machine-id', () => { + it('should read and parse the linux machine-id', async () => { Object.defineProperty(process, 'platform', { value: 'linux' }); const spy = mock.method( childProcess, @@ -126,31 +136,36 @@ describe('helpers/osUuid()', () => { () => 'AB12CD34EF567890ABCDEF1234567890\n', ); - assert.equal(freshOsUuid()(), 'ab12cd34ef567890abcdef1234567890'); + assert.equal( + (await freshOsUuid())(), + 'ab12cd34ef567890abcdef1234567890', + ); assert.match( spy.mock.calls[0].arguments[0] as string, /\/etc\/machine-id/, ); }); - it('should build the freebsd command', () => { + it('should build the freebsd command', async () => { Object.defineProperty(process, 'platform', { value: 'freebsd' }); const spy = mock.method(childProcess, 'execSync', () => 'ABCDEF12\n'); - assert.equal(freshOsUuid()(), 'abcdef12'); + assert.equal((await freshOsUuid())(), 'abcdef12'); assert.match( spy.mock.calls[0].arguments[0] as string, /kenv -q smbios\.system\.uuid/, ); }); - it('should throw for an unsupported platform', () => { + it('should throw for an unsupported platform', async () => { Object.defineProperty(process, 'platform', { value: 'aix' }); - assert.throws(() => freshOsUuid()(), /Unsupported platform/); + const uuid = await freshOsUuid(); + + assert.throws(() => uuid(), /Unsupported platform/); }); - it('should use the sysnative redirector for 32-bit on 64-bit windows', () => { + it('should use the sysnative redirector for 32-bit on 64-bit windows', async () => { Object.defineProperty(process, 'platform', { value: 'win32' }); Object.defineProperty(process, 'arch', { value: 'ia32' }); process.env.PROCESSOR_ARCHITEW6432 = 'AMD64'; @@ -160,7 +175,7 @@ describe('helpers/osUuid()', () => { () => 'MachineGuid REG_SZ AB12CD34\r\n', ); - freshOsUuid()(); + (await freshOsUuid())(); assert.match(spy.mock.calls[0].arguments[0] as string, /sysnative/); }); diff --git a/test/helpers/pid.spec.ts b/test/helpers/pid.spec.ts index 4304686..f2b2265 100644 --- a/test/helpers/pid.spec.ts +++ b/test/helpers/pid.spec.ts @@ -21,10 +21,10 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { logger } from '../mocks'; +import { logger } from '../mocks/index.js'; import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; -import { pid, forgetPid, IMQ_TMP_DIR } from '../../src/helpers'; +import { pid, forgetPid, IMQ_TMP_DIR } from '../../src/helpers/index.js'; import { randomUUID as uuid } from 'node:crypto'; import * as fs from 'node:fs'; diff --git a/test/helpers/signature.spec.ts b/test/helpers/signature.spec.ts index 215fd9f..3bf3e10 100644 --- a/test/helpers/signature.spec.ts +++ b/test/helpers/signature.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 { signature } from '../../src/helpers'; +import { signature } from '../../src/helpers/index.js'; describe('helpers/signature()', () => { it('should be a function', () => { diff --git a/test/mocks/index.ts b/test/mocks/index.ts index d14556f..03417fb 100644 --- a/test/mocks/index.ts +++ b/test/mocks/index.ts @@ -21,6 +21,6 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -export * from './logger'; -export * from './redis'; -export * from './moduleMock'; +export * from './logger.js'; +export * from './redis.js'; +export * from './moduleMock.js'; diff --git a/test/mocks/redis.ts b/test/mocks/redis.ts index 58a5ea8..df1fe4a 100644 --- a/test/mocks/redis.ts +++ b/test/mocks/redis.ts @@ -21,7 +21,8 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { mockModule } from './moduleMock'; +import { mock } from 'node:test'; +import { moduleMockOptions } from './moduleMock.js'; import { EventEmitter } from 'node:events'; import * as crypto from 'node:crypto'; @@ -51,7 +52,7 @@ export class RedisClientMock extends EventEmitter { RedisClientMock.__constructed++; setTimeout(() => { this.emit('ready', this); - }); + }).unref(); if (options.connectionName) { this.__name = options.connectionName; @@ -87,7 +88,7 @@ export class RedisClientMock extends EventEmitter { } setTimeout(() => { delete RedisClientMock.__keys[key]; - }, expire); + }, expire).unref(); } const cb = args.pop(); @@ -139,7 +140,7 @@ export class RedisClientMock extends EventEmitter { this.__rt = setTimeout( () => resolve(this.brpop(key, timeout, cb)), timeout || 100, - ); + ).unref(); }); } else { const result = [key, q.shift()]; @@ -167,7 +168,7 @@ export class RedisClientMock extends EventEmitter { this.__rt = setTimeout( () => resolve(this.brpoplpush(from, to, timeout, cb)), timeout || 100, - ); + ).unref(); }); } else { toQ.push(fromQ.shift()); @@ -289,7 +290,7 @@ export class RedisClientMock extends EventEmitter { setTimeout(() => { const toKey = key.split(/:/).slice(0, 2).join(':'); this.lpush(toKey, value); - }, timeout); + }, timeout).unref(); this.cbExecute(cb); return true; } @@ -316,13 +317,16 @@ export class RedisClientMock extends EventEmitter { const Redis = RedisClientMock; -// __esModule marks this as an ES-module shape so core's esModuleInterop -// default import (`import Redis from 'ioredis'`) resolves to the constructor -// rather than the wrapper object. Native module mocking cannot merge named -// exports onto a class default, so the whole ESM-shaped namespace object is -// registered as the module's export (for CJS consumers it IS what -// `require('ioredis')` returns). -mockModule('ioredis', { __esModule: true, Redis, default: Redis }); +// 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({ + default: { __esModule: true, Redis, default: Redis }, + Redis, + }), +); // @ts-ignore export * from 'ioredis'; diff --git a/test/signals.cleanup.spec.ts b/test/signals.cleanup.spec.ts index 06a3a2e..13ba426 100644 --- a/test/signals.cleanup.spec.ts +++ b/test/signals.cleanup.spec.ts @@ -3,11 +3,11 @@ * handlers when destroyed, so long-lived processes that create and destroy * clients/services do not leak listeners. */ -import './mocks'; +import './mocks/index.js'; import { describe, it, mock, afterEach } from 'node:test'; import assert from 'node:assert/strict'; -import { IMQClient, IMQService, expose, remote } from '..'; -import { logger } from './mocks'; +import { IMQClient, IMQService, expose, remote } from '../index.js'; +import { logger } from './mocks/index.js'; class SignalCleanupClient extends IMQClient { @remote() From 984d1880ca511841b920008e36f0f6c844dee974 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 9 Jul 2026 19:35:47 +0200 Subject: [PATCH 2/5] chore: modernize tsconfig (es2024, skipLibCheck, verbatimModuleSyntax) - target/lib es2024 (Node >=22.12 floor fully implements it) - skipLibCheck guards against third-party d.ts breakage - verbatimModuleSyntax with import type annotations throughout (ESM-only option, hence esm branch only); vendored core tarball refreshed --- package-lock.json | 2 +- src/IMQCache.ts | 2 +- src/IMQClient.ts | 16 ++++++++-------- src/IMQLock.ts | 2 +- src/IMQMetadata.ts | 2 +- src/IMQRPCError.ts | 2 +- src/IMQRPCOptions.ts | 6 +++--- src/IMQRPCRequest.ts | 2 +- src/IMQRPCResponse.ts | 4 ++-- src/IMQRequestContext.ts | 2 +- src/IMQService.ts | 24 ++++++++++++------------ src/cache/RedisCache.ts | 8 ++++---- src/decorators/cache.ts | 2 +- src/decorators/expose.ts | 4 ++-- src/decorators/indexed.ts | 2 +- src/decorators/lock.ts | 2 +- src/decorators/logged.ts | 2 +- src/helpers/pid.ts | 2 +- test/IMQLock.spec.ts | 2 +- test/IMQService.afterCall.spec.ts | 2 +- test/IMQService.console.logger.spec.ts | 2 +- test/IMQService.hooks.spec.ts | 2 +- test/IMQService.logger.fallback.spec.ts | 2 +- test/IMQService.replyFailure.spec.ts | 2 +- test/IMQService.spec.ts | 2 +- test/cache/RedisCache.spec.ts | 2 +- test/decorators/cache.spec.ts | 2 +- test/mocks/moduleMock.ts | 2 +- tsconfig.json | 7 +++++-- 29 files changed, 58 insertions(+), 55 deletions(-) diff --git a/package-lock.json b/package-lock.json index 70877fa..d41afe4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "node_modules/@imqueue/core": { "version": "3.1.1", "resolved": "file:vendor/imqueue-core-3.1.1.tgz", - "integrity": "sha512-lrjgDaIh4m0iASgQSKk8Y6Pp6OLDFPW255f9FR3Ne0qwoksOLNdQpfPX/vGNddEbbEfsRZ8MiGYJ60C8Rjju7g==", + "integrity": "sha512-L0Wm0aNvuG+e+/y5X3C3vxt0JqIx52qZQfhi2kqIkFGfSOo8iNVeZcj4HBdWj1TO5C6PMreJfg1JZHjUYW1iLQ==", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.11.1" diff --git a/src/IMQCache.ts b/src/IMQCache.ts index 451ce5c..c0addb4 100644 --- a/src/IMQCache.ts +++ b/src/IMQCache.ts @@ -22,7 +22,7 @@ * to get commercial licensing options. */ import { RedisCache } from './cache/index.js'; -import { ICache, ICacheAdapter, ICacheConstructor } from './index.js'; +import { type ICache, type ICacheAdapter, type ICacheConstructor } from './index.js'; // ES modules provide no synchronous dynamic loading, so the built-in cache // adapters registrable by name are enumerated statically; custom adapters diff --git a/src/IMQClient.ts b/src/IMQClient.ts index 46d9c0a..9662e3b 100644 --- a/src/IMQClient.ts +++ b/src/IMQClient.ts @@ -22,17 +22,17 @@ * to get commercial licensing options. */ import IMQ, { - IMessageQueue, - ILogger, - JsonObject, - AnyJson, + type IMessageQueue, + type ILogger, + type JsonObject, + type AnyJson, IMQ_SHUTDOWN_TIMEOUT, } from '@imqueue/core'; import { DEFAULT_IMQ_CLIENT_OPTIONS, - IMQClientOptions, - IMQRPCResponse, - IMQRPCRequest, + type IMQClientOptions, + type IMQRPCResponse, + type IMQRPCRequest, IMQDelay, IMQError, remote, @@ -63,7 +63,7 @@ import { import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; import { join, dirname } from 'node:path'; -import { IMQBeforeCall, IMQAfterCall } from './IMQRPCOptions.js'; +import { type IMQBeforeCall, type IMQAfterCall } from './IMQRPCOptions.js'; // CommonJS require scoped to this module: resolves the typescript package // and loads generated CommonJS client modules from the ESM host diff --git a/src/IMQLock.ts b/src/IMQLock.ts index 842e69e..cd8f5e0 100644 --- a/src/IMQLock.ts +++ b/src/IMQLock.ts @@ -21,7 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { ILogger } from '@imqueue/core'; +import { type ILogger } from '@imqueue/core'; export type AcquiredLock = T | boolean; export type IMQLockTask = [(...args: any[]) => any, (...args: any[]) => any]; diff --git a/src/IMQMetadata.ts b/src/IMQMetadata.ts index caa2c04..4bf6fc4 100644 --- a/src/IMQMetadata.ts +++ b/src/IMQMetadata.ts @@ -21,7 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { AnyJson, JsonObject } from '@imqueue/core'; +import { type AnyJson, type JsonObject } from '@imqueue/core'; /** * Arbitrary, JSON-serializable metadata bag carried alongside an IMQ request. diff --git a/src/IMQRPCError.ts b/src/IMQRPCError.ts index db4d16e..7a3c8ec 100644 --- a/src/IMQRPCError.ts +++ b/src/IMQRPCError.ts @@ -21,7 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { JsonObject } from '@imqueue/core'; +import { type JsonObject } from '@imqueue/core'; export const BEFORE_HOOK_ERROR = 'Before call hook error:'; export const AFTER_HOOK_ERROR = 'After call hook error:'; diff --git a/src/IMQRPCOptions.ts b/src/IMQRPCOptions.ts index f238721..c6d7ad2 100644 --- a/src/IMQRPCOptions.ts +++ b/src/IMQRPCOptions.ts @@ -21,9 +21,9 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { DEFAULT_IMQ_OPTIONS, IMQOptions } from '@imqueue/core'; -import { IMQRPCRequest } from './IMQRPCRequest.js'; -import { IMQRPCResponse } from './IMQRPCResponse.js'; +import { DEFAULT_IMQ_OPTIONS, type IMQOptions } from '@imqueue/core'; +import { type IMQRPCRequest } from './IMQRPCRequest.js'; +import { type IMQRPCResponse } from './IMQRPCResponse.js'; import { IMQService } from './IMQService.js'; import { IMQClient } from './IMQClient.js'; diff --git a/src/IMQRPCRequest.ts b/src/IMQRPCRequest.ts index bf97702..07d31c6 100644 --- a/src/IMQRPCRequest.ts +++ b/src/IMQRPCRequest.ts @@ -21,7 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { JsonObject } from '@imqueue/core'; +import { type JsonObject } from '@imqueue/core'; import { IMQMetadata } from './IMQMetadata.js'; /** diff --git a/src/IMQRPCResponse.ts b/src/IMQRPCResponse.ts index 10d2770..f2300d1 100644 --- a/src/IMQRPCResponse.ts +++ b/src/IMQRPCResponse.ts @@ -21,8 +21,8 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { JsonObject } from '@imqueue/core'; -import { IMQRPCError, IMQRPCRequest } from './index.js'; +import { type JsonObject } from '@imqueue/core'; +import { type IMQRPCError, type IMQRPCRequest } from './index.js'; /** * Response message data structure that a service replies with to handled diff --git a/src/IMQRequestContext.ts b/src/IMQRequestContext.ts index bbcfa7e..d8dbc15 100644 --- a/src/IMQRequestContext.ts +++ b/src/IMQRequestContext.ts @@ -23,7 +23,7 @@ */ import { AsyncLocalStorage } from 'node:async_hooks'; import { IMQMetadata } from './IMQMetadata.js'; -import { IMQRPCRequest } from './IMQRPCRequest.js'; +import { type IMQRPCRequest } from './IMQRPCRequest.js'; const storage = new AsyncLocalStorage(); diff --git a/src/IMQService.ts b/src/IMQService.ts index c37bf8a..616a0b7 100644 --- a/src/IMQService.ts +++ b/src/IMQService.ts @@ -22,23 +22,23 @@ * to get commercial licensing options. */ import IMQ, { - JsonObject, - ILogger, - IMessageQueue, + type JsonObject, + type ILogger, + type IMessageQueue, profile, IMQ_SHUTDOWN_TIMEOUT, } from '@imqueue/core'; import { - TypesDescription, + type TypesDescription, IMQRPCDescription, - IMQRPCRequest, - IMQRPCResponse, - IMQServiceOptions, + type IMQRPCRequest, + type IMQRPCResponse, + type IMQServiceOptions, IMQError, expose, - ICache, - ServiceClassDescription, - MethodsCollectionDescription, + type ICache, + type ServiceClassDescription, + type MethodsCollectionDescription, DEFAULT_IMQ_SERVICE_OPTIONS, AFTER_HOOK_ERROR, BEFORE_HOOK_ERROR, @@ -47,8 +47,8 @@ import { import { SIGNALS } from './helpers/index.js'; import { cpus } from 'node:os'; import cluster, { type Worker } from 'node:cluster'; -import { ArgDescription } from './IMQRPCDescription.js'; -import { IMQBeforeCall, IMQAfterCall } from './IMQRPCOptions.js'; +import { type ArgDescription } from './IMQRPCDescription.js'; +import { type IMQBeforeCall, type IMQAfterCall } from './IMQRPCOptions.js'; import { runWithRequest } from './IMQRequestContext.js'; import { createServer, type Server } from 'node:http'; diff --git a/src/cache/RedisCache.ts b/src/cache/RedisCache.ts index a18cf9f..83edcfe 100644 --- a/src/cache/RedisCache.ts +++ b/src/cache/RedisCache.ts @@ -22,14 +22,14 @@ * to get commercial licensing options. */ import { - ILogger, + type ILogger, DEFAULT_IMQ_OPTIONS, - IRedisClient, - IMQOptions, + type IRedisClient, + type IMQOptions, Redis, } from '@imqueue/core'; import { hostname } from 'node:os'; -import { ICache } from './index.js'; +import { type ICache } from './index.js'; export interface IRedisCacheOptions extends Partial { conn?: IRedisClient; diff --git a/src/decorators/cache.ts b/src/decorators/cache.ts index a14c81f..b4a74c8 100644 --- a/src/decorators/cache.ts +++ b/src/decorators/cache.ts @@ -23,7 +23,7 @@ */ // cache classes are imported from their defining modules (not the package // barrel) to avoid a circular-import TDZ on the module-scope default options -import { ICache, ICacheConstructor, RedisCache } from '../cache/index.js'; +import { type ICache, type ICacheConstructor, RedisCache } from '../cache/index.js'; import { IMQCache } from '../IMQCache.js'; import { signature } from '../helpers/index.js'; diff --git a/src/decorators/expose.ts b/src/decorators/expose.ts index 6969f45..1bbf8cd 100644 --- a/src/decorators/expose.ts +++ b/src/decorators/expose.ts @@ -23,8 +23,8 @@ */ import { parse, type Comment, type Options } from 'acorn'; import { - ArgDescription, - ReturnValueDescription, + type ArgDescription, + type ReturnValueDescription, IMQRPCDescription, } from '../index.js'; diff --git a/src/decorators/indexed.ts b/src/decorators/indexed.ts index 3155a2d..a6380d3 100644 --- a/src/decorators/indexed.ts +++ b/src/decorators/indexed.ts @@ -21,7 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { Thunk, IMQRPCDescription } from '../index.js'; +import { type Thunk, IMQRPCDescription } from '../index.js'; import { registerType } from './property.js'; /** diff --git a/src/decorators/lock.ts b/src/decorators/lock.ts index cd5ef38..90c86e2 100644 --- a/src/decorators/lock.ts +++ b/src/decorators/lock.ts @@ -21,7 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { IMQLock, AcquiredLock } from '../index.js'; +import { IMQLock, type AcquiredLock } from '../index.js'; import { signature } from '../helpers/index.js'; export interface LockOptions { diff --git a/src/decorators/logged.ts b/src/decorators/logged.ts index 9134607..2a72c44 100644 --- a/src/decorators/logged.ts +++ b/src/decorators/logged.ts @@ -21,7 +21,7 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { ILogger } from '@imqueue/core'; +import { type ILogger } from '@imqueue/core'; export type LoggedLogLevel = 'info' | 'log' | 'warn' | 'error'; diff --git a/src/helpers/pid.ts b/src/helpers/pid.ts index de31430..a55e8c6 100644 --- a/src/helpers/pid.ts +++ b/src/helpers/pid.ts @@ -23,7 +23,7 @@ */ import { resolve } from 'node:path'; import { existsSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs'; -import { ILogger } from '@imqueue/core'; +import { type ILogger } from '@imqueue/core'; /** * OS signals that should trigger pid file cleanup on process termination. diff --git a/test/IMQLock.spec.ts b/test/IMQLock.spec.ts index f1f35bd..5e4a0ca 100644 --- a/test/IMQLock.spec.ts +++ b/test/IMQLock.spec.ts @@ -24,7 +24,7 @@ import './mocks/index.js'; import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; -import { IMQLock, AcquiredLock } from '../index.js'; +import { IMQLock, type AcquiredLock } from '../index.js'; const LOCK_TIMEOUT = 100; const ORIGINAL_LOCK_TIMEOUT = IMQLock.deadlockTimeout; diff --git a/test/IMQService.afterCall.spec.ts b/test/IMQService.afterCall.spec.ts index b4faa6d..505cc96 100644 --- a/test/IMQService.afterCall.spec.ts +++ b/test/IMQService.afterCall.spec.ts @@ -27,7 +27,7 @@ import { randomUUID as uuid } from 'node:crypto'; import { logger } from './mocks/index.js'; import { IMQService, - IMQRPCRequest, + type IMQRPCRequest, expose, AFTER_HOOK_ERROR, } from '../index.js'; diff --git a/test/IMQService.console.logger.spec.ts b/test/IMQService.console.logger.spec.ts index 0f7d7cf..2f61b3a 100644 --- a/test/IMQService.console.logger.spec.ts +++ b/test/IMQService.console.logger.spec.ts @@ -6,7 +6,7 @@ import assert from 'node:assert/strict'; import { randomUUID as uuid } from 'node:crypto'; import { IMQService, - IMQRPCRequest, + type IMQRPCRequest, expose, BEFORE_HOOK_ERROR, AFTER_HOOK_ERROR, diff --git a/test/IMQService.hooks.spec.ts b/test/IMQService.hooks.spec.ts index 495be80..2ac8600 100644 --- a/test/IMQService.hooks.spec.ts +++ b/test/IMQService.hooks.spec.ts @@ -27,7 +27,7 @@ import { randomUUID as uuid } from 'node:crypto'; import { logger } from './mocks/index.js'; import { IMQService, - IMQRPCRequest, + type IMQRPCRequest, expose, BEFORE_HOOK_ERROR, } from '../index.js'; diff --git a/test/IMQService.logger.fallback.spec.ts b/test/IMQService.logger.fallback.spec.ts index 8bd1189..e09cf65 100644 --- a/test/IMQService.logger.fallback.spec.ts +++ b/test/IMQService.logger.fallback.spec.ts @@ -6,7 +6,7 @@ import assert from 'node:assert/strict'; import { randomUUID as uuid } from 'node:crypto'; import { IMQService, - IMQRPCRequest, + type IMQRPCRequest, expose, BEFORE_HOOK_ERROR, AFTER_HOOK_ERROR, diff --git a/test/IMQService.replyFailure.spec.ts b/test/IMQService.replyFailure.spec.ts index 2f61ebd..a5c9b48 100644 --- a/test/IMQService.replyFailure.spec.ts +++ b/test/IMQService.replyFailure.spec.ts @@ -5,7 +5,7 @@ import './mocks/index.js'; import { describe, it, afterEach, mock } from 'node:test'; import assert from 'node:assert/strict'; import { randomUUID as uuid } from 'node:crypto'; -import { IMQService, IMQRPCRequest, expose } from '../index.js'; +import { IMQService, type IMQRPCRequest, expose } from '../index.js'; class ReplyFailService extends IMQService { @expose() diff --git a/test/IMQService.spec.ts b/test/IMQService.spec.ts index 773f26e..9d7f4cf 100644 --- a/test/IMQService.spec.ts +++ b/test/IMQService.spec.ts @@ -24,7 +24,7 @@ import { logger } from './mocks/index.js'; import { describe, it, mock } from 'node:test'; import assert from 'node:assert/strict'; -import { IMQService, IMQRPCRequest, Description, expose } from '../index.js'; +import { IMQService, type IMQRPCRequest, Description, expose } from '../index.js'; import { randomUUID as uuid } from 'node:crypto'; // require (not import): at runtime node:cluster's module.exports is the Cluster diff --git a/test/cache/RedisCache.spec.ts b/test/cache/RedisCache.spec.ts index 8961f3c..2bfaf09 100644 --- a/test/cache/RedisCache.spec.ts +++ b/test/cache/RedisCache.spec.ts @@ -25,7 +25,7 @@ import { Redis, logger } from '../mocks/index.js'; import { describe, it, before, after, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; import { RedisCache } from '../../index.js'; -import { IRedisClient } from '@imqueue/core'; +import { type IRedisClient } from '@imqueue/core'; import { randomUUID as uuid } from 'node:crypto'; describe('cache/RedisCache', () => { diff --git a/test/decorators/cache.spec.ts b/test/decorators/cache.spec.ts index 3270b48..3411dd9 100644 --- a/test/decorators/cache.spec.ts +++ b/test/decorators/cache.spec.ts @@ -25,7 +25,7 @@ import { logger } from '../mocks/index.js'; import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; import { RedisCache, cache, IMQCache } from '../../index.js'; -import { ILogger } from '@imqueue/core'; +import { type ILogger } from '@imqueue/core'; class CacheTestClass { private logger: ILogger = logger; diff --git a/test/mocks/moduleMock.ts b/test/mocks/moduleMock.ts index 3ef988f..2e19231 100644 --- a/test/mocks/moduleMock.ts +++ b/test/mocks/moduleMock.ts @@ -21,7 +21,7 @@ * 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 `mock.module()` options for the running Node version. Node 24+ takes diff --git a/tsconfig.json b/tsconfig.json index a16cc1c..beaa969 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,14 @@ { "compilerOptions": { - "target": "es2023", - "lib": ["es2023", "esnext.decorators"], + "target": "es2024", + "lib": ["es2024", "esnext.decorators"], "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, "strictPropertyInitialization": false, "noImplicitOverride": true, From 4328fcbe64f40b6cb3a16fe3dca067dba65f4853 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 9 Jul 2026 22:15:44 +0200 Subject: [PATCH 3/5] feat: finalized esm transition --- package-lock.json | 11 +++++++---- package.json | 6 +++++- src/IMQCache.ts | 6 +++++- src/decorators/cache.ts | 6 +++++- test/IMQService.spec.ts | 7 ++++++- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 39ee4e9..2adb992 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "3.2.0", "license": "GPL-3.0-only", "dependencies": { - "@imqueue/core": "^3.2.0", + "@imqueue/core": "^3.2.1", "acorn": "^8.17.0", "typescript": "^7.0.2" }, @@ -17,12 +17,15 @@ "@types/node": "^24.9.1", "oxfmt": "0.57.0", "oxlint": "1.72.0" + }, + "engines": { + "node": ">=22.12.0" } }, "node_modules/@imqueue/core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@imqueue/core/-/core-3.2.0.tgz", - "integrity": "sha512-JKVV6/bYjhOWdD6tGr66X0EQRfysKw+QHiFUGcGw2ysZw/GAn5VKhjjfWbmjBh1polCFdmpRRuyUXOcUa4h2LA==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@imqueue/core/-/core-3.2.1.tgz", + "integrity": "sha512-iMRsXsHg9wS/vNKVw/2hVHdPfvzE6R8e5aAnE0n5bJyo7LP7MOgPGgb7rDpSbqVZwZoc7vJGeaW8MuBbDTJakQ==", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.11.1" diff --git a/package.json b/package.json index 0dc8b6e..0026a63 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "author": "imqueue.com (https://imqueue.com)", "license": "GPL-3.0-only", "dependencies": { - "@imqueue/core": "^3.2.0", + "@imqueue/core": "^3.2.1", "acorn": "^8.17.0", "typescript": "^7.0.2" }, @@ -59,5 +59,9 @@ "types": "./index.d.ts", "default": "./index.js" } + }, + "type": "module", + "engines": { + "node": ">=22.12.0" } } diff --git a/src/IMQCache.ts b/src/IMQCache.ts index c0addb4..e2ad745 100644 --- a/src/IMQCache.ts +++ b/src/IMQCache.ts @@ -22,7 +22,11 @@ * to get commercial licensing options. */ import { RedisCache } from './cache/index.js'; -import { type ICache, type ICacheAdapter, type ICacheConstructor } from './index.js'; +import { + type ICache, + type ICacheAdapter, + type ICacheConstructor, +} from './index.js'; // ES modules provide no synchronous dynamic loading, so the built-in cache // adapters registrable by name are enumerated statically; custom adapters diff --git a/src/decorators/cache.ts b/src/decorators/cache.ts index b4a74c8..a4ecbbd 100644 --- a/src/decorators/cache.ts +++ b/src/decorators/cache.ts @@ -23,7 +23,11 @@ */ // cache classes are imported from their defining modules (not the package // barrel) to avoid a circular-import TDZ on the module-scope default options -import { type ICache, type ICacheConstructor, RedisCache } from '../cache/index.js'; +import { + type ICache, + type ICacheConstructor, + RedisCache, +} from '../cache/index.js'; import { IMQCache } from '../IMQCache.js'; import { signature } from '../helpers/index.js'; diff --git a/test/IMQService.spec.ts b/test/IMQService.spec.ts index 9d7f4cf..baf0dbb 100644 --- a/test/IMQService.spec.ts +++ b/test/IMQService.spec.ts @@ -24,7 +24,12 @@ import { logger } from './mocks/index.js'; import { describe, it, mock } from 'node:test'; import assert from 'node:assert/strict'; -import { IMQService, type IMQRPCRequest, Description, expose } from '../index.js'; +import { + IMQService, + type IMQRPCRequest, + Description, + expose, +} from '../index.js'; import { randomUUID as uuid } from 'node:crypto'; // require (not import): at runtime node:cluster's module.exports is the Cluster From c51d022ae245f9509be2962ddb87cc84ecd5ce35 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 9 Jul 2026 22:24:36 +0200 Subject: [PATCH 4/5] fix: tests --- .../IMQClient.generator.trailing.args.spec.ts | 23 ++++++---- test/IMQClient.generator.types.spec.ts | 24 ++++++---- test/IMQClient.spec.ts | 23 ++++++---- test/helpers/pid.spec.ts | 22 +++++---- test/mocks/redis.ts | 46 +++++++++++++++---- 5 files changed, 95 insertions(+), 43 deletions(-) diff --git a/test/IMQClient.generator.trailing.args.spec.ts b/test/IMQClient.generator.trailing.args.spec.ts index 7bb07c2..39f6ff0 100644 --- a/test/IMQClient.generator.trailing.args.spec.ts +++ b/test/IMQClient.generator.trailing.args.spec.ts @@ -1,7 +1,14 @@ /*! * IMQClient generator trailing args removal coverage test */ -import * as fs from 'node:fs'; +import { + existsSync, + lstatSync, + readFileSync, + readdirSync, + rmdirSync, + unlinkSync, +} from 'node:fs'; import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; import { @@ -37,16 +44,16 @@ describe('IMQClient.generator trailing args removal (IMQDelay/IMQMetadata)', () let service: GenTrailingService; function rmdirr(path: string) { - if (fs.existsSync(path)) { - fs.readdirSync(path).forEach(file => { + if (existsSync(path)) { + readdirSync(path).forEach(file => { const curPath = `${path}/${file}`; - if (fs.lstatSync(curPath).isDirectory()) { + if (lstatSync(curPath).isDirectory()) { rmdirr(curPath); } else { - fs.unlinkSync(curPath); + unlinkSync(curPath); } }); - fs.rmdirSync(path); + rmdirSync(path); } } @@ -73,8 +80,8 @@ describe('IMQClient.generator trailing args removal (IMQDelay/IMQMetadata)', () // Read generated TypeScript to verify the signature const tsPath = `${CLIENTS_PATH}/GenTrailingService.ts`; - assert.equal(fs.existsSync(tsPath), true); - const src = fs.readFileSync(tsPath, 'utf8'); + assert.equal(existsSync(tsPath), true); + const src = readFileSync(tsPath, 'utf8'); // The generated method should not keep original parameter names 'meta' or 'delay' // as they are stripped and replaced by imqMetadata/imqDelay at the end once. diff --git a/test/IMQClient.generator.types.spec.ts b/test/IMQClient.generator.types.spec.ts index a013df9..dee41f8 100644 --- a/test/IMQClient.generator.types.spec.ts +++ b/test/IMQClient.generator.types.spec.ts @@ -22,7 +22,14 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import * as fs from 'node:fs'; +import { + existsSync, + lstatSync, + readFileSync, + readdirSync, + rmdirSync, + unlinkSync, +} from 'node:fs'; import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; import { @@ -81,17 +88,17 @@ class GenTypesService extends IMQService { } function rmdirr(path: string): void { - if (fs.existsSync(path)) { - fs.readdirSync(path).forEach(file => { + if (existsSync(path)) { + readdirSync(path).forEach(file => { const curPath = `${path}/${file}`; - if (fs.lstatSync(curPath).isDirectory()) { + if (lstatSync(curPath).isDirectory()) { rmdirr(curPath); } else { - fs.unlinkSync(curPath); + unlinkSync(curPath); } }); - fs.rmdirSync(path); + rmdirSync(path); } } @@ -118,10 +125,7 @@ describe('IMQClient.generator type interfaces', () => { assert.equal(result, null); - const src = fs.readFileSync( - `${CLIENTS_PATH}/GenTypesService.ts`, - 'utf8', - ); + const src = readFileSync(`${CLIENTS_PATH}/GenTypesService.ts`, 'utf8'); // registered types become interfaces; an inheriting type uses extends assert.match(src, /export interface GenPoint\s*\{/); diff --git a/test/IMQClient.spec.ts b/test/IMQClient.spec.ts index c75f71f..e9368fa 100644 --- a/test/IMQClient.spec.ts +++ b/test/IMQClient.spec.ts @@ -32,7 +32,14 @@ import { expose, remote, } from '../index.js'; -import * as fs from 'node:fs'; +import { + access, + existsSync, + lstatSync, + readdirSync, + rmdirSync, + unlinkSync, +} from 'node:fs'; // The generated client imports from '@imqueue/rpc'; the package.json `exports` // field lets that specifier self-resolve to this in-tree build, so no module @@ -73,18 +80,18 @@ class TestServiceClient extends IMQClient { } function rmdirr(path: string) { - if (fs.existsSync(path)) { - fs.readdirSync(path).forEach(file => { + if (existsSync(path)) { + readdirSync(path).forEach(file => { const curPath = `${path}/${file}`; - if (fs.lstatSync(curPath).isDirectory()) { + if (lstatSync(curPath).isDirectory()) { rmdirr(curPath); } else { - fs.unlinkSync(curPath); + unlinkSync(curPath); } }); - fs.rmdirSync(path); + rmdirSync(path); } } @@ -213,7 +220,7 @@ describe('IMQClient', () => { assert.ok(cli instanceof IMQClient); const notExists = await new Promise(resolve => - fs.access(`${CLIENTS_PATH}/TestService.ts`, resolve), + access(`${CLIENTS_PATH}/TestService.ts`, resolve), ); assert.equal(!notExists, true, 'TestService.ts does not exit'); @@ -237,7 +244,7 @@ describe('IMQClient', () => { assert.ok(cli instanceof IMQClient); const notExists = await new Promise(resolve => - fs.access(`${CLIENTS_PATH}/TestService.ts`, resolve), + access(`${CLIENTS_PATH}/TestService.ts`, resolve), ); assert.equal(!notExists, false, 'TestService.ts exits'); diff --git a/test/helpers/pid.spec.ts b/test/helpers/pid.spec.ts index f2b2265..71f9b9f 100644 --- a/test/helpers/pid.spec.ts +++ b/test/helpers/pid.spec.ts @@ -26,21 +26,27 @@ import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; import { pid, forgetPid, IMQ_TMP_DIR } from '../../src/helpers/index.js'; import { randomUUID as uuid } from 'node:crypto'; -import * as fs from 'node:fs'; +import { + existsSync, + lstatSync, + readdirSync, + rmdirSync, + unlinkSync, +} from 'node:fs'; function rmdirr(path: string) { - if (fs.existsSync(path)) { - fs.readdirSync(path).forEach(file => { + if (existsSync(path)) { + readdirSync(path).forEach(file => { const curPath = `${path}/${file}`; - if (fs.lstatSync(curPath).isDirectory()) { + if (lstatSync(curPath).isDirectory()) { rmdirr(curPath); } else { - fs.unlinkSync(curPath); + unlinkSync(curPath); } }); - fs.rmdirSync(path); + rmdirSync(path); } } @@ -72,7 +78,7 @@ describe('helpers/pid()', () => { assert.equal(pid(name, TEST_PID_DIR), 0); assert.equal(pid(name, TEST_PID_DIR), 1); assert.equal(pid(name, TEST_PID_DIR), 2); - fs.unlinkSync(`${TEST_PID_DIR}/${name}-1.pid`); + unlinkSync(`${TEST_PID_DIR}/${name}-1.pid`); assert.equal(pid(name, TEST_PID_DIR), 1); }); }); @@ -97,7 +103,7 @@ describe('helpers/forgetPid()', () => { forgetPid(name, id, logger, TEST_PID_DIR); - assert.ok(!fs.existsSync(`${TEST_PID_DIR}/${name}-0.pid`)); + assert.ok(!existsSync(`${TEST_PID_DIR}/${name}-0.pid`)); }); it('should ignore a missing pid file', () => { diff --git a/test/mocks/redis.ts b/test/mocks/redis.ts index df1fe4a..2e7f03f 100644 --- a/test/mocks/redis.ts +++ b/test/mocks/redis.ts @@ -24,10 +24,10 @@ import { mock } from 'node:test'; import { moduleMockOptions } from './moduleMock.js'; import { EventEmitter } from 'node:events'; -import * as crypto from 'node:crypto'; +import { createHash, type Hash } from 'node:crypto'; function sha1(str: string) { - let sha: crypto.Hash = crypto.createHash('sha1'); + let sha: Hash = createHash('sha1'); sha.update(str); return sha.digest('hex'); } @@ -52,7 +52,7 @@ export class RedisClientMock extends EventEmitter { RedisClientMock.__constructed++; setTimeout(() => { this.emit('ready', this); - }).unref(); + }); if (options.connectionName) { this.__name = options.connectionName; @@ -65,8 +65,25 @@ export class RedisClientMock extends EventEmitter { this.status = 'ready'; } - public end() {} - public async quit(): Promise {} + public end() { + this.disconnectMock(); + } + + public async quit(): Promise { + this.disconnectMock(); + } + + // Stops the blocking-pop polling loop: with the connection gone no + // timer may be rescheduled, so a finished test process can exit + // instead of being kept alive by an orphaned polling timeout. + private disconnectMock(): void { + this.connected = false; + + if (this.__rt) { + clearTimeout(this.__rt); + this.__rt = undefined; + } + } public async set(...args: any[]): Promise { let [key, val, units, expire, nx] = args; @@ -88,7 +105,7 @@ export class RedisClientMock extends EventEmitter { } setTimeout(() => { delete RedisClientMock.__keys[key]; - }, expire).unref(); + }, expire); } const cb = args.pop(); @@ -136,11 +153,17 @@ export class RedisClientMock extends EventEmitter { if (!q.length) { this.__rt && clearTimeout(this.__rt); + if (!this.connected) { + // disconnected: stay pending forever without scheduling a + // timer, so the event loop is free to drain + return new Promise(() => undefined); + } + return new Promise(resolve => { this.__rt = setTimeout( () => resolve(this.brpop(key, timeout, cb)), timeout || 100, - ).unref(); + ); }); } else { const result = [key, q.shift()]; @@ -164,11 +187,16 @@ export class RedisClientMock extends EventEmitter { if (!fromQ.length) { this.__rt && clearTimeout(this.__rt); + if (!this.connected) { + // see brpop(): no rescheduling after disconnect + return new Promise(() => undefined); + } + return new Promise(resolve => { this.__rt = setTimeout( () => resolve(this.brpoplpush(from, to, timeout, cb)), timeout || 100, - ).unref(); + ); }); } else { toQ.push(fromQ.shift()); @@ -290,7 +318,7 @@ export class RedisClientMock extends EventEmitter { setTimeout(() => { const toKey = key.split(/:/).slice(0, 2).join(':'); this.lpush(toKey, value); - }, timeout).unref(); + }, timeout); this.cbExecute(cb); return true; } From d5ec80346215fd409d81b082e855ee7699c61950 Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 9 Jul 2026 22:25:48 +0200 Subject: [PATCH 5/5] 3.2.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2adb992..03dedcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/rpc", - "version": "3.2.0", + "version": "3.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/rpc", - "version": "3.2.0", + "version": "3.2.1", "license": "GPL-3.0-only", "dependencies": { "@imqueue/core": "^3.2.1", diff --git a/package.json b/package.json index 0026a63..4cc6e0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/rpc", - "version": "3.2.0", + "version": "3.2.1", "description": "RPC-like client-service implementation over messaging queue", "keywords": [ "redis",