From 1180916556dc9c6d74915bc432dcab298734526d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 1 Apr 2026 04:58:00 +0000 Subject: [PATCH 01/11] chore(release): 1.0.0-alpha.1 [skip ci] # 1.0.0-alpha.1 (2026-04-01) ### Features * Initial release ([48f2043](https://github.com/TeamControlium/controlium-utils/commit/48f20436d59d309c506ca1d766f143fa79ee1085)) --- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7334f77 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# 1.0.0-alpha.1 (2026-04-01) + + +### Features + +* Initial release ([48f2043](https://github.com/TeamControlium/controlium-utils/commit/48f20436d59d309c506ca1d766f143fa79ee1085)) diff --git a/package-lock.json b/package-lock.json index c1f0f03..bf8677a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@controlium/utils", - "version": "0.0.0", + "version": "1.0.0-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@controlium/utils", - "version": "0.0.0", + "version": "1.0.0-alpha.1", "license": "MIT", "dependencies": { "@types/jsonpath-plus": "^5.0.2", diff --git a/package.json b/package.json index 149f963..a7c45de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@controlium/utils", - "version": "0.0.0", + "version": "1.0.0-alpha.1", "description": "Shared utilities for Controlium-based projects", "type": "module", "author": "Team Controllium contributors", From da295d073d7e21edacb4aef82e8ddee12cb86541 Mon Sep 17 00:00:00 2001 From: Mat Walker Date: Wed, 1 Apr 2026 18:16:06 +1000 Subject: [PATCH 02/11] fix: Trivial update to force alpha release --- src/utils/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 650b0ca..c97167d 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -14,10 +14,10 @@ import { StringUtils } from "../index"; // ─── Module-level constants ─────────────────────────────────────────────────── /** Milliseconds in one second. */ -const MS_PER_SECOND = 1000; +const MS_PER_SECOND = 1_000; /** Milliseconds in one day. */ -const MS_PER_DAY = 86400000; +const MS_PER_DAY = 86_400_000; /** * Prefix used when storing the original value of a modified environment variable. From d94b52949dc2be6271a0829eb242686f3831fea9 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 1 Apr 2026 08:20:52 +0000 Subject: [PATCH 03/11] chore(release): 1.0.2-alpha.1 [skip ci] ## [1.0.2-alpha.1](https://github.com/TeamControlium/controlium-utils/compare/v1.0.1...v1.0.2-alpha.1) (2026-04-01) ### Bug Fixes * Trivial update to force alpha release ([da295d0](https://github.com/TeamControlium/controlium-utils/commit/da295d073d7e21edacb4aef82e8ddee12cb86541)) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee0a8c7..eda71ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [1.0.2-alpha.1](https://github.com/TeamControlium/controlium-utils/compare/v1.0.1...v1.0.2-alpha.1) (2026-04-01) + + +### Bug Fixes + +* Trivial update to force alpha release ([da295d0](https://github.com/TeamControlium/controlium-utils/commit/da295d073d7e21edacb4aef82e8ddee12cb86541)) + ## [1.0.1](https://github.com/TeamControlium/controlium-utils/compare/v1.0.0...v1.0.1) (2026-04-01) diff --git a/package-lock.json b/package-lock.json index caf337b..1a43cab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@controlium/utils", - "version": "1.0.1", + "version": "1.0.2-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@controlium/utils", - "version": "1.0.1", + "version": "1.0.2-alpha.1", "license": "MIT", "dependencies": { "@types/jsonpath-plus": "^5.0.2", diff --git a/package.json b/package.json index ff38a0e..a7d2d40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@controlium/utils", - "version": "1.0.1", + "version": "1.0.2-alpha.1", "description": "Shared utilities for Controlium-based projects", "type": "module", "author": "Team Controllium contributors", From 9141c9a085f155cc907fcd25c7d4807f0e20da43 Mon Sep 17 00:00:00 2001 From: Mat Walker Date: Thu, 9 Apr 2026 18:21:27 +1000 Subject: [PATCH 04/11] chore: Updated detokeniser for sync/async use --- .vscode/settings.json | 3 + package-lock.json | 277 +++++++++++++---- package.json | 18 +- src/detokeniser/detokeniser.spec.ts | 444 ++++++++++++++++++++++++++++ src/detokeniser/detokeniser.ts | 256 ++++------------ tsconfig.build.json | 10 + tsconfig.cjs.json | 4 +- tsconfig.json | 8 +- tsconfig.spec.json | 10 + 9 files changed, 767 insertions(+), 263 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/detokeniser/detokeniser.spec.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.spec.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..25fa621 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/package-lock.json b/package-lock.json index caf337b..f0f3dad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "jest": "^29.6.0", "semantic-release": "^24.0.0", "ts-jest": "^29.4.2", - "typescript": "^5.9.1" + "typescript": "^6.0.2" } }, "node_modules/@babel/code-frame": { @@ -2119,28 +2119,47 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz", + "integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/utils": "8.48.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, "engines": { - "node": ">= 4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/parser": { + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz", - "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", + "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/project-service": "8.48.0", + "@typescript-eslint/tsconfig-utils": "8.48.0", "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0", "@typescript-eslint/visitor-keys": "8.48.0", - "debug": "^4.3.4" + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2150,11 +2169,10 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/project-service": { + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { "version": "8.48.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", @@ -2176,30 +2194,35 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/scope-manager": { + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz", - "integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", + "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", - "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz", + "integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==", "dev": true, "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2208,20 +2231,25 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/type-utils": { + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz", - "integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", + "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", "dev": true, "license": "MIT", "dependencies": { + "@typescript-eslint/project-service": "8.48.0", + "@typescript-eslint/tsconfig-utils": "8.48.0", "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0", - "@typescript-eslint/utils": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "engines": { @@ -2232,25 +2260,123 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/types": { + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz", - "integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", + "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.48.0", + "@typescript-eslint/types": "^8.48.0", + "debug": "^4.3.4" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree": { + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", + "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz", + "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { "version": "8.48.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", @@ -2278,7 +2404,46 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", + "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.48.0", + "@typescript-eslint/types": "^8.48.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", + "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", @@ -2288,7 +2453,7 @@ "balanced-match": "^1.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "node_modules/@typescript-eslint/parser/node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", @@ -2304,10 +2469,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "node_modules/@typescript-eslint/parser/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -2317,17 +2482,15 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/utils": { + "node_modules/@typescript-eslint/scope-manager": { "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz", - "integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz", + "integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0" + "@typescript-eslint/visitor-keys": "8.48.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2335,10 +2498,20 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz", + "integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/visitor-keys": { @@ -10799,9 +10972,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index ff38a0e..3acff94 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "scripts": { "clean": "rm -rf dist coverage", "build:cjs": "tsc -p tsconfig.cjs.json", - "build:esm": "tsc -p tsconfig.json", + "build:esm": "tsc -p tsconfig.build.json", "build": "npm run clean && npm run build:cjs && npm run build:esm", "test": "jest --silent --runInBand", "test:debug": "jest --runInBand", @@ -59,6 +59,11 @@ "ps-tree": "^1.2.0" }, "devDependencies": { + "@semantic-release/changelog": "^6.0.0", + "@semantic-release/commit-analyzer": "^13.0.0", + "@semantic-release/git": "^10.0.0", + "@semantic-release/npm": "^12.0.0", + "@semantic-release/release-notes-generator": "^14.0.0", "@types/jest": "^29.0.0", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.0.0", @@ -66,14 +71,9 @@ "@typescript-eslint/parser": "^8.0.0", "eslint": "^8.0.0", "jest": "^29.6.0", - "ts-jest": "^29.4.2", - "typescript": "^5.9.1", "semantic-release": "^24.0.0", - "@semantic-release/changelog": "^6.0.0", - "@semantic-release/commit-analyzer": "^13.0.0", - "@semantic-release/git": "^10.0.0", - "@semantic-release/npm": "^12.0.0", - "@semantic-release/release-notes-generator": "^14.0.0" + "ts-jest": "^29.4.2", + "typescript": "^6.0.2" }, "publishConfig": { "access": "public" @@ -88,7 +88,7 @@ ], "rootDir": "src", "transform": { - "^.+\\.(t|j)s$": "ts-jest" + "^.+\\.(t|j)s$": ["ts-jest", { "tsconfig": "tsconfig.spec.json" }] }, "transformIgnorePatterns": [], "coverageDirectory": "../coverage", diff --git a/src/detokeniser/detokeniser.spec.ts b/src/detokeniser/detokeniser.spec.ts new file mode 100644 index 0000000..ca3ba1d --- /dev/null +++ b/src/detokeniser/detokeniser.spec.ts @@ -0,0 +1,444 @@ +import { addDays, addMonths, addYears, format } from "date-fns"; + +import { Detokeniser } from "./detokeniser"; + +describe("Detokeniser", () => { + afterEach(() => { + Detokeniser.reset(); + }); + + // ─── Basic string manipulation ─────────────────────────────────────────────── + + describe("Basic string manipulation with no tokens", () => { + it("no token in string", () => { + expect(Detokeniser.do("No token here")).toStrictEqual("No token here"); + }); + + it("empty string", () => { + expect(Detokeniser.do("")).toStrictEqual(""); + }); + + it("no token but with an opener", () => { + expect(Detokeniser.do("No token [[here")).toStrictEqual("No token [[here"); + }); + + it("no token but with a closer", () => { + expect(Detokeniser.do("No token ]]here")).toStrictEqual("No token ]]here"); + }); + + it("string with unknown token throws", () => { + expect(() => Detokeniser.do("[[unknown]]")).toThrow( + "Error processing [[[unknown]]]: Unsupported token [unknown]" + ); + }); + + it("string with escaped token opener and closer", () => { + expect(Detokeniser.do("No /[[token/]] here")).toStrictEqual("No [[token]] here"); + }); + + it("string with escaped escape then escaped token opener and closer", () => { + expect(Detokeniser.do("No ///[[token/]] here")).toStrictEqual("No /[[token]] here"); + }); + + it("non-token text preserved around a resolved token", () => { + expect(Detokeniser.do("PREFIX-[[random|digits|4]]-SUFFIX")).toMatch(/^PREFIX-\d{4}-SUFFIX$/); + }); + }); + + // ─── Random token ──────────────────────────────────────────────────────────── + + describe("Random token", () => { + it("float - 1 decimal place, no rounding", () => { + expect(Detokeniser.do("[[random|float(100.4999,100.4999)|1]]")).toMatch(/^100\.4$/); + expect(Detokeniser.do("[[random|float(100.5,100.5)|2]]")).toMatch(/^100\.50$/); + }); + + it("float - 2 decimal places", () => { + expect(Detokeniser.do("[[random|float(100.3,100.4999)|2]]")).toMatch(/^100\.[34]\d$/); + }); + + it("from - single char from numeric set", () => { + expect(Detokeniser.do("[[random|from(1234567890)|1]]")).toMatch(/^\d$/); + }); + + it("from - 5 chars from alpha set", () => { + expect(Detokeniser.do("[[random|from(abcd)|5]]")).toMatch(/^[abcd]{5}$/); + }); + + it("from - with escaped token opener inside paren content", () => { + // /[[ inside from() is a literal [[ — result chars should include [ + expect(Detokeniser.do("[[random|from(ab/[[)|3]]")).toMatch(/^[ab[]{3}$/); + }); + + it("digits - 1 character", () => { + expect(Detokeniser.do("[[random|digits|1]]")).toMatch(/^[0-9]$/); + }); + + it("digits - 4 characters", () => { + expect(Detokeniser.do("[[random|digits|4]]")).toMatch(/^[0-9]{4}$/); + }); + + it("letters - 1 character", () => { + expect(Detokeniser.do("[[random|letters|1]]")).toMatch(/^[a-zA-Z]$/); + }); + + it("letters - 5 characters", () => { + expect(Detokeniser.do("[[random|letters|5]]")).toMatch(/^[a-zA-Z]{5}$/); + }); + + it("lowercaseletters - 6 characters", () => { + expect(Detokeniser.do("[[random|lowercaseletters|6]]")).toMatch(/^[a-z]{6}$/); + }); + + it("uppercaseletters - 10 characters", () => { + expect(Detokeniser.do("[[random|uppercaseletters|10]]")).toMatch(/^[A-Z]{10}$/); + }); + + it("alphanumerics - 10 characters", () => { + expect(Detokeniser.do("[[random|alphanumerics|10]]")).toMatch(/^[0-9a-zA-Z]{10}$/); + }); + + it("date - fixed range, formatted", () => { + // 21600000–21660000 ms epoch is entirely within 1970-01-01 + expect(Detokeniser.do("[[random|date(21600000,21660000)|dd-MM-yyyy]]")).toMatch(/^01-01-1970$/); + }); + + it("date - range returned as epoch within bounds", () => { + const result = parseInt(Detokeniser.do("[[random|date(21600000,108000000)|epoch]]")); + expect(result).toBeGreaterThanOrEqual(21600000); + expect(result).toBeLessThanOrEqual(108000000); + }); + }); + + // ─── Date token (sync) ─────────────────────────────────────────────────────── + + describe("Date token (sync)", () => { + it("fixed date as epoch", () => { + expect(Detokeniser.do("[[date|2013-4-5|epoch]]")).toBe("1365120000000"); + }); + + it("fixed date as second-epoch", () => { + expect(Detokeniser.do("[[date|2013-4-5|second-epoch]]")).toBe("1365120000"); + }); + + it("fixed date formatted", () => { + expect(Detokeniser.do("[[date|2013-4-5|MM=dd=yyyy]]")).toBe("04=05=2013"); + }); + + it("today returns today", () => { + const result = Detokeniser.do("[[date|today|yyyy-MM-dd]]"); + expect(result).toBe(format(new Date(), "yyyy-MM-dd")); + }); + + it("now returns today", () => { + const result = Detokeniser.do("[[date|now|yyyy-MM-dd]]"); + expect(result).toBe(format(new Date(), "yyyy-MM-dd")); + }); + + it("yesterday", () => { + const result = Detokeniser.do("[[date|yesterday|yyyy-MM-dd]]"); + expect(result).toBe(format(addDays(new Date(), -1), "yyyy-MM-dd")); + }); + + it("tomorrow", () => { + const result = Detokeniser.do("[[date|tomorrow|yyyy-MM-dd]]"); + expect(result).toBe(format(addDays(new Date(), 1), "yyyy-MM-dd")); + }); + + it("addYears", () => { + const result = Detokeniser.do("[[date|AddYears(3)|EEE MMM dd yyyy]]"); + expect(result).toBe(format(addYears(new Date(), 3), "EEE MMM dd yyyy")); + }); + + it("addMonths", () => { + const result = Detokeniser.do("[[date|AddMonths(3)|EEE MMM dd yyyy]]"); + expect(result).toBe(format(addMonths(new Date(), 3), "EEE MMM dd yyyy")); + }); + + it("addDays", () => { + const result = Detokeniser.do("[[date|AddDays(3)|EEE MMM dd yyyy]]"); + expect(result).toBe(format(addDays(new Date(), 3), "EEE MMM dd yyyy")); + }); + + it("addHours", () => { + // 0 hours → same date + const result = Detokeniser.do("[[date|AddHours(0)|yyyy-MM-dd]]"); + expect(result).toBe(format(new Date(), "yyyy-MM-dd")); + }); + + it("addMinutes", () => { + // 0 minutes → same date + const result = Detokeniser.do("[[date|AddMinutes(0)|yyyy-MM-dd]]"); + expect(result).toBe(format(new Date(), "yyyy-MM-dd")); + }); + + it("random date within a fixed epoch range", () => { + // 946684800000 = 2000-01-01 UTC, 946771200000 = 2000-01-02 UTC + const result = Detokeniser.do("[[date|random(946684800000,946771200000)|yyyy-MM-dd]]"); + expect(result).toMatch(/^2000-01-0[12]$/); + }); + + it("followingDay - following Friday after Sunday 2013-04-07 is 2013-04-12", () => { + // 1365292800000 = 2013-04-07 00:00:00 UTC + expect(Detokeniser.do("[[date|followingDay(1365292800000,friday)|yyyy-MM-dd]]")).toBe("2013-04-12"); + }); + + it("addWorkingDays throws - requires doAsync", () => { + expect(() => Detokeniser.do("[[date|addWorkingDays(5)|dd-MM-yyyy]]")).toThrow( + "addworkingdays uses asynchoronous calls" + ); + }); + + it("followingWorkingDay throws - requires doAsync", () => { + expect(() => Detokeniser.do("[[date|followingWorkingDay(1234567890,wednesday)|dd-MM-yyyy]]")).toThrow( + "followingworkingday uses asynchoronous calls" + ); + }); + + it("nextPublicHoliday throws - requires doAsync", () => { + expect(() => Detokeniser.do("[[date|nextPublicHoliday(1234567890,90)|dd-MM-yyyy]]")).toThrow( + "nextpublicholiday uses asynchoronous calls" + ); + }); + }); + + // ─── Date token (async) ────────────────────────────────────────────────────── + + describe("Date token (async)", () => { + it("fixed date as epoch", async () => { + expect(await Detokeniser.doAsync("[[date|2013-4-5|epoch]]")).toBe("1365120000000"); + }); + + it("fixed date formatted", async () => { + expect(await Detokeniser.doAsync("[[date|2013-4-5|MM=dd=yyyy]]")).toBe("04=05=2013"); + }); + + it("today", async () => { + const result = await Detokeniser.doAsync("[[date|today|yyyy-MM-dd]]"); + expect(result).toBe(format(new Date(), "yyyy-MM-dd")); + }); + + it("addDays", async () => { + const result = await Detokeniser.doAsync("[[date|AddDays(5)|EEE MMM dd yyyy]]"); + expect(result).toBe(format(addDays(new Date(), 5), "EEE MMM dd yyyy")); + }); + }); + + // ─── Nested tokens ─────────────────────────────────────────────────────────── + + describe("Nested tokens", () => { + it("random digit nested inside date addDays", () => { + const result = Detokeniser.do("[[date|addDays([[random|digits|1]])|yyyy-MM-dd]]"); + expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/); + }); + + it("nested date epoch inside date random", () => { + // Innermost [[date|...]] tokens resolve to epoch strings first, then outer random picks between them + const result = Detokeniser.do( + "[[date|random([[date|2000-01-01|epoch]],[[date|2000-01-02|epoch]])|yyyy-MM-dd]]" + ); + expect(result).toMatch(/^2000-01-0[12]$/); + }); + + it("async: nested tokens resolve innermost first", async () => { + const result = await Detokeniser.doAsync( + "[[date|random([[date|2000-01-01|epoch]],[[date|2000-01-02|epoch]])|yyyy-MM-dd]]" + ); + expect(result).toMatch(/^2000-01-0[12]$/); + }); + }); + + // ─── BASE64 token ──────────────────────────────────────────────────────────── + + describe("BASE64 token", () => { + it("encode", () => { + const value = "Hello World"; + const encoded = Detokeniser.do(`[[base64|encode|${value}]]`); + expect(Buffer.from(encoded, "base64").toString()).toBe(value); + }); + + it("encode value containing pipe character", () => { + // pipe is the 3rd segment so splitRemaining handles it correctly + const value = "Hello|World"; + const encoded = Detokeniser.do(`[[base64|encode|${value}]]`); + expect(Buffer.from(encoded, "base64").toString()).toBe(value); + }); + + it("decode", () => { + const value = "Hello World"; + const encoded = Buffer.from(value).toString("base64"); + expect(Detokeniser.do(`[[base64|decode|${encoded}]]`)).toBe(value); + }); + + it("decode value containing pipe character in original", () => { + const value = "Hello|World"; + const encoded = Buffer.from(value).toString("base64"); + expect(Detokeniser.do(`[[base64|decode|${encoded}]]`)).toBe(value); + }); + + it("encode/decode roundtrip via nested tokens", () => { + const value = "roundtrip test 123"; + const encoded = Detokeniser.do(`[[base64|encode|${value}]]`); + const decoded = Detokeniser.do(`[[base64|decode|${encoded}]]`); + expect(decoded).toBe(value); + }); + + it("invalid direction throws", () => { + expect(() => Detokeniser.do("[[base64|baddir|something]]")).toThrow(); + }); + }); + + // ─── JWT token ─────────────────────────────────────────────────────────────── + + describe("JWT token", () => { + it("full JWT with payload, signature and options", () => { + const result = Detokeniser.do('[[jwt|{"sub":"1234","name":"Test"}|MySecret|{"algorithm":"HS256"}]]'); + expect(result).toBeDefined(); + expect(result.split(".").length).toBe(3); + }); + + it("JWT with payload and signature, no options", () => { + const result = Detokeniser.do('[[jwt|{"sub":"1234"}|MySecret]]'); + expect(result).toBeDefined(); + expect(result.split(".").length).toBe(3); + }); + + it("JWT with payload only, no signature or options", () => { + const result = Detokeniser.do('[[jwt|{"sub":"1234","name":"Test"}]]'); + expect(result).toBeDefined(); + expect(result.split(".").length).toBe(3); + }); + }); + + // ─── Setting token ─────────────────────────────────────────────────────────── + + describe("Setting token", () => { + it("invalid postamble (not key:value JSON) throws", () => { + expect(() => Detokeniser.do("[[setting|invalid]]")).toThrow(); + }); + + it("reads from process env", () => { + const envName = "DETOKENISER_SPEC_TEST_VAR"; + const envValue = "hello from env"; + process.env[envName] = envValue; + try { + expect(Detokeniser.do(`[[setting|processEnvName: "${envName}"]]`)).toBe(envValue); + } finally { + delete process.env[envName]; + } + }); + + it("reads from contextParameters", () => { + const result = Detokeniser.do('[[setting|profileParameterName: "myParam"]]', { + contextParameters: { myParam: "contextValue" }, + }); + expect(result).toBe("contextValue"); + }); + + it("uses defaultValue when env var not set", () => { + const result = Detokeniser.do( + '[[setting|processEnvName: "DETOKENISER_SPEC_DEFINITELY_NOT_SET_XYZ", defaultValue: "fallback"]]' + ); + expect(result).toBe("fallback"); + }); + }); + + // ─── mockintercepts token ──────────────────────────────────────────────────── + + describe("mockintercepts token", () => { + it("throws when no intercepted requests are available", () => { + expect(() => Detokeniser.do("[[mockintercepts|$.requests[0].body.id]]")).toThrow( + "No Mock Intercepted requests to harvest from!?" + ); + }); + }); + + // ─── Callbacks ─────────────────────────────────────────────────────────────── + + describe("Callbacks", () => { + afterEach(() => { + Detokeniser.resetCallbacks(); + }); + + it("sync callback receives full token body and its return value is used", () => { + let capturedToken: string | undefined; + Detokeniser.addCallback((token: string) => { + capturedToken = token; + return "syncResult"; + }); + const result = Detokeniser.do("[[mytoken]]"); + expect(capturedToken).toBe("mytoken"); + expect(result).toBe("syncResult"); + }); + + it("sync callback receives token with multiple pipe-separated parts", () => { + let capturedToken: string | undefined; + Detokeniser.addCallback((token: string) => { + capturedToken = token; + return "ok"; + }); + Detokeniser.do("[[mytype|arg1|arg2]]"); + expect(capturedToken).toBe("mytype|arg1|arg2"); + }); + + it("first callback returning undefined passes to second", () => { + Detokeniser.addCallback((token: string) => (token === "first" ? "one" : undefined)); + Detokeniser.addCallback((token: string) => (token === "second" ? "two" : undefined)); + expect(Detokeniser.do("[[first]]")).toBe("one"); + expect(Detokeniser.do("[[second]]")).toBe("two"); + }); + + it("all callbacks returning undefined throws unsupported token", () => { + Detokeniser.addCallback(() => undefined); + expect(() => Detokeniser.do("[[anything]]")).toThrow("Unsupported token"); + }); + + it("async callback is skipped by do() and throws unsupported token", () => { + Detokeniser.addCallback(async () => "asyncResult"); + expect(() => Detokeniser.do("[[anything]]")).toThrow("Unsupported token"); + }); + + it("async callback receives full token body and its return value is used by doAsync", async () => { + let capturedToken: string | undefined; + Detokeniser.addCallback(async (token: string) => { + capturedToken = token; + return "asyncResult"; + }); + const result = await Detokeniser.doAsync("[[asynctoken]]"); + expect(capturedToken).toBe("asynctoken"); + expect(result).toBe("asyncResult"); + }); + + it("async callback receives token with multiple pipe-separated parts via doAsync", async () => { + let capturedToken: string | undefined; + Detokeniser.addCallback(async (token: string) => { + capturedToken = token; + return "ok"; + }); + await Detokeniser.doAsync("[[asynctype|arg1|arg2]]"); + expect(capturedToken).toBe("asynctype|arg1|arg2"); + }); + + it("sync callback also works via doAsync", async () => { + Detokeniser.addCallback((token: string) => (token === "mytoken" ? "syncResult" : undefined)); + const result = await Detokeniser.doAsync("[[mytoken]]"); + expect(result).toBe("syncResult"); + }); + + it("resetCallbacks clears all callbacks", async () => { + Detokeniser.addCallback(() => "sync"); + Detokeniser.addCallback(async () => "async"); + Detokeniser.resetCallbacks(); + expect(() => Detokeniser.do("[[anything]]")).toThrow("Unsupported token"); + await expect(Detokeniser.doAsync("[[anything]]")).rejects.toThrow("Unsupported token"); + }); + + it("reset clears all callbacks", async () => { + Detokeniser.addCallback(() => "sync"); + Detokeniser.addCallback(async () => "async"); + Detokeniser.reset(); + expect(() => Detokeniser.do("[[anything]]")).toThrow("Unsupported token"); + await expect(Detokeniser.doAsync("[[anything]]")).rejects.toThrow("Unsupported token"); + }); + }); +}); diff --git a/src/detokeniser/detokeniser.ts b/src/detokeniser/detokeniser.ts index 2566266..0648f3f 100644 --- a/src/detokeniser/detokeniser.ts +++ b/src/detokeniser/detokeniser.ts @@ -57,9 +57,6 @@ interface DoTokenPreambleReturn { * - **expression** — what to compute (type-specific) * - **format** — how to format the result (type-specific, often optional) * - * The delimiter `|` and the `[[` / `]]` endstops are configurable via {@link Detokeniser.delimiter} and - * {@link Detokeniser.tokenStartEndChars}, but the defaults cover the vast majority of use cases. - * * --- * ## Built-in token types * @@ -164,88 +161,28 @@ interface DoTokenPreambleReturn { * * --- * ## Extending with callbacks - * Custom token types are registered via {@link Detokeniser.addCallbackSync} (for sync processing) or - * {@link Detokeniser.addCallbackAsync} (for async processing). Callbacks are tried in registration - * order; return `undefined` to pass to the next callback. If all callbacks return `undefined` and no - * built-in handler matched, an error is thrown. + * Custom token types are registered via {@link Detokeniser.addCallback}. Both sync and async handlers + * share the same registration method and callback list. Callbacks are tried in registration order; + * return `undefined` to pass to the next callback. If all callbacks return `undefined` and no built-in + * handler matched, an error is thrown. + * + * Async callbacks are silently skipped when {@link Detokeniser.do} (sync) is used — use + * {@link Detokeniser.doAsync} if your callback returns a Promise. * - * @see {@link Detokeniser.addCallbackSync} - * @see {@link Detokeniser.addCallbackAsync} + * @see {@link Detokeniser.addCallback} * @see {@link Detokeniser.do} * @see {@link Detokeniser.doAsync} */ export class Detokeniser { - private static _endTokenChar = "]]"; - private static _startTokenChar = "[["; + private static readonly _endTokenChar = "]]"; + private static readonly _startTokenChar = "[["; private static EscapeChar = "/"; - private static _delimiter = "|"; - - private static _asyncCallbacks: Array | undefined = undefined; - private static _syncCallbacks: Array | undefined = undefined; - - /** - * Gets the current single-character delimiter used to separate token parts. - * Default: `|` - */ - public static get delimiter(): string { - return this._delimiter; - } + private static readonly _delimiter = "|"; - /** - * Sets the single-character delimiter used to separate token parts. - * The new value applies to all subsequent {@link Detokeniser.do} / {@link Detokeniser.doAsync} calls. - * @param newDelimiter - Exactly one character - * @throws If `newDelimiter` is not exactly one character - * @example - * Detokeniser.delimiter = ':'; - * Detokeniser.do('[[random:digits:6]]'); // → e.g. '482910' - * Detokeniser.reset(); // restore default '|' - */ - public static set delimiter(newDelimiter: string) { - if (newDelimiter.length !== 1) { - const errTxt = `Invalid delimiter [${newDelimiter}]. Must be exactly 1 character!`; - Log.writeLine(LogLevels.Error, errTxt); - throw new Error(errTxt); - } else { - this._delimiter = newDelimiter; - } - } - - /** - * Sets the start and end token sequences. - * @param startEndChars - An even-length string whose first half is the start sequence and second half the end sequence. - * @example Detokeniser.tokenStartEndChars = "[[]]"; // start = "[[", end = "]]" - * @remarks Start and end sequences must differ. Minimum total length is 2 (one char each). - */ - public static set tokenStartEndChars(startEndChars: string) { - if (startEndChars.length < 2 || startEndChars.length % 2 !== 0) { - const errTxt = `Invalid start/end chars [${startEndChars}]. Must be an even number of characters (minimum 2) — first half as start sequence, second half as end sequence!`; - Log.writeLine(LogLevels.Error, errTxt); - throw new Error(errTxt); - } - const half = startEndChars.length / 2; - const start = startEndChars.substring(0, half); - const end = startEndChars.substring(half); - if (start === end) { - const errTxt = `Invalid start/end chars — start sequence [${start}] must differ from end sequence [${end}]!`; - Log.writeLine(LogLevels.Error, errTxt); - throw new Error(errTxt); - } - this._startTokenChar = start; - this._endTokenChar = end; - } - - /** - * Gets the current token start/end sequences concatenated (e.g. `"[[]]"`). - */ - public static get tokenStartEndChars(): string { - return this._startTokenChar + this._endTokenChar; - } + private static _callbacks: Array | undefined = undefined; /** * Resets the Detokeniser to factory defaults: - * - Token endstops restored to `[[` / `]]` - * - Delimiter restored to `|` * - Escape char restored to `/` * - All registered sync and async callbacks cleared * @@ -254,125 +191,60 @@ export class Detokeniser { * afterEach(() => Detokeniser.reset()); */ public static reset() { - this._endTokenChar = "]]"; - this._startTokenChar = "[["; this.EscapeChar = "/"; - this._delimiter = "|"; - if (this._syncCallbacks) { - this._syncCallbacks = []; - } - if (this._asyncCallbacks) { - this._asyncCallbacks = []; - } - this._asyncCallbacks = undefined; - this._syncCallbacks = undefined; + this._callbacks = undefined; } /** - * Registers a synchronous custom token handler. + * Registers a custom token handler. Both sync and async handlers are registered via this method + * and share the same callback list. + * + * Callbacks are tried in registration order. The first to return a non-`undefined` value wins. + * Return `undefined` to pass to the next callback. If all callbacks return `undefined` and no + * built-in handler matched, an error is thrown. * - * When {@link Detokeniser.do} encounters a token not handled by the built-in set, it invokes each - * registered sync callback in registration order. The first to return a non-`undefined` string wins. - * Return `undefined` to pass to the next callback. If all callbacks return `undefined`, an error is thrown. + * Async callbacks (those returning a `Promise`) are silently skipped when {@link Detokeniser.do} + * is used — register an async callback only if you intend to call {@link Detokeniser.doAsync}. * - * @param callback - `(delimiter: string, token: string) => string | undefined` - * - `delimiter` — current delimiter character (default `|`) - * - `token` — full token body without `[[` / `]]`, e.g. `"mytype|arg1|arg2"` + * @param callback - `(token: string) => string | undefined | Promise` + * - `token` — full token body without `[[` / `]]`, e.g. `"mytype|arg1|arg2"` (delimiter is always `|`) * * @example - * // Handle [[env|VAR_NAME]] tokens - * Detokeniser.addCallbackSync((delimiter, token) => { - * const [type, name] = token.split(delimiter); - * if (type.toLowerCase() !== 'env') return undefined; + * // Sync handler for [[env|VAR_NAME]] tokens + * Detokeniser.addCallback((token) => { + * const [type, name] = token.split('|'); + * if (type !== 'env') return undefined; * return process.env[name] ?? ''; * }); * Detokeniser.do('Path: [[env|HOME]]'); // → 'Path: /home/user' * * @example - * // Multiple callbacks — each handles one type, passes on the rest - * Detokeniser.addCallbackSync((delimiter, token) => { - * const [type, value] = token.split(delimiter); - * if (type === 'upper') return value.toUpperCase(); - * return undefined; - * }); - * Detokeniser.addCallbackSync((delimiter, token) => { - * const [type, value] = token.split(delimiter); - * if (type === 'lower') return value.toLowerCase(); - * return undefined; - * }); - * Detokeniser.do('[[upper|hello]] [[lower|WORLD]]'); // → 'HELLO world' - * - * @see {@link Detokeniser.resetSyncCallbacks} to remove all sync callbacks - * @see {@link Detokeniser.addCallbackAsync} for async token handlers - */ - public static addCallbackSync(callback: Detokeniser.Callback_Sync) { - if (!this._syncCallbacks) { - this._syncCallbacks = new Array(); - } - this._syncCallbacks.push(callback); - } - - /** - * Registers an asynchronous custom token handler. - * - * Works identically to {@link Detokeniser.addCallbackSync} but is invoked by {@link Detokeniser.doAsync}. - * Async callbacks are tried in registration order; the first to return a non-`undefined` value wins. - * Return `undefined` to pass to the next callback. - * - * @param asyncCallback - `(delimiter: string, token: string) => Promise` - * - `delimiter` — current delimiter character (default `|`) - * - `token` — full token body without `[[` / `]]`, e.g. `"mytype|arg1|arg2"` - * - * @example - * // Handle [[db|table|column|whereClause]] tokens - * Detokeniser.addCallbackAsync(async (delimiter, token) => { - * const [type, table, column, where] = token.split(delimiter); - * if (type.toLowerCase() !== 'db') return undefined; + * // Async handler for [[db|table|column|where]] tokens + * Detokeniser.addCallback(async (token) => { + * const [type, table, column, where] = token.split('|'); + * if (type !== 'db') return undefined; * const row = await db.query(`SELECT ${column} FROM ${table} WHERE ${where} LIMIT 1`); * return String(row[column]); * }); * const result = await Detokeniser.doAsync('ID: [[db|users|id|active=1]]'); * - * @example - * // Combine with nested tokens — inner tokens resolve before the callback is called - * Detokeniser.addCallbackAsync(async (delimiter, token) => { - * const [type, key] = token.split(delimiter); - * if (type !== 'cache') return undefined; - * return await redis.get(key); - * }); - * // [[random|digits|8]] resolves first, then [[cache|...]] receives the result - * await Detokeniser.doAsync('Val: [[cache|prefix-[[random|digits|8]]]]'); - * - * @see {@link Detokeniser.resetAsyncCallbacks} to remove all async callbacks - * @see {@link Detokeniser.addCallbackSync} for synchronous token handlers + * @see {@link Detokeniser.resetCallbacks} to remove all registered callbacks + * @see {@link Detokeniser.doAsync} for async token resolution */ - public static addCallbackAsync(asyncCallback: Detokeniser.Callback_Async) { - if (!this._asyncCallbacks) { - this._asyncCallbacks = new Array(); + public static addCallback(callback: Detokeniser.Callback) { + if (!this._callbacks) { + this._callbacks = new Array(); } - this._asyncCallbacks?.push(asyncCallback); + this._callbacks.push(callback); } /** - * Removes all registered sync callbacks. Built-in token handlers are unaffected. + * Removes all registered callbacks. Built-in token handlers are unaffected. * Use between tests or scenarios to ensure callback isolation. - * @see {@link Detokeniser.reset} to also clear async callbacks and restore all defaults + * @see {@link Detokeniser.reset} to also restore all defaults */ - public static resetSyncCallbacks() { - if (this._syncCallbacks) { - this._syncCallbacks = []; - } - } - - /** - * Removes all registered async callbacks. Built-in token handlers are unaffected. - * Use between tests or scenarios to ensure callback isolation. - * @see {@link Detokeniser.reset} to also clear sync callbacks and restore all defaults - */ - public static resetAsyncCallbacks() { - if (this._asyncCallbacks) { - this._asyncCallbacks = []; - } + public static resetCallbacks() { + this._callbacks = undefined; } /** @@ -467,8 +339,8 @@ export class Detokeniser { * * @example * // Async callback for database-driven tokens - * Detokeniser.addCallbackAsync(async (delim, token) => { - * const [type, key] = token.split(delim); + * Detokeniser.addCallbackAsync(async (token) => { + * const [type, key] = token.split('|'); * if (type !== 'db') return undefined; * return await fetchFromDatabase(key); * }); @@ -616,9 +488,11 @@ export class Detokeniser { // try { if (Utils.isNullOrUndefined(processedToken)) { - if (typeof this._syncCallbacks != "undefined") { - this._syncCallbacks.every((callback) => { - processedToken = callback(this._delimiter, token); + if (typeof this._callbacks != "undefined") { + this._callbacks.every((callback) => { + const result = callback(token); + if (result instanceof Promise) return true; // skip async callbacks in sync context + processedToken = result; return Utils.isNullOrUndefined(processedToken); }); } @@ -647,9 +521,9 @@ export class Detokeniser { // try { if (Utils.isNullOrUndefined(processedToken)) { - if (typeof this._asyncCallbacks != "undefined") { - for (let i = 0; i < this._asyncCallbacks.length; i++) { - processedToken = await this._asyncCallbacks[i](this._delimiter, token); + if (typeof this._callbacks != "undefined") { + for (const callback of this._callbacks) { + processedToken = await Promise.resolve(callback(token)); if (!Utils.isNullOrUndefined(processedToken)) break; } } @@ -901,7 +775,7 @@ export class Detokeniser { case "addhours": returnDate = addHours(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime(); break; - case "addMinutes": + case "addminutes": returnDate = addMinutes(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime(); break; case "followingday": { @@ -1234,25 +1108,16 @@ class InnermostToken { // ─── Detokeniser types ──────────────────────────────────────────────────────── /** - * Signature for an asynchronous custom token handler registered via {@link Detokeniser.addCallbackAsync}. + * Signature for a custom token handler registered via {@link Detokeniser.addCallback}. * - * @param delimiter - Current delimiter character (default `|`) - * @param token - Full token body without `[[` / `]]` endstops, e.g. `"mytype|arg1|arg2"` - * @returns Promise resolving to the replacement string, or `undefined` to pass to the next handler - */ -export interface Detokeniser_Callback_Async { - (delimiter: string, token: string): Promise; -} - -/** - * Signature for a synchronous custom token handler registered via {@link Detokeniser.addCallbackSync}. + * May be synchronous or asynchronous. Async callbacks (returning a `Promise`) are silently skipped + * by {@link Detokeniser.do} and only invoked by {@link Detokeniser.doAsync}. * - * @param delimiter - Current delimiter character (default `|`) - * @param token - Full token body without `[[` / `]]` endstops, e.g. `"mytype|arg1|arg2"` - * @returns The replacement string, or `undefined` to pass to the next handler + * @param token - Full token body without `[[` / `]]` endstops, e.g. `"mytype|arg1|arg2"` (delimiter is always `|`) + * @returns The replacement string, a Promise resolving to it, or `undefined` to pass to the next handler */ -export interface Detokeniser_Callback_Sync { - (delimiter: string, token: string): string | undefined; +export interface Detokeniser_Callback { + (token: string): string | undefined | Promise; } /** @@ -1269,7 +1134,6 @@ export interface Detokeniser_DoOptions { // eslint-disable-next-line @typescript-eslint/no-namespace export namespace Detokeniser { - export type Callback_Async = Detokeniser_Callback_Async; - export type Callback_Sync = Detokeniser_Callback_Sync; + export type Callback = Detokeniser_Callback; export type DoOptions = Detokeniser_DoOptions; } diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..9cb4cc5 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "declaration": true, + "declarationDir": "dist/types", + "outDir": "dist/esm" + }, + "exclude": ["**/*.spec.ts", "**/*.test.ts"] +} diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json index 6c3026c..f516d42 100644 --- a/tsconfig.cjs.json +++ b/tsconfig.cjs.json @@ -1,7 +1,9 @@ { - "extends": "./tsconfig.json", + "extends": "./tsconfig.build.json", "compilerOptions": { "module": "CommonJS", + "moduleResolution": "Node10", + "ignoreDeprecations": "6.0", "outDir": "dist/cjs" } } diff --git a/tsconfig.json b/tsconfig.json index 694136d..87e7f48 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,14 +2,12 @@ "compilerOptions": { "target": "ES2020", "module": "ESNext", - "declaration": true, - "declarationDir": "dist/types", - "outDir": "dist/esm", "strict": true, "esModuleInterop": true, - "moduleResolution": "Node", + "moduleResolution": "Bundler", "skipLibCheck": true, - "forceConsistentCasingInFileNames": true + "forceConsistentCasingInFileNames": true, + "types": ["jest", "node"] }, "include": ["src"] } diff --git a/tsconfig.spec.json b/tsconfig.spec.json new file mode 100644 index 0000000..2592c76 --- /dev/null +++ b/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "Node10", + "ignoreDeprecations": "6.0", + "noEmit": true + }, + "include": ["src"] +} From e06159f65d4362fe831cf9b43c3f513096e5da01 Mon Sep 17 00:00:00 2001 From: Mat Walker Date: Thu, 9 Apr 2026 18:39:15 +1000 Subject: [PATCH 05/11] chore: Fix typescript 6.0.2 related issues --- package-lock.json | 441 +++++++++++++++------------------------------- package.json | 6 +- 2 files changed, 143 insertions(+), 304 deletions(-) diff --git a/package-lock.json b/package-lock.json index a968536..940d0e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,12 +30,12 @@ "@types/jest": "^29.0.0", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.0.0", - "@typescript-eslint/eslint-plugin": "^8.0.0", - "@typescript-eslint/parser": "^8.0.0", + "@typescript-eslint/eslint-plugin": "^8.58.0", + "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.0.0", "jest": "^29.6.0", "semantic-release": "^24.0.0", - "ts-jest": "^29.4.2", + "ts-jest": "^29.4.9", "typescript": "^6.0.2" } }, @@ -547,9 +547,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2090,21 +2090,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz", - "integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", + "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.48.0", - "@typescript-eslint/type-utils": "8.48.0", - "@typescript-eslint/utils": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/type-utils": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2114,52 +2113,33 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.48.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@typescript-eslint/parser": "^8.58.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz", - "integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0", - "@typescript-eslint/utils": "8.48.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "node": ">= 4" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", - "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", + "node_modules/@typescript-eslint/parser": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz", + "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.48.0", - "@typescript-eslint/tsconfig-utils": "8.48.0", - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2169,19 +2149,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", - "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz", + "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.48.0", - "@typescript-eslint/types": "^8.48.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.58.1", + "@typescript-eslint/types": "^8.58.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2191,37 +2172,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", - "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz", - "integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz", + "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.48.0", - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0" + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2229,29 +2191,14 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", - "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz", + "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.48.0", - "@typescript-eslint/tsconfig-utils": "8.48.0", - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2260,19 +2207,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", - "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz", + "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.48.0", - "@typescript-eslint/types": "^8.48.0", - "debug": "^4.3.4" + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2282,13 +2231,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", - "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", + "node_modules/@typescript-eslint/types": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", + "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==", "dev": true, "license": "MIT", "engines": { @@ -2297,72 +2247,24 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz", + "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz", - "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.48.0", - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0", - "debug": "^4.3.4" + "@typescript-eslint/project-service": "8.58.1", + "@typescript-eslint/tsconfig-utils": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2372,104 +2274,49 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", - "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.48.0", - "@typescript-eslint/tsconfig-utils": "8.48.0", - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "node": "18 || 20 || >=22" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", - "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.48.0", - "@typescript-eslint/types": "^8.48.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "balanced-match": "^4.0.2" }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", - "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", - "dev": true, - "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "node": "18 || 20 || >=22" } }, - "node_modules/@typescript-eslint/parser/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/parser/node_modules/semver": { + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", @@ -2482,15 +2329,17 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz", - "integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==", + "node_modules/@typescript-eslint/utils": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz", + "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2498,31 +2347,21 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz", - "integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz", - "integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz", + "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.58.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2533,13 +2372,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -5868,15 +5707,15 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "dev": true, "license": "MIT" }, @@ -10844,9 +10683,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -10857,19 +10696,19 @@ } }, "node_modules/ts-jest": { - "version": "29.4.6", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", - "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "version": "29.4.9", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz", + "integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==", "dev": true, "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", - "handlebars": "^4.7.8", + "handlebars": "^4.7.9", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.7.3", + "semver": "^7.7.4", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, @@ -10886,7 +10725,7 @@ "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", - "typescript": ">=4.3 <6" + "typescript": ">=4.3 <7" }, "peerDependenciesMeta": { "@babel/core": { @@ -10910,9 +10749,9 @@ } }, "node_modules/ts-jest/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { diff --git a/package.json b/package.json index 2a69a02..4d2747c 100644 --- a/package.json +++ b/package.json @@ -67,12 +67,12 @@ "@types/jest": "^29.0.0", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.0.0", - "@typescript-eslint/eslint-plugin": "^8.0.0", - "@typescript-eslint/parser": "^8.0.0", + "@typescript-eslint/eslint-plugin": "^8.58.0", + "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.0.0", "jest": "^29.6.0", "semantic-release": "^24.0.0", - "ts-jest": "^29.4.2", + "ts-jest": "^29.4.9", "typescript": "^6.0.2" }, "publishConfig": { From b2440f597f9d01bc5f419cc83d759e78a191ee86 Mon Sep 17 00:00:00 2001 From: Mat Walker Date: Thu, 9 Apr 2026 19:00:11 +1000 Subject: [PATCH 06/11] chore: Use npmjs trusted publisher --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d0c6099..146fbbd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,5 +32,4 @@ jobs: - run: npx semantic-release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_CONFIG_PROVENANCE: true From 2ecb728a24a04c30344687847e32e9e2bd524a27 Mon Sep 17 00:00:00 2001 From: Mat Walker Date: Thu, 9 Apr 2026 19:11:46 +1000 Subject: [PATCH 07/11] fix: Properly track remote branch --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 146fbbd..99d7f40 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,7 @@ jobs: with: fetch-depth: 0 persist-credentials: false + ref: ${{ github.ref }} - uses: actions/setup-node@v4 with: From 9354f5e0acf3e56c0c1a4bde3c18fdb297fab338 Mon Sep 17 00:00:00 2001 From: Mat Walker Date: Thu, 9 Apr 2026 19:30:40 +1000 Subject: [PATCH 08/11] fix: Revert back to npm tokens until Semantic release catchup --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99d7f40..4d4b617 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,6 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - persist-credentials: false ref: ${{ github.ref }} - uses: actions/setup-node@v4 @@ -33,4 +32,5 @@ jobs: - run: npx semantic-release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_CONFIG_PROVENANCE: true From b52e7613676b96a2abc646c7f6853126ab59f7f5 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 9 Apr 2026 09:36:16 +0000 Subject: [PATCH 09/11] chore(release): 1.0.2-alpha.2 [skip ci] ## [1.0.2-alpha.2](https://github.com/TeamControlium/controlium-utils/compare/v1.0.2-alpha.1...v1.0.2-alpha.2) (2026-04-09) ### Bug Fixes * PR Comments; Added ability for remote mocks ([af12e48](https://github.com/TeamControlium/controlium-utils/commit/af12e48389abe48a5724a0fc897e2cc679c352e3)) * Properly track remote branch ([2ecb728](https://github.com/TeamControlium/controlium-utils/commit/2ecb728a24a04c30344687847e32e9e2bd524a27)) * Revert back to npm tokens until Semantic release catchup ([9354f5e](https://github.com/TeamControlium/controlium-utils/commit/9354f5e0acf3e56c0c1a4bde3c18fdb297fab338)) --- CHANGELOG.md | 9 +++++++++ package-lock.json | 4 ++-- package.json | 9 +++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eda71ea..39e5880 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [1.0.2-alpha.2](https://github.com/TeamControlium/controlium-utils/compare/v1.0.2-alpha.1...v1.0.2-alpha.2) (2026-04-09) + + +### Bug Fixes + +* PR Comments; Added ability for remote mocks ([af12e48](https://github.com/TeamControlium/controlium-utils/commit/af12e48389abe48a5724a0fc897e2cc679c352e3)) +* Properly track remote branch ([2ecb728](https://github.com/TeamControlium/controlium-utils/commit/2ecb728a24a04c30344687847e32e9e2bd524a27)) +* Revert back to npm tokens until Semantic release catchup ([9354f5e](https://github.com/TeamControlium/controlium-utils/commit/9354f5e0acf3e56c0c1a4bde3c18fdb297fab338)) + ## [1.0.2-alpha.1](https://github.com/TeamControlium/controlium-utils/compare/v1.0.1...v1.0.2-alpha.1) (2026-04-01) diff --git a/package-lock.json b/package-lock.json index 940d0e5..d9dd477 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@controlium/utils", - "version": "1.0.2-alpha.1", + "version": "1.0.2-alpha.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@controlium/utils", - "version": "1.0.2-alpha.1", + "version": "1.0.2-alpha.2", "license": "MIT", "dependencies": { "@types/jsonpath-plus": "^5.0.2", diff --git a/package.json b/package.json index 4d2747c..dae6fe3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@controlium/utils", - "version": "1.0.2-alpha.1", + "version": "1.0.2-alpha.2", "description": "Shared utilities for Controlium-based projects", "type": "module", "author": "Team Controllium contributors", @@ -88,7 +88,12 @@ ], "rootDir": "src", "transform": { - "^.+\\.(t|j)s$": ["ts-jest", { "tsconfig": "tsconfig.spec.json" }] + "^.+\\.(t|j)s$": [ + "ts-jest", + { + "tsconfig": "tsconfig.spec.json" + } + ] }, "transformIgnorePatterns": [], "coverageDirectory": "../coverage", From fd619508fa25615ce08d0f6ce90f17434c4dfdd8 Mon Sep 17 00:00:00 2001 From: Mat Walker Date: Fri, 10 Apr 2026 17:59:44 +1000 Subject: [PATCH 10/11] chore: Added mock tests --- package.json | 2 +- src/mock/mock.spec.ts | 137 ++++++++++++++++++++++++++++++++++++++++++ src/mock/mock.ts | 90 +++++++++++++++------------ 3 files changed, 191 insertions(+), 38 deletions(-) create mode 100644 src/mock/mock.spec.ts diff --git a/package.json b/package.json index dae6fe3..4d7f5bb 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "build:esm": "tsc -p tsconfig.build.json", "build": "npm run clean && npm run build:cjs && npm run build:esm", "test": "jest --silent --runInBand", - "test:debug": "jest --runInBand", + "test:debug": "jest --silent=false --runInBand --testMatch=**/mock.spec.ts", "test:coverage": "jest --silent --coverage --runInBand", "lint": "eslint 'src/**/*.ts' --fix", "release": "semantic-release" diff --git a/src/mock/mock.spec.ts b/src/mock/mock.spec.ts new file mode 100644 index 0000000..3f4ff25 --- /dev/null +++ b/src/mock/mock.spec.ts @@ -0,0 +1,137 @@ +import { Mock } from "./mock"; +import { Logger } from "../logger/logger"; + +beforeAll(() => { + Logger.logToConsole = false; +}); + +describe("Mocking fulfill", () => { + beforeEach(() => { + Mock.reset(); + }); + + describe("Simple status 200 no headers or data", () => { + it("Only status (200) returned", async () => { + const methodToListenFor = 'POST'; + const statusReturned = 200; + Mock.addListener("Just Status", [`$[?(@.method == '${methodToListenFor}')]`], { status: statusReturned }); + const intercepted = await Mock.processInterceptedRequest({ url: 'https://asdf', method: 'POST', headers: {}, body: '' }); + expect(intercepted.action).toBe('fulfill'); + const fulfulledResponse = (intercepted as { response: Mock.Response })?.response; + expect(fulfulledResponse.status).toBe(statusReturned); + expect(fulfulledResponse.statusText).toBe('OK'); + expect(fulfulledResponse.headers).toBeUndefined(); + expect(fulfulledResponse.body).toBeUndefined(); + }); + + it("Only status (200) returned - statusText overidden", async () => { + const methodToListenFor = 'POST'; + const statusReturned = 200; + Mock.addListener("Just Status and text", [`$[?(@.method == '${methodToListenFor}')]`], { status: statusReturned, statusText: 'Test OK' }); + const intercepted = await Mock.processInterceptedRequest({ url: 'https://asdf', method: 'POST', headers: {}, body: '' }); + expect(intercepted.action).toBe('fulfill'); + const fulfulledResponse = (intercepted as { response: Mock.Response })?.response; + expect(fulfulledResponse.status).toBe(statusReturned); + expect(fulfulledResponse.statusText).toBe('Test OK'); + expect(fulfulledResponse.headers).toBeUndefined(); + expect(fulfulledResponse.body).toBeUndefined(); + }); + + it("Only status (200) returned - statusText overidden with undefined", async () => { + const methodToListenFor = 'POST'; + const statusReturned = 200; + Mock.addListener("Just Status and undefined text", [`$[?(@.method == '${methodToListenFor}')]`], { status: statusReturned, statusText: '_undefined' }); + const intercepted = await Mock.processInterceptedRequest({ url: 'https://asdf', method: methodToListenFor, headers: {}, body: '' }); + expect(intercepted.action).toBe('fulfill'); + const fulfulledResponse = (intercepted as { response: Mock.Response })?.response; + expect(fulfulledResponse.status).toBe(statusReturned); + expect(fulfulledResponse.statusText).toBeUndefined(); + expect(fulfulledResponse.headers).toBeUndefined(); + expect(fulfulledResponse.body).toBeUndefined(); + }); + }); + + describe("Status 200 with headers and text data", () => { + it("Status and headers returned", async () => { + const methodToListenFor = 'POST'; + const statusReturned = 200; + const testHeader = { "xyzzy": "abc123", "test1": "value1" }; + const testData = 'Hello world'; + Mock.addListener("Just Status", [`$[?(@.method == '${methodToListenFor}')]`], { status: statusReturned, headers: testHeader, body: testData }); + const intercepted = await Mock.processInterceptedRequest({ url: 'https://asdf', method: "POST", headers: {}, body: '' }); + expect(intercepted.action).toBe('fulfill'); + const fulfulledResponse = (intercepted as { response: Mock.Response })?.response; + expect(fulfulledResponse.status).toBe(statusReturned); + expect(fulfulledResponse.statusText).toBe('OK'); + expect(fulfulledResponse.headers).toMatchObject(testHeader); + expect(fulfulledResponse.body).toBe(testData); + }); + it("Data as an object", async () => { + const methodToListenFor = 'POST'; + const statusReturned = 200; + const testData = { "xyzzy": "abc123", "test1": "value1" }; + Mock.addListener("Just Status", [`$[?(@.method == '${methodToListenFor}')]`], { status: statusReturned, headers: {}, body: testData }); + const intercepted = await Mock.processInterceptedRequest({ url: 'https://asdf', method: "POST", headers: {}, body: '' }); + expect(intercepted.action).toBe('fulfill'); + const fulfulledResponse = (intercepted as { response: Mock.Response })?.response; + expect(fulfulledResponse.body).toMatchObject(testData); + }); + }); + + describe("Intercept combinations", () => { + it("Status and headers returned", async () => { + const methodToListenFor = 'POST'; + const statusReturned = 200; + const testHeader = { "xyzzy": "abc123", "test1": "value1" }; + const testData = 'Hello world'; + Mock.addListener("Just Status", [`$[?(@.method == '${methodToListenFor}')]`], { status: statusReturned, headers: testHeader, body: testData }); + const intercepted = await Mock.processInterceptedRequest({ url: 'https://asdf.my.ending', method: "POST", headers: {}, body: '' }); + expect(intercepted.action).toBe('fulfill'); + const fulfulledResponse = (intercepted as { response: Mock.Response })?.response; + expect(fulfulledResponse.status).toBe(statusReturned); + expect(fulfulledResponse.statusText).toBe('OK'); + expect(fulfulledResponse.headers).toMatchObject(testHeader); + expect(fulfulledResponse.body).toBe(testData); + }); + it("Match on partial url", async () => { + const methodToListenFor = 'POST'; + const statusReturned = 200; + const testData = { "xyzzy": "abc123", "test1": "value1" }; + Mock.addListener("Just Status", ['$[?(@.url.match("my\.ending$"))]'], { status: statusReturned, headers: {}, body: testData }); + const intercepted = await Mock.processInterceptedRequest({ url: 'https://asdf.my.ending', method: "POST", headers: {}, body: '' }); + expect(intercepted.action).toBe('fulfill'); + }); + it("Dont match on partial url not matching", async () => { + const methodToListenFor = 'POST'; + const statusReturned = 200; + const testData = { "xyzzy": "abc123", "test1": "value1" }; + Mock.addListener("Just Status", ['$[?(@.url.match("my\.end$"))]'], { status: statusReturned, headers: {}, body: testData }); + const intercepted = await Mock.processInterceptedRequest({ url: 'https://asdf.my.ending', method: "POST", headers: {}, body: '' }); + expect(intercepted.action).toBe('block'); + }); + it("Match on 2 partial url matchers", async () => { + const methodToListenFor = 'POST'; + const statusReturned = 200; + const testData = { "xyzzy": "abc123", "test1": "value1" }; + Mock.addListener("Just Status", ['$[?(@.url.match("my\.ending$"))]','$[?(@.url.match("^https"))]'], { status: statusReturned, headers: {}, body: testData }); + const intercepted = await Mock.processInterceptedRequest({ url: 'https://asdf.my.ending', method: "POST", headers: {}, body: '' }); + expect(intercepted.action).toBe('fulfill'); + }); + it("Match on 2 partial url matchers and a method", async () => { + const methodToListenFor = 'POST'; + const statusReturned = 200; + const testData = { "xyzzy": "abc123", "test1": "value1" }; + Mock.addListener("Just Status", ['$[?(@.url.match("my\.ending$"))]','$[?(@.url.match("^https"))]', `$[?(@.method == '${methodToListenFor}')]`], { status: statusReturned, headers: {}, body: testData }); + const intercepted = await Mock.processInterceptedRequest({ url: 'https://asdf.my.ending', method: "POST", headers: {}, body: '' }); + expect(intercepted.action).toBe('fulfill'); + }); + it("Match on partial url matcher and a body property", async () => { + const methodToListenFor = 'POST'; + const statusReturned = 200; + const testData = { "xyzzy": "abc123", "test1": "value1" }; + Mock.addListener("Just Status", ['$[?(@.url.match("my\.ending$"))]','$[?(@.body.xyzzy == "abc123")]'], { status: statusReturned, headers: {}, body: testData }); + const intercepted = await Mock.processInterceptedRequest({ url: 'https://asdf.my.ending', method: "POST", headers: {}, body: testData }); + expect(intercepted.action).toBe('fulfill'); + }); + }); +}); diff --git a/src/mock/mock.ts b/src/mock/mock.ts index 26cf764..7417be9 100644 --- a/src/mock/mock.ts +++ b/src/mock/mock.ts @@ -1,14 +1,14 @@ +import { STATUS_CODES } from 'node:http'; import { randomUUID } from 'node:crypto'; -import { JSONPath } from 'jsonpath-plus'; import { Utils } from '../utils/utils'; -import { Log, LogLevels } from '..'; +import { JsonUtils, Log, Logger, LogLevels } from '..'; /** * Static HTTP request interception and mocking utility for test suites. * * `Mock` is designed to sit between a test framework's network interceptor * (e.g. Playwright's `page.route`) and the application under test. Every - * outgoing request from the AUT is forwarded to {@link Mock.intercept}, which + * outgoing request from the AUT must be forwarded to {@link Mock.processInterceptedRequest}, which * decides how to handle it based on the registered listeners. * * **Default-deny**: any request that does not match a listener is blocked and @@ -27,7 +27,7 @@ import { Log, LogLevels } from '..'; * * `Mock` runs in the same process as the test framework. When a `passthrough` * listener matches, `Mock` fetches the real response itself and returns it - * directly. {@link Mock.intercept} always resolves to a {@link Mock.InterceptResult} + * directly. {@link Mock.processInterceptedRequest} always resolves to a {@link Mock.InterceptResult} * with `action: 'fulfill'` or `action: 'block'`. * * ### Delegate mode @@ -35,7 +35,7 @@ import { Log, LogLevels } from '..'; * Enable with `Mock.delegateMode = true` when `Mock` is hosted in a remote * server (e.g. a mock proxy) and the caller — not `Mock` — is better placed * to perform the real network fetch. In this mode, when a `passthrough` - * listener matches, {@link Mock.intercept} returns `action: 'passthrough'` + * listener matches, {@link Mock.processInterceptedRequest} returns `action: 'passthrough'` * plus a `correlationId`. The caller fetches the real response and reports it * back via {@link Mock.completePassthrough}, allowing `Mock` to record the * full transaction. Use {@link Mock.getPendingTransactions} to detect @@ -89,7 +89,7 @@ export class Mock { /** * When `true`, passthrough requests are delegated to the caller rather than - * fetched by `Mock` itself. {@link Mock.intercept} returns + * fetched by `Mock` itself. {@link Mock.processInterceptedRequest} returns * `action: 'passthrough'` with a `correlationId` for the caller to use when * reporting the real response back via {@link Mock.completePassthrough}. * @@ -174,9 +174,9 @@ export class Mock { Mock.throwError('addListener', `[matchers[${index}]] must not be empty`); } try { - JSONPath({ path: matcher, json: {}, wrap: true }); + JsonUtils.getMatchingJSONPropertyCount({}, matcher); } catch (e) { - Mock.throwError('addListener', `[matchers[${index}]] is not valid JSONPath syntax: "${matcher}". ${(e as Error).message}`); + Mock.throwError('addListener', `Matcher item [${index}] is not valid JSONPath syntax: "${matcher}". ${(e as Error).message}`); } }); @@ -191,6 +191,9 @@ export class Mock { if (response.headers !== undefined && (typeof response.headers !== 'object' || response.headers === null || Array.isArray(response.headers))) { Mock.throwError('addListener', `[action.headers] must be a plain object if provided`); } + if (Utils.isUndefined(action.statusText)) + action.statusText = STATUS_CODES[action.status] ?? undefined; + else if (action.statusText === '_undefined') action.statusText = undefined; } Utils.assertType(delayMs, 'number', 'addListener', 'delayMs'); @@ -265,7 +268,7 @@ export class Mock { * | Matched → block | `'block'` | `'blocked'` | * | No listener matched | `'block'` | `'unmatched'` | * - * @param request - The intercepted request. Must be a non-null object with + * @param interceptedRequest - The intercepted request. Must be a non-null object with * non-empty `url` and `method` string properties. * * @returns A promise resolving to a {@link Mock.InterceptResult}. @@ -285,27 +288,27 @@ export class Mock { * abortRequest(); * } */ - static async intercept(request: Mock.Request): Promise { - if (request === null || request === undefined || typeof request !== 'object' || Array.isArray(request)) { - Mock.throwError('intercept', `[request] must be a non-null object, got [${request === null ? 'null' : typeof request}]`); + static async processInterceptedRequest(interceptedRequest: Mock.Request): Promise { + if (interceptedRequest === null || interceptedRequest === undefined || typeof interceptedRequest !== 'object' || Array.isArray(interceptedRequest)) { + Mock.throwError('intercept', `[request] must be a non-null object, got [${interceptedRequest === null ? 'null' : typeof interceptedRequest}]`); } - if (typeof request.url !== 'string' || request.url.trim().length === 0) { - Mock.throwError('intercept', `[request.url] must be a non-empty string, got [${typeof request.url}]`); + if (typeof interceptedRequest.url !== 'string' || interceptedRequest.url.trim().length === 0) { + Mock.throwError('intercept', `[request.url] must be a non-empty string, got [${typeof interceptedRequest.url}]`); } - if (typeof request.method !== 'string' || request.method.trim().length === 0) { - Mock.throwError('intercept', `[request.method] must be a non-empty string, got [${typeof request.method}]`); + if (typeof interceptedRequest.method !== 'string' || interceptedRequest.method.trim().length === 0) { + Mock.throwError('intercept', `[request.method] must be a non-empty string, got [${typeof interceptedRequest.method}]`); } - if (request.headers !== undefined && request.headers !== null && (typeof request.headers !== 'object' || Array.isArray(request.headers))) { + if (interceptedRequest.headers !== undefined && interceptedRequest.headers !== null && (typeof interceptedRequest.headers !== 'object' || Array.isArray(interceptedRequest.headers))) { Mock.throwError('intercept', `[request.headers] must be a plain object if provided`); } - Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: ${request.method} ${request.url}`); + Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: ${interceptedRequest.method} ${interceptedRequest.url}`); - const listener = Mock.findMatch(request); + const listener = Mock.findMatch(interceptedRequest); if (!listener) { Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: no listener matched — blocking`); - Mock.record({ type: 'unmatched', request }); + Mock.record({ type: 'unmatched', request: interceptedRequest }); return { action: 'block' }; } @@ -318,7 +321,7 @@ export class Mock { if (listener.action === 'block') { Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: blocking request`); - Mock.record({ type: 'blocked', listenerName: listener.name, request }); + Mock.record({ type: 'blocked', listenerName: listener.name, request: interceptedRequest }); return { action: 'block' }; } @@ -330,19 +333,19 @@ export class Mock { correlationId, timestamp: new Date(), listenerName: listener.name, - request, + request: interceptedRequest, }); return { action: 'passthrough', correlationId }; } Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: passing through to real endpoint`); - const response = await Mock.fetchReal(request); - Mock.record({ type: 'passthrough', listenerName: listener.name, request, response }); + const response = await Mock.fetchReal(interceptedRequest); + Mock.record({ type: 'passthrough', listenerName: listener.name, request: interceptedRequest, response }); return { action: 'fulfill', response }; } const response = listener.action as Mock.Response; Log.writeLine(LogLevels.FrameworkDebug, `Mock.intercept: returning mocked response (status ${response.status})`); - Mock.record({ type: 'mocked', listenerName: listener.name, request, response }); + Mock.record({ type: 'mocked', listenerName: listener.name, request: interceptedRequest, response }); return { action: 'fulfill', response }; } @@ -351,10 +354,10 @@ export class Mock { * fetched. Records the full transaction and removes the pending entry. * * Only relevant when {@link Mock.delegateMode} is `true`. The `correlationId` - * must match one previously returned by {@link Mock.intercept}. + * must match one previously returned by {@link Mock.processInterceptedRequest}. * * @param correlationId - The UUID returned in the `action: 'passthrough'` - * result from {@link Mock.intercept}. Must be a non-empty string that + * result from {@link Mock.processInterceptedRequest}. Must be a non-empty string that * matches a pending transaction. * * @param response - The real response the caller received from the endpoint. @@ -477,18 +480,29 @@ export class Mock { } private static findMatch(request: Mock.Request): Mock.Listener | undefined { + Logger.writeLine(LogLevels.FrameworkInformation, `Checking if any listeners for [${request.url} (${request.method})]`); for (const listener of Mock.listeners.values()) { try { - const allMatch = listener.matchers.every((path) => { + const allMatch = listener.matchers.every((matcher) => { try { - const results = JSONPath({ path, json: request, wrap: true }); - return Array.isArray(results) && results.length > 0; + const x = JSON.stringify(request, null, 2); + Logger.writeLine(LogLevels.TestInformation, x); + const matched = JsonUtils.getMatchingJSONPropertyCount([request] as unknown as object, matcher) > 0; + if (matched) { + Logger.writeLine(LogLevels.FrameworkDebug, `Matched on [${matcher}]`); + return true; + } + return false; } catch (e) { - Log.writeLine(LogLevels.Warning, `Mock: JSONPath evaluation error in listener <${listener.name}> for path "${path}": ${(e as Error).message} — treating as non-match`); + Log.writeLine(LogLevels.Warning, `Mock: JSONPath evaluation error in listener <${listener.name}> for path "${matcher}": ${(e as Error).message} — treating as non-match`); return false; } }); + if (allMatch) return listener; + else { + Logger.writeLine(LogLevels.FrameworkDebug, 'No matches'); + } } catch (e) { Log.writeLine(LogLevels.Warning, `Mock: Unexpected error evaluating listener <${listener.name}>: ${(e as Error).message} — skipping`); } @@ -558,6 +572,8 @@ export namespace Mock { export interface Response { /** HTTP status code, e.g. `200`, `404`. Must be 100–599. */ status: number; + /** HTTP status text, eg. 'ok' or 'Internal Server Error' etc */ + statusText?: string; /** Response headers as a plain key/value object. */ headers?: Record; /** Response body. Objects are serialised to JSON by the framework adapter. */ @@ -575,7 +591,7 @@ export namespace Mock { export type ListenerAction = 'block' | 'passthrough' | Response; /** - * The result returned by {@link Mock.intercept} to the framework adapter. + * The result returned by {@link Mock.processInterceptedRequest} to the framework adapter. * * - `action: 'fulfill'` — return `response` to the AUT. * - `action: 'block'` — abort the request; the AUT receives nothing. @@ -584,9 +600,9 @@ export namespace Mock { * {@link Mock.completePassthrough} using the supplied `correlationId`. */ export type InterceptResult = - | { action: 'fulfill'; response: Response } - | { action: 'block' } - | { action: 'passthrough'; correlationId: string }; + | { action: 'fulfill'; response: Response } + | { action: 'block' } + | { action: 'passthrough'; correlationId: string }; /** * Describes the outcome of a single completed intercepted request. @@ -621,13 +637,13 @@ export namespace Mock { } /** - * A delegated passthrough that has been issued by {@link Mock.intercept} but + * A delegated passthrough that has been issued by {@link Mock.processInterceptedRequest} but * not yet completed via {@link Mock.completePassthrough}. * * Only created when {@link Mock.delegateMode} is `true`. */ export interface PendingTransaction { - /** The UUID issued by {@link Mock.intercept} to identify this passthrough. */ + /** The UUID issued by {@link Mock.processInterceptedRequest} to identify this passthrough. */ correlationId: string; /** Wall-clock time at which the passthrough was issued. */ timestamp: Date; From cd87497d80f06aa36eaf7a2e7992fb5b71ec8cf0 Mon Sep 17 00:00:00 2001 From: Mat Walker Date: Wed, 15 Apr 2026 15:47:20 +1000 Subject: [PATCH 11/11] chore: Added unit tests --- src/mock/mock.spec.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/mock/mock.spec.ts b/src/mock/mock.spec.ts index 3f4ff25..d21e582 100644 --- a/src/mock/mock.spec.ts +++ b/src/mock/mock.spec.ts @@ -8,6 +8,7 @@ beforeAll(() => { describe("Mocking fulfill", () => { beforeEach(() => { Mock.reset(); + }); describe("Simple status 200 no headers or data", () => { @@ -79,20 +80,6 @@ describe("Mocking fulfill", () => { }); describe("Intercept combinations", () => { - it("Status and headers returned", async () => { - const methodToListenFor = 'POST'; - const statusReturned = 200; - const testHeader = { "xyzzy": "abc123", "test1": "value1" }; - const testData = 'Hello world'; - Mock.addListener("Just Status", [`$[?(@.method == '${methodToListenFor}')]`], { status: statusReturned, headers: testHeader, body: testData }); - const intercepted = await Mock.processInterceptedRequest({ url: 'https://asdf.my.ending', method: "POST", headers: {}, body: '' }); - expect(intercepted.action).toBe('fulfill'); - const fulfulledResponse = (intercepted as { response: Mock.Response })?.response; - expect(fulfulledResponse.status).toBe(statusReturned); - expect(fulfulledResponse.statusText).toBe('OK'); - expect(fulfulledResponse.headers).toMatchObject(testHeader); - expect(fulfulledResponse.body).toBe(testData); - }); it("Match on partial url", async () => { const methodToListenFor = 'POST'; const statusReturned = 200;