diff --git a/.gitignore b/.gitignore index a3ff48f..41e76eb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,12 @@ myagent/ # Serena MCP tooling /.serena/ -# Compiled TypeScript task output (from the Phase 2 branch; never belongs here) +# Compiled TypeScript task output (emitted in-place by tsc) tb-*/index.js tb-*/index.js.map tb-*/tests/**/*.js tb-*/tests/**/*.js.map + +# Test run artifacts (task-lib mock-run / mock-test) +.taskkey +tb-*/tests/testingbot.json diff --git a/CLAUDE.md b/CLAUDE.md index f3284e5..84e899b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,18 +12,22 @@ for what the package contains and contributes. ## Build & package -Requires Node.js >= 20. The build is npm-scripts + webpack 5 (there is no gulp). +Requires Node.js >= 20. The build is npm-scripts + TypeScript + webpack 5 (no gulp). ```bash npm install # root build deps -npm run build # webpack: bundle the tab scripts + copy static files -> ./dist +npm run compile # tsc: compile each task's index.ts -> index.js +npm test # compile + mocha (task-lib mock-run/mock-test) npm run package # full pipeline -> ./Packages/*.vsix ``` -`npm run package` chains: `clean` (rimraf dist Packages) -> `build` (webpack) -> -`deps` (`scripts/install-task-deps.js` runs `npm ci --omit=dev` into each -`dist/tb-*` that has a `package.json`) -> `stamp` (`scripts/stamp-version.js`) -> -`create` (`tfx extension create`). +`npm run package` chains: `clean` -> `compile` (`tsc` per task, emitting +`index.js` next to `index.ts`) -> `build` (webpack: bundle the tab scripts + copy +static files, including the compiled task `index.js`, into `dist/`) -> `deps` +(`scripts/install-task-deps.js` runs `npm ci --omit=dev` into each `dist/tb-*` +that has a `package.json`) -> `stamp` (`scripts/stamp-version.js`) -> `create` +(`tfx extension create`). A `precompile` hook (`scripts/ensure-task-src-deps.js`) +installs each task's source `node_modules` so `tsc` can resolve library types. Versioning: `package.json`'s `version` is the single source of truth. The stamp step writes it into `dist/vss-extension.json` (flat semver) and both @@ -31,27 +35,35 @@ step writes it into `dist/vss-extension.json` (flat semver) and both placeholder version — only the `dist` copies are stamped, so don't rely on the version in the checked-in `vss-extension.json` / `task.json`. +Tests: `npm test` compiles and runs the mocha suites under `tb-*/tests/` (they +use `azure-pipelines-task-lib`'s `mock-run`/`mock-test`; `MockTestRunner` +downloads the Node20 runtime on first run, so the first `npm test` is slow). There is no lint wired up yet (the old `.eslintrc` targets the removed -babel-eslint) and no test suite — both are planned (see `MODERNIZATION.md`). +babel-eslint) — planned for Phase 4 (see `MODERNIZATION.md`). ## Architecture The extension contributes three server-side pieces plus one endpoint type, declared in `vss-extension.json` under `contributions`: -- **`tb-main/`** — Pipeline task "TestingBot Configuration" (`TBMain`). Node task - (`testingbot.js`) that reads a TestingBot service-endpoint credential, exports - `TB_KEY`/`TB_SECRET`/`SELENIUM_HOST`/etc. as pipeline variables so downstream - test steps can reach the grid, and optionally launches **TestingBot Tunnel** - (`tb-main/tunnel/2.30.jar`, spawned as a child process). It writes - `testingbot.json` and attaches it to the build via - `task.addattachment` with type `TestingBotBuildResult` — this attachment is how - the results tab (below) later finds the build's credentials and build name. +- **`tb-main/`** — Pipeline task "TestingBot Configuration" (`TBMain`). TypeScript + task (`index.ts`) that reads a TestingBot service-endpoint credential, exports + `TB_KEY`/`TB_SECRET`/`SELENIUM_HOST`/etc. as pipeline variables (the secret ones + via `setSecret` + a secret variable) so downstream test steps can reach the + grid, and optionally launches **TestingBot Tunnel** via the + `testingbot-tunnel-launcher` npm package (which downloads/caches the current + tunnel jar at runtime — nothing is bundled). It writes `testingbot.json` and + attaches it to the build via `task.addattachment` with type + `TestingBotBuildResult` — this attachment is how the results tab (below) later + finds the build's credentials and build name. The credentials still travel in + that attachment because the tab needs them to sign TestingBot `/mini` share + URLs; removing them from the browser (finding #1) is the Phase 3 tab redesign. - **`tb-stop-tunnel/`** — Pipeline task "Stop TestingBot Tunnel" (`TBStopTunnel`). - Kills the tunnel process using the PID that `tb-main` stored in the - `TB_TUNNEL_PID` variable. Add this task after your test steps to tear the tunnel - down. + TypeScript task (`index.ts`) that kills the tunnel process using the PID that + `tb-main` stored in the `TB_TUNNEL_PID` variable (the launcher's in-process + `killAsync` can't reach a tunnel started by a different task). Add this task + after your test steps to tear the tunnel down. - **`tb-build-info/`** — A build-results web tab (`infoTab.html` + `scripts/info.js`) shown inside the Azure DevOps build view. It reads the @@ -70,15 +82,20 @@ service endpoint. The tab never sees raw credentials outside that attachment. ## Task vs. web code — two different runtimes -- **Task code** runs on the build agent under the `Node20_1` handler. - - `tb-main/testingbot.js` uses `azure-pipelines-task-lib/task` (`tl.getInput`, - `tl.getBoolInput`, `tl.setVariable`, `tl.command`). It declares its runtime +- **Task code** runs on the build agent under the `Node20_1` handler. Both tasks + are TypeScript (`index.ts` compiled to `index.js` in place). + - `tb-main/index.ts` uses `azure-pipelines-task-lib/task` (`tl.getInput`, + `tl.getBoolInput`, `tl.setVariable`, `tl.setSecret`, `tl.command`, + `tl.setResult`) and `testingbot-tunnel-launcher`. It declares those runtime deps in `tb-main/package.json`; they are NOT committed — the `deps` build - step installs them into `dist/tb-main/node_modules` so the packaged task is + step installs them into `dist/tb-main/node_modules` so the task is self-contained. - - `tb-stop-tunnel/sample.js` reads `TB_TUNNEL_PID` directly from `process.env` - and calls `process.kill`. It must stay **dependency-free** — do not add - `azure-pipelines-task-lib` or any other dependency to the stop task. + - `tb-stop-tunnel/index.ts` must stay **dependency-free** — it reads + `TB_TUNNEL_PID` directly from `process.env`, calls `process.kill`, and emits + the raw `##vso[task.complete …]` logging command itself. Do NOT add + `azure-pipelines-task-lib` (or any runtime dependency) to the stop task; its + `package.json` has no `dependencies`, so nothing is installed into + `dist/tb-stop-tunnel`. - **Web/tab code** (`tb-build-info/scripts/*.js`) runs in the browser inside Azure DevOps and is bundled by webpack (`webpack.config.js`) with AMD output and diff --git a/package-lock.json b/package-lock.json index fbac9b5..8facd05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,15 @@ "devDependencies": { "@babel/core": "^8.0.1", "@babel/preset-env": "^8.0.2", + "@types/mocha": "^10.0.10", + "@types/node": "^20.17.0", "babel-loader": "^10.1.1", "buffer": "^6.0.3", "copy-webpack-plugin": "^14.0.0", + "mocha": "^11.7.6", "rimraf": "^6.1.3", "tfx-cli": "^0.23.4", + "typescript": "^5.7.0", "vss-web-extension-sdk": "^1.106.0", "webpack": "^5.108.4", "webpack-cli": "^7.2.1" @@ -1887,14 +1891,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { - "version": "26.1.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", - "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~8.3.0" + "undici-types": "~6.21.0" } }, "node_modules/@webassemblyjs/ast": { @@ -2134,6 +2145,32 @@ "dev": true, "license": "MIT" }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/app-root-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-1.0.0.tgz", @@ -2178,54 +2215,6 @@ "node": ">= 14" } }, - "node_modules/archiver-utils/node_modules/brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/archiver-utils/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/archiver-utils/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/archiver-utils/node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2337,9 +2326,7 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "license": "Python-2.0", - "optional": true, - "peer": true + "license": "Python-2.0" }, "node_modules/array-find-index": { "version": "1.0.2", @@ -2585,6 +2572,23 @@ "node": ">=6.0.0" } }, + "node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, "node_modules/browserslist": { "version": "4.28.6", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", @@ -2747,6 +2751,52 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -2774,6 +2824,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -2789,6 +2854,26 @@ "node": ">=6" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/colors": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", @@ -3030,6 +3115,24 @@ "node": ">=0.4.0" } }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -3066,6 +3169,16 @@ "minimalistic-assert": "^1.0.0" } }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3258,6 +3371,19 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -3808,6 +3934,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3885,6 +4021,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3939,6 +4097,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", @@ -3992,6 +4160,16 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/hosted-git-info": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", @@ -4212,6 +4390,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4270,6 +4458,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -4353,6 +4561,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -4461,32 +4682,6 @@ "node": ">= 10.13.0" } }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jju": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", @@ -4667,6 +4862,23 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", @@ -4760,6 +4972,22 @@ "dev": true, "license": "ISC" }, + "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/minimizer-webpack-plugin": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", @@ -4831,6 +5059,110 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mocha": { + "version": "11.7.6", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.6.tgz", + "integrity": "sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/mocha/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", @@ -5330,6 +5662,16 @@ ], "license": "MIT" }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", @@ -5394,16 +5736,6 @@ "minimatch": "^5.1.0" } }, - "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/readdir-glob/node_modules/minimatch": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", @@ -5414,7 +5746,21 @@ "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/redent": { @@ -5479,6 +5825,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -6119,8 +6475,7 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/string-width-cjs": { - "name": "string-width", + "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", @@ -6135,34 +6490,17 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8" @@ -6196,8 +6534,7 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", + "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", @@ -6210,12 +6547,16 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, "engines": { "node": ">=8" } @@ -6248,6 +6589,35 @@ "node": ">=0.10.0" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -6666,6 +7036,20 @@ "dev": true, "license": "MIT" }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -6682,9 +7066,9 @@ } }, "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -7152,8 +7536,14 @@ "node": ">=0.1.90" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", @@ -7171,112 +7561,128 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=4.0.0" } }, - "node_modules/wrap-ansi-cjs/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=4.0" + } }, - "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, - "license": "MIT", + "license": "ISC", "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/xml2js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, "engines": { - "node": ">=4.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/yocto-queue": { @@ -8555,13 +8961,19 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true + }, "@types/node": { - "version": "26.1.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", - "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "requires": { - "undici-types": "~8.3.0" + "undici-types": "~6.21.0" } }, "@webassemblyjs/ast": { @@ -8766,6 +9178,21 @@ } } }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "app-root-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-1.0.0.tgz", @@ -8832,38 +9259,6 @@ "readable-stream": "^4.0.0" }, "dependencies": { - "brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - } - }, - "minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.2" - } - }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -8904,9 +9299,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "array-find-index": { "version": "1.0.2", @@ -9051,6 +9444,21 @@ "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", "dev": true }, + "brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, "browserslist": { "version": "4.28.6", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", @@ -9138,6 +9546,36 @@ "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", "dev": true }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "requires": { + "readdirp": "^4.0.1" + } + }, "chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -9155,6 +9593,17 @@ "is64bit": "^2.0.0" } }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, "clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -9166,6 +9615,21 @@ "shallow-clone": "^3.0.0" } }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "colors": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", @@ -9324,6 +9788,15 @@ "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==", "dev": true }, + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -9350,6 +9823,12 @@ "minimalistic-assert": "^1.0.0" } }, + "diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true + }, "dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -9491,6 +9970,12 @@ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, "esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -9849,6 +10334,12 @@ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, "get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -9899,6 +10390,20 @@ "get-intrinsic": "^1.1.1" } }, + "glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -9935,6 +10440,12 @@ "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "dev": true }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "has-property-descriptors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", @@ -9968,6 +10479,12 @@ "function-bind": "^1.1.2" } }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, "hosted-git-info": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", @@ -10104,6 +10621,12 @@ "number-is-nan": "^1.0.0" } }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -10137,6 +10660,18 @@ "has-tostringtag": "^1.0.0" } }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -10189,6 +10724,12 @@ "has-symbols": "^1.0.2" } }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -10265,23 +10806,6 @@ "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } } }, "jju": { @@ -10415,6 +10939,16 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, "loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", @@ -10487,6 +11021,15 @@ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", "dev": true }, + "minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.2" + } + }, "minimizer-webpack-plugin": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", @@ -10505,6 +11048,77 @@ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true }, + "mocha": { + "version": "11.7.6", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.6.tgz", + "integrity": "sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==", + "dev": true, + "requires": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", @@ -10841,6 +11455,15 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, "read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", @@ -10895,15 +11518,6 @@ "minimatch": "^5.1.0" }, "dependencies": { - "brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, "minimatch": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", @@ -10915,6 +11529,12 @@ } } }, + "readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true + }, "redent": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", @@ -10960,6 +11580,12 @@ "is-finite": "^1.0.0" } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -11389,6 +12015,17 @@ "safe-buffer": "~5.1.0" } }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "string-width-cjs": { "version": "npm:string-width@4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -11398,29 +12035,6 @@ "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } } }, "string.prototype.trimend": { @@ -11445,6 +12059,15 @@ "es-abstract": "^1.19.5" } }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, "strip-ansi-cjs": { "version": "npm:strip-ansi@6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -11452,14 +12075,6 @@ "dev": true, "requires": { "ansi-regex": "^5.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - } } }, "strip-final-newline": { @@ -11477,6 +12092,21 @@ "get-stdin": "^4.0.1" } }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -11760,6 +12390,12 @@ } } }, + "typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true + }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -11773,9 +12409,9 @@ } }, "undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true }, "unicode-canonical-property-names-ecmascript": { @@ -12071,6 +12707,23 @@ } } }, + "workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrap-ansi-cjs": { "version": "npm:wrap-ansi@7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -12080,64 +12733,6 @@ "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } } }, "xml2js": { @@ -12156,6 +12751,59 @@ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "dev": true }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + } + } + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 87b1e9e..bb4b318 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,15 @@ "version": "0.2.7", "description": "TestingBot integration with Azure DevOps", "scripts": { - "clean": "rimraf dist Packages", + "clean": "rimraf dist Packages \"tb-*/index.js\" \"tb-*/index.js.map\" \"tb-*/tests/**/*.js\" \"tb-*/tests/**/*.js.map\"", + "precompile": "node scripts/ensure-task-src-deps.js", + "compile": "tsc -p tb-main/tsconfig.json && tsc -p tb-stop-tunnel/tsconfig.json", "build": "webpack --mode production", "deps": "node scripts/install-task-deps.js", "stamp": "node scripts/stamp-version.js", + "test": "npm run compile && mocha \"tb-*/tests/**/*.js\"", "create": "tfx extension create --root dist --manifest-globs vss-extension.json --output-path Packages", - "package": "npm run clean && npm run build && npm run deps && npm run stamp && npm run create" + "package": "npm run clean && npm run compile && npm run build && npm run deps && npm run stamp && npm run create" }, "repository": { "type": "git", @@ -19,11 +22,15 @@ "devDependencies": { "@babel/core": "^8.0.1", "@babel/preset-env": "^8.0.2", + "@types/mocha": "^10.0.10", + "@types/node": "^20.17.0", "babel-loader": "^10.1.1", "buffer": "^6.0.3", "copy-webpack-plugin": "^14.0.0", + "mocha": "^11.7.6", "rimraf": "^6.1.3", "tfx-cli": "^0.23.4", + "typescript": "^5.7.0", "vss-web-extension-sdk": "^1.106.0", "webpack": "^5.108.4", "webpack-cli": "^7.2.1" diff --git a/scripts/ensure-task-src-deps.js b/scripts/ensure-task-src-deps.js new file mode 100644 index 0000000..d473516 --- /dev/null +++ b/scripts/ensure-task-src-deps.js @@ -0,0 +1,31 @@ +// Ensures each task's dependencies are installed in its source folder so `tsc` +// can resolve the library type declarations (azure-pipelines-task-lib, +// testingbot-tunnel-launcher) when compiling index.ts and the tests. Runs +// automatically before `compile` (via the `precompile` npm hook). The packaged +// production node_modules are installed separately into dist/ by +// scripts/install-task-deps.js. +const fs = require('fs'); +const path = require('path'); +const { execFileSync } = require('child_process'); + +const root = path.join(__dirname, '..'); +const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + +for (const entry of fs.readdirSync(root)) { + if (!entry.startsWith('tb-')) continue; + + const taskDir = path.join(root, entry); + const pkgPath = path.join(taskDir, 'package.json'); + if (!fs.existsSync(pkgPath)) continue; + if (fs.existsSync(path.join(taskDir, 'node_modules'))) continue; + + // A dependency-free task (e.g. tb-stop-tunnel) needs no install; tsc resolves + // @types/node from the repo root. + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + const hasDeps = (pkg.dependencies && Object.keys(pkg.dependencies).length) || + (pkg.devDependencies && Object.keys(pkg.devDependencies).length); + if (!hasDeps) continue; + + console.log(`Installing source deps for ${entry}`); + execFileSync(npm, ['install', '--no-audit', '--no-fund'], { cwd: taskDir, stdio: 'inherit' }); +} diff --git a/scripts/install-task-deps.js b/scripts/install-task-deps.js index c83f031..bd17240 100644 --- a/scripts/install-task-deps.js +++ b/scripts/install-task-deps.js @@ -12,7 +12,15 @@ for (const entry of fs.readdirSync(distDir)) { if (!entry.startsWith('tb-')) continue; const taskDir = path.join(distDir, entry); - if (!fs.existsSync(path.join(taskDir, 'package.json'))) continue; + const pkgPath = path.join(taskDir, 'package.json'); + if (!fs.existsSync(pkgPath)) continue; + + // A dependency-free task (e.g. tb-stop-tunnel) has nothing to install. + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + if (!pkg.dependencies || Object.keys(pkg.dependencies).length === 0) { + console.log(`No production deps for ${entry}, skipping`); + continue; + } const hasLock = fs.existsSync(path.join(taskDir, 'package-lock.json')); const args = hasLock diff --git a/tb-main/index.ts b/tb-main/index.ts new file mode 100644 index 0000000..cd12f04 --- /dev/null +++ b/tb-main/index.ts @@ -0,0 +1,189 @@ +import tl = require('azure-pipelines-task-lib/task'); +import tunnelLauncher = require('testingbot-tunnel-launcher'); +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +type TunnelOptions = NonNullable[0]>; + +interface Credentials { + key: string; + secret: string; +} + +const taskVersion: string = (() => { + // task.json sits next to the compiled index.js in the packaged task. + const v = require('./task.json').version; + return [v.Major, v.Minor, v.Patch].join('.'); +})(); + +// The endpoint stores auth as JSON whose key casing has drifted over time +// (username vs Username). Match the parameter name case-insensitively. +function getAuthParameter(endpoint: string, paramName: string): string | undefined { + const auth = tl.getEndpointAuthorization(endpoint, false); + if (!auth) { + throw new Error('Could not read the TestingBot credentials endpoint authorization. Please check the service connection.'); + } + if (auth.scheme !== 'UsernamePassword') { + throw new Error(`The authorization scheme ${auth.scheme} is not supported for the TestingBot endpoint. Please use a username and a password.`); + } + + const key = Object.getOwnPropertyNames(auth.parameters).find( + (name) => name.toLowerCase() === paramName.toLowerCase() + ); + return key ? auth.parameters[key] : undefined; +} + +function getEndpointDetails(fieldName: string): Credentials { + const endpoint = tl.getInput(fieldName, true); + if (!endpoint) { + throw new Error('No TestingBot service connection was provided.'); + } + + const secret = tl.getInput('endpointAuthToken') || getAuthParameter(endpoint, 'password'); + const key = tl.getInput('endpointUsername') || getAuthParameter(endpoint, 'username'); + + if (!key || !secret) { + throw new Error('The TestingBot service connection is missing a key or secret.'); + } + return { key, secret }; +} + +function exportVariables(credentials: Credentials): void { + // Register the secret so it is masked in logs, then export it as a secret + // pipeline variable. The key is not sensitive and stays a normal variable. + tl.setSecret(credentials.secret); + + tl.setVariable('TB_KEY', credentials.key); + tl.setVariable('TESTINGBOT_KEY', credentials.key); + tl.setVariable('TB_SECRET', credentials.secret, true); + tl.setVariable('TESTINGBOT_SECRET', credentials.secret, true); + tl.setVariable('TB_API_ENDPOINT', 'api.testingbot.com'); + tl.setVariable('SELENIUM_HOST', 'hub.testingbot.com'); + tl.setVariable('SELENIUM_PORT', '80'); + + // Build runs expose Build.*, release runs expose Release.* instead. + const buildName = [ + tl.getVariable('Build.DefinitionName') || tl.getVariable('Release.DefinitionName'), + tl.getVariable('Build.BuildId') || tl.getVariable('Release.ReleaseId') + ].filter(Boolean).join('_').replace(/ /g, '_'); + tl.setVariable('TB_BUILD_NAME', buildName); +} + +function defaultTunnelIdentifier(): string { + const id = tl.getVariable('Build.BuildId') || tl.getVariable('Release.ReleaseId') || 'local'; + const attempt = tl.getVariable('System.JobAttempt') || tl.getVariable('Release.AttemptNumber') || '1'; + return `azure-${id}-${attempt}`; +} + +function tunnelOptions(credentials: Credentials): TunnelOptions { + const options: TunnelOptions = { + apiKey: credentials.key, + apiSecret: credentials.secret, + tunnelIdentifier: tl.getInput('tunnelIdentifier') || defaultTunnelIdentifier(), + // Log to a file so the tunnel does not depend on this task's stdout pipe, + // which closes once the task exits and the tunnel keeps running. + logfile: path.join(agentTemp(), 'testingbot-tunnel.log') + }; + + const sePort = tl.getInput('sePort'); + if (sePort) { + options['se-port'] = parseInt(sePort, 10); + } + const proxy = tl.getInput('proxy'); + if (proxy) { + options.proxy = proxy; + } + const readyTimeout = tl.getInput('readyTimeout'); + if (readyTimeout) { + options.timeout = parseInt(readyTimeout, 10); + } + if (tl.getBoolInput('noBump', false)) { + options.noBump = true; + } + if (tl.getBoolInput('noCache', false)) { + options.noCache = true; + } + if (tl.getBoolInput('sharedTunnel', false)) { + options.shared = true; + } + + if (tl.getInput('tbTunnelOptions')) { + tl.warning('tbTunnelOptions is deprecated and ignored. Use the dedicated tunnel inputs (tunnelIdentifier, sePort, proxy, readyTimeout, noBump, noCache, sharedTunnel) instead.'); + } + + return options; +} + +async function startTunnel(credentials: Credentials): Promise { + const options = tunnelOptions(credentials); + console.log(`Starting TestingBot Tunnel (identifier: ${options.tunnelIdentifier})...`); + + // Rejects on a Java version problem, download failure, timeout, invalid + // credentials or an exhausted account, so a pre-ready exit fails the task. + const tunnel = await tunnelLauncher.downloadAndRunAsync(options); + if (!tunnel || !tunnel.pid) { + throw new Error('TestingBot Tunnel failed to start.'); + } + + // The stop task (a separate process) kills it by pid; also record the + // identifier so the tunnel can be cleaned up via the API if needed. + tl.setVariable('TB_TUNNEL_PID', `pid_${tunnel.pid}`); + tl.setVariable('TB_TUNNEL_IDENTIFIER', String(options.tunnelIdentifier)); + + // Let the tunnel outlive this task; do not keep our event loop alive for it. + if (typeof tunnel.unref === 'function') { + tunnel.unref(); + } + console.log(`TestingBot Tunnel is ready (pid ${tunnel.pid}).`); +} + +function agentTemp(): string { + return tl.getVariable('Agent.TempDirectory') || os.tmpdir(); +} + +function writeAttachment(credentials: Credentials): void { + // The results tab needs the key/secret to sign TestingBot /mini share URLs so + // developers can watch the test video/logs embedded in the build view. Those + // credentials therefore travel in this build attachment. + // + // SECURITY (finding #1): build attachments are readable by anyone with + // build-read access, so this still exposes the secret. The Phase 3 tab + // redesign removes it from the browser by signing the /mini URL server-side + // through a TestingBot API data source, keeping the embedded viewer. + const data = { + TB_KEY: credentials.key, + TB_SECRET: credentials.secret, + TB_BUILD_NAME: tl.getVariable('TB_BUILD_NAME'), + TB_API_ENDPOINT: tl.getVariable('TB_API_ENDPOINT'), + SELENIUM_HOST: tl.getVariable('SELENIUM_HOST'), + SELENIUM_PORT: tl.getVariable('SELENIUM_PORT'), + CONNECTED_SERVICE_NAME: tl.getInput('connectedServiceName'), + TASK_VERSION: taskVersion + }; + + const file = path.join(agentTemp(), 'testingbot.json'); + fs.writeFileSync(file, JSON.stringify(data)); + tl.command('task.addattachment', { type: 'TestingBotBuildResult', name: 'buildresults' }, file); +} + +async function run(): Promise { + try { + const credentials = getEndpointDetails('connectedServiceName'); + exportVariables(credentials); + + if (tl.getBoolInput('tbTunnel', false)) { + await startTunnel(credentials); + } + + writeAttachment(credentials); + tl.setResult(tl.TaskResult.Succeeded, 'TestingBot configured.'); + // Exit explicitly: a running tunnel child keeps the event loop alive. + process.exit(0); + } catch (err) { + tl.setResult(tl.TaskResult.Failed, err instanceof Error ? err.message : String(err)); + process.exit(1); + } +} + +run(); diff --git a/tb-main/package-lock.json b/tb-main/package-lock.json index b21e354..68ab2c6 100644 --- a/tb-main/package-lock.json +++ b/tb-main/package-lock.json @@ -9,7 +9,8 @@ "version": "0.2.7", "license": "MIT", "dependencies": { - "azure-pipelines-task-lib": "^5.277.0" + "azure-pipelines-task-lib": "^5.277.0", + "testingbot-tunnel-launcher": "^1.1.18" } }, "node_modules/@nodelib/fs.scandir": { @@ -588,6 +589,15 @@ "node": ">=6" } }, + "node_modules/testingbot-tunnel-launcher": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/testingbot-tunnel-launcher/-/testingbot-tunnel-launcher-1.1.18.tgz", + "integrity": "sha512-0xSA7r/RsW4ltxrviDaIDAEevEZDbTq0E8vFPfxjOPBfo8aTee0rMAqgGTsYnTP0St4rTfosdgS/0aRLEzCEHw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/tb-main/package.json b/tb-main/package.json index f53b2e0..c19247f 100644 --- a/tb-main/package.json +++ b/tb-main/package.json @@ -3,12 +3,13 @@ "version": "0.2.7", "description": "TestingBot Configuration task", "private": true, - "main": "testingbot.js", + "main": "index.js", "license": "MIT", "engines": { "node": ">=20" }, "dependencies": { - "azure-pipelines-task-lib": "^5.277.0" + "azure-pipelines-task-lib": "^5.277.0", + "testingbot-tunnel-launcher": "^1.1.18" } } diff --git a/tb-main/task.json b/tb-main/task.json index 6f2e5d0..a76a89b 100644 --- a/tb-main/task.json +++ b/tb-main/task.json @@ -4,12 +4,16 @@ "friendlyName": "TestingBot Configuration", "description": "TestingBot Configuration for Web and Device Application Testing", "author": "TestingBot", - "helpMarkDown": "", + "helpMarkDown": "Exports your TestingBot credentials as pipeline variables and optionally starts TestingBot Tunnel.", "category": "Test", "visibility": [ "Build", "Release" ], + "runsOn": [ + "Agent", + "DeploymentGroup" + ], "demands": [], "version": { "Major": "0", @@ -17,7 +21,15 @@ "Patch": "0" }, "minimumAgentVersion": "3.232.1", - "instanceNameFormat": "Manage TestingBot Credentials", + "instanceNameFormat": "Configure TestingBot", + "groups": [ + { + "name": "tunnel", + "displayName": "TestingBot Tunnel", + "isExpanded": false, + "visibleRule": "tbTunnel = true" + } + ], "inputs": [ { "name": "connectedServiceName", @@ -25,28 +37,116 @@ "label": "TestingBot Credentials", "defaultValue": "", "required": true, - "helpMarkDown": "TestingBot Account Details" + "helpMarkDown": "The TestingBot service connection holding your key and secret." }, { "name": "tbTunnel", "type": "boolean", - "label": "TestingBot Tunnel", + "label": "Start TestingBot Tunnel", + "defaultValue": false, + "required": false, + "helpMarkDown": "Download and start TestingBot Tunnel so tests can reach applications behind your firewall. Requires Java 11+ on the agent. Add the 'Stop TestingBot Tunnel' task after your tests." + }, + { + "name": "tunnelIdentifier", + "type": "string", + "label": "Tunnel identifier", + "defaultValue": "", + "required": false, + "groupName": "tunnel", + "helpMarkDown": "Unique identifier for this tunnel. Defaults to azure--." + }, + { + "name": "sePort", + "type": "string", + "label": "Selenium relay port", + "defaultValue": "", + "required": false, + "groupName": "tunnel", + "helpMarkDown": "Port the tunnel's Selenium relay listens on (default 4445)." + }, + { + "name": "proxy", + "type": "string", + "label": "Upstream proxy", + "defaultValue": "", + "required": false, + "groupName": "tunnel", + "helpMarkDown": "Upstream proxy host and port, e.g. localhost:1234." + }, + { + "name": "readyTimeout", + "type": "string", + "label": "Ready timeout (seconds)", + "defaultValue": "", + "required": false, + "groupName": "tunnel", + "helpMarkDown": "How long to wait for the tunnel to become ready (default 90)." + }, + { + "name": "sharedTunnel", + "type": "boolean", + "label": "Share tunnel with team", + "defaultValue": false, + "required": false, + "groupName": "tunnel", + "helpMarkDown": "Share this tunnel with other members of your TestingBot team." + }, + { + "name": "noBump", + "type": "boolean", + "label": "Disable SSL bumping", + "defaultValue": false, + "required": false, + "groupName": "tunnel", + "helpMarkDown": "Disable the tunnel's SSL bumping/rewriting." + }, + { + "name": "noCache", + "type": "boolean", + "label": "Disable caching", "defaultValue": false, "required": false, - "helpMarkDown": "TestingBot Tunnel" + "groupName": "tunnel", + "helpMarkDown": "Disable the tunnel's caching." }, { "name": "tbTunnelOptions", "type": "string", - "label": "TestingBot Tunnel Options", + "label": "Tunnel options (deprecated)", "defaultValue": "", "required": false, - "helpMarkDown": "TestingBot Tunnel Commandline Options" + "groupName": "tunnel", + "helpMarkDown": "Deprecated and ignored. Use the dedicated tunnel inputs instead." } - ], + ], + "outputVariables": [ + { "name": "TB_TUNNEL_PID", "description": "Process id marker of the started tunnel." }, + { "name": "TB_TUNNEL_IDENTIFIER", "description": "Identifier of the started tunnel." }, + { "name": "TB_BUILD_NAME", "description": "Build name reported to TestingBot." } + ], + "restrictions": { + "commands": { + "mode": "restricted" + }, + "settableVariables": { + "allowed": [ + "TB_KEY", + "TESTINGBOT_KEY", + "TB_SECRET", + "TESTINGBOT_SECRET", + "TB_API_ENDPOINT", + "SELENIUM_HOST", + "SELENIUM_PORT", + "TB_BUILD_NAME", + "TB_TUNNEL_PID", + "TB_TUNNEL_IDENTIFIER" + ] + } + }, "execution": { "Node20_1": { - "target": "testingbot.js", + "target": "index.js", "argumentFormat": "" } } diff --git a/tb-main/testingbot.js b/tb-main/testingbot.js deleted file mode 100644 index 1d31474..0000000 --- a/tb-main/testingbot.js +++ /dev/null @@ -1,185 +0,0 @@ -/* global Promise */ -var tl = require('azure-pipelines-task-lib/task'); -var fs = require('fs'); -var path = require('path'); -var spawn = require('child_process').spawn; -var EventEmitter = require('events').EventEmitter; - -var version = require('./task.json').version; -version = [version.Major, version.Minor, version.Patch].join('.'); - -var main = function main(cb) { - // The endpoint stores the auth details as JSON. Unfortunately the structure of the JSON has changed through time, namely the keys were sometimes upper-case. - // To work around this, we can perform case insensitive checks in the property dictionary of the object. Note that the PowerShell implementation does not suffer from this problem. - // See https://github.com/Microsoft/vso-agent/blob/bbabbcab3f96ef0cfdbae5ef8237f9832bef5e9a/src/agent/plugins/release/artifact/jenkinsArtifact.ts for a similar implementation - var getAuthParameter = function getAuthParameter(endpoint, paramName) { - - var paramValue = null; - var auth = tl.getEndpointAuthorization(endpoint, false); - - if (!auth) { - throw new Error('Could not read the TestingBot credentials endpoint authorization. Please check the service connection.'); - } - - if (auth.scheme !== 'UsernamePassword') { - throw new Error('The authorization scheme ' + auth.scheme + ' is not supported for the TestingBot endpoint. Please use a username and a password.'); - } - - var parameters = Object.getOwnPropertyNames(auth['parameters']); - - var keyName; - parameters.some(function (key) { - - if (key.toLowerCase() === paramName.toLowerCase()) { - keyName = key; - - return true; - } - }); - - paramValue = auth['parameters'][keyName]; - - return paramValue; - }; - - var getEndpointDetails = function getEndpointDetails(endpointInputFieldName) { - var errorMessage = 'Could not decode the credentials endpoint. Please ensure you are running an agent that meets the minimum version (3.232.1)'; - if (!tl.getEndpointUrl) { - throw new Error(errorMessage); - } - - var genericEndpoint = tl.getInput(endpointInputFieldName); - if (!genericEndpoint) { - throw new Error(errorMessage); - } - - var hostUrl = tl.getInput('endpointUrl') || tl.getEndpointUrl(genericEndpoint, false); - var secret = tl.getInput('endpointAuthToken') || getAuthParameter(genericEndpoint, 'password'); - var key = tl.getInput('endpointUsername') || getAuthParameter(genericEndpoint, 'username'); - - return { url: hostUrl, secret, key }; - }; - - var credentials = getEndpointDetails('connectedServiceName'); - tl.setVariable('TB_KEY', credentials.key); - tl.setVariable('TESTINGBOT_KEY', credentials.key); - tl.setVariable('TB_SECRET', credentials.secret); - tl.setVariable('TESTINGBOT_SECRET', credentials.secret); - tl.setVariable('TB_API_ENDPOINT', 'api.testingbot.com'); - tl.setVariable('SELENIUM_HOST', 'hub.testingbot.com'); - tl.setVariable('SELENIUM_PORT', '80'); - // Build runs expose BUILD_*; release runs expose RELEASE_* instead. Fall back - // so the build name is never a bare "_" join of undefined parts. - var buildName = [ - tl.getVariable('BUILD_DEFINITIONNAME') || tl.getVariable('RELEASE_DEFINITIONNAME'), - tl.getVariable('BUILD_BUILDID') || tl.getVariable('RELEASE_RELEASEID') - ].filter(Boolean).join('_').replace(/ /g, '_'); - tl.setVariable('TB_BUILD_NAME', buildName); - cb(credentials); -}; - -var startTunnel = function startTunnel(credentials, resolve, tunnelOptions) { - var self_path = __dirname; - var tunnel_path = path.join(self_path, 'tunnel'); - var tunnel_jar = path.join(tunnel_path, '2.30.jar'); - - console.log('Running TestingBot Tunnel: ', tunnel_jar); - - var pid_path = path.join((process.env.BUILD_STAGINGDIRECTORY || self_path), 'testingbot-tunnel.pid'); - tl.setVariable('TB_TUNNEL_PID_PATH', pid_path); - console.log('Setting PID path', pid_path); - - var tunnel_process = spawn( - 'java', - [ - '-jar', - tunnel_jar, - credentials.key, - credentials.secret - ].concat(tunnelOptions.split(' ')), - { - detached: true, - cwd: self_path - } - ); - - tl.setVariable('TB_TUNNEL_PID', 'pid_' + tunnel_process.pid); - - var lineEmitter = new EventEmitter(); - lineEmitter.on('stdout', function(line) { - console.log('Tunnel stdout: ' + line.toString()); - if (/You may start your tests/.test(line)) { - return resolve(); - } - }); - lineEmitter.on('stderr', function(line) { - console.error('Tunnel stderr: ' + line.toString()); - if (/You may start your tests/.test(line)) { - return resolve(); - } - }); - - var dataHolders = {}; - ['stdout', 'stderr'].forEach(function(channel) { - dataHolders[channel] = []; - tunnel_process[channel].on('data', function (data) { - data.toString().split('').forEach(function(char) { - if (char === '\n' || char === '\r') { - if (dataHolders[channel].length !== 0) { - lineEmitter.emit(channel, dataHolders[channel].join('')); - dataHolders[channel] = []; - } - return; - } - dataHolders[channel].push(char); - }); - }); - }); - - tunnel_process.on('close', function(code) { - return process.exit(code); - }); -}; - -main(function(credentials) { - var shouldStartTunnel = tl.getBoolInput('tbTunnel', false); - var tunnelOptions = tl.getInput('tbTunnelOptions'); - if (!tunnelOptions) { - tunnelOptions = ''; - } - new Promise(function(resolve) { - if (shouldStartTunnel) { - startTunnel(credentials, resolve, tunnelOptions); - } else { - resolve(true); - } - }).then(function(skipTunnel) { - var data = [ - 'TB_KEY', - 'TB_SECRET', - 'TB_API_ENDPOINT', - 'SELENIUM_PORT', - 'SELENIUM_HOST', - 'TB_BUILD_NAME' - ].filter(function(key) { - if (skipTunnel) { - return !/^TB_TUNNEL_/.test(key); - } else { - return true; - } - }).reduce(function (ret, key) { - ret[key] = tl.getVariable(key); - return ret; - }, {}); - data.CONNECTED_SERVICE_NAME = tl.getInput('connectedServiceName'); - fs.writeFileSync('testingbot.json', JSON.stringify(data)); - tl.command('task.addattachment', { type: 'TestingBotBuildResult', name: 'buildresults' }, path.resolve('testingbot.json')); - return true; - }).then(function() { - console.log('all started'); - process.exit(0); // SUCCESS - }).catch(function(err) { - console.log('error starting', err); - throw err; - }); -}); diff --git a/tb-main/tests/_suite.ts b/tb-main/tests/_suite.ts new file mode 100644 index 0000000..581cccf --- /dev/null +++ b/tb-main/tests/_suite.ts @@ -0,0 +1,41 @@ +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as ttm from 'azure-pipelines-task-lib/mock-test'; + +describe('TBMain (TestingBot Configuration)', function () { + this.timeout(20000); + + it('registers the secret, exports variables and writes the attachment when the tunnel is off', function (done) { + const tp = path.join(__dirname, 'no-tunnel.js'); + const tr = new ttm.MockTestRunner(tp); + tr.runAsync().then(() => { + assert.strictEqual(tr.succeeded, true, 'task should have succeeded. stderr: ' + tr.stderr); + + // Build name falls back / joins correctly. + assert.ok(tr.stdout.indexOf('My_Build_42') >= 0, 'TB_BUILD_NAME should be My_Build_42'); + + // Secret is registered (masked in logs) and set as a secret variable. + assert.ok(tr.stdout.indexOf('task.setsecret') >= 0, 'secret should be registered with setSecret'); + assert.ok( + tr.stdout.indexOf('TB_SECRET;isOutput=false;issecret=true;') >= 0, + 'TB_SECRET should be a secret variable' + ); + // ...and never exported as a non-secret variable. + assert.ok( + tr.stdout.indexOf('issecret=false;]my-secret') < 0, + 'the secret must not be exported as a non-secret variable' + ); + + // Attachment is written and still carries the credentials the results tab + // needs to sign TestingBot /mini share URLs. + assert.ok(tr.stdout.indexOf('task.addattachment') >= 0, 'should add the build-result attachment'); + const attachment = JSON.parse(fs.readFileSync(path.join(__dirname, 'testingbot.json'), 'utf8')); + assert.strictEqual(attachment.TB_KEY, 'my-key'); + assert.strictEqual(attachment.TB_SECRET, 'my-secret'); + assert.strictEqual(attachment.TB_BUILD_NAME, 'My_Build_42'); + + done(); + }).catch((err) => done(err)); + }); +}); diff --git a/tb-main/tests/no-tunnel.ts b/tb-main/tests/no-tunnel.ts new file mode 100644 index 0000000..c8ae8c6 --- /dev/null +++ b/tb-main/tests/no-tunnel.ts @@ -0,0 +1,26 @@ +import ma = require('azure-pipelines-task-lib/mock-answer'); +import tmrm = require('azure-pipelines-task-lib/mock-run'); +import * as path from 'path'; + +// Mock scenario: valid credentials, tunnel disabled. The task should export the +// variables, write the (secret-free) attachment and succeed without touching Java. +const taskPath = path.join(__dirname, '..', 'index.js'); +const tmr = new tmrm.TaskMockRunner(taskPath); + +tmr.setInput('connectedServiceName', 'tb-conn'); +tmr.setInput('tbTunnel', 'false'); + +// Endpoint auth (case-insensitive parameter lookup is exercised by the task). +process.env['ENDPOINT_URL_tb-conn'] = 'https://api.testingbot.com/v1/'; +process.env['ENDPOINT_AUTH_tb-conn'] = JSON.stringify({ + scheme: 'UsernamePassword', + parameters: { username: 'my-key', password: 'my-secret' } +}); +process.env['BUILD_DEFINITIONNAME'] = 'My Build'; +process.env['BUILD_BUILDID'] = '42'; +process.env['AGENT_TEMPDIRECTORY'] = __dirname; + +const answers: ma.TaskLibAnswers = {}; +tmr.setAnswers(answers); + +tmr.run(); diff --git a/tb-main/tsconfig.json b/tb-main/tsconfig.json new file mode 100644 index 0000000..d0b253f --- /dev/null +++ b/tb-main/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "moduleResolution": "node", + "lib": ["ES2020"], + "strict": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "skipLibCheck": true + }, + "include": ["index.ts", "tests/**/*.ts"] +} diff --git a/tb-main/tunnel/.DS_Store b/tb-main/tunnel/.DS_Store deleted file mode 100644 index 27b277f..0000000 Binary files a/tb-main/tunnel/.DS_Store and /dev/null differ diff --git a/tb-main/tunnel/2.30.jar b/tb-main/tunnel/2.30.jar deleted file mode 100644 index 1f426af..0000000 Binary files a/tb-main/tunnel/2.30.jar and /dev/null differ diff --git a/tb-stop-tunnel/index.ts b/tb-stop-tunnel/index.ts new file mode 100644 index 0000000..b8f2320 --- /dev/null +++ b/tb-stop-tunnel/index.ts @@ -0,0 +1,48 @@ +// This task is intentionally dependency-free: it only needs to kill a pid, so it +// reads TB_TUNNEL_PID from the environment, calls process.kill, and emits the +// raw Azure Pipelines logging command itself instead of pulling in +// azure-pipelines-task-lib. Do not add runtime dependencies here. + +function complete(result: 'Succeeded' | 'Failed', message: string): void { + console.log(`##vso[task.complete result=${result};]${message}`); +} + +function run(): void { + // Guard before touching the variable: if the tunnel was never started the + // variable is unset, and this should be a no-op rather than an error. + const rawPid = process.env.TB_TUNNEL_PID; + if (!rawPid) { + console.log('No TestingBot Tunnel pid found. The tunnel was probably not started.'); + complete('Succeeded', 'Nothing to stop.'); + return; + } + + // Accept only the exact "pid_" shape. A negative value would + // make process.kill signal an entire process group, and parseInt alone would + // let "123abc" through, so validate strictly before killing anything. + const match = /^pid_([1-9][0-9]*)$/.exec(rawPid); + if (!match) { + console.log('No valid TestingBot Tunnel pid found. The tunnel probably failed to start.'); + complete('Succeeded', 'Nothing to stop.'); + return; + } + const pid = parseInt(match[1], 10); + + try { + console.log(`Stopping TestingBot Tunnel (pid ${pid})...`); + process.kill(pid); + console.log('TestingBot Tunnel stopped.'); + complete('Succeeded', 'TestingBot Tunnel stopped.'); + } catch (err) { + if ((err as NodeJS.ErrnoException).code === 'ESRCH') { + // The process is already gone; nothing left to do. + console.log('TestingBot Tunnel process was already gone.'); + complete('Succeeded', 'Tunnel already stopped.'); + return; + } + complete('Failed', `Could not stop TestingBot Tunnel: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } +} + +run(); diff --git a/tb-stop-tunnel/package.json b/tb-stop-tunnel/package.json new file mode 100644 index 0000000..1c8a727 --- /dev/null +++ b/tb-stop-tunnel/package.json @@ -0,0 +1,11 @@ +{ + "name": "tb-stop-tunnel", + "version": "0.2.7", + "description": "Stop TestingBot Tunnel task", + "private": true, + "main": "index.js", + "license": "MIT", + "engines": { + "node": ">=20" + } +} diff --git a/tb-stop-tunnel/sample.js b/tb-stop-tunnel/sample.js deleted file mode 100644 index 490ea4a..0000000 --- a/tb-stop-tunnel/sample.js +++ /dev/null @@ -1,32 +0,0 @@ -// Guard before touching the variable: in jobs where the tunnel was never -// started, TB_TUNNEL_PID is unset and reading .replace() on it would throw -// before we ever reach the friendly "not started" message. -var rawPid = process.env.TB_TUNNEL_PID; -if (!rawPid) { - console.log('Unable to shut down TestingBot as no pid was found. Maybe the Tunnel was not started?'); - process.exit(0); -} - -// Accept only the exact "pid_" shape. parseInt would let -// "123abc" through and, worse, a negative value would make process.kill signal -// an entire process group, so validate strictly before killing anything. -var pidMatch = /^pid_([1-9][0-9]*)$/.exec(rawPid); -if (!pidMatch) { - console.log('Unable to shut down TestingBot as no valid pid was found. Maybe the Tunnel failed to start?'); - process.exit(0); -} -var pid = parseInt(pidMatch[1], 10); -console.log('Getting TB Tunnel PID: ', pid); - -console.log('Killing TestingBot Tunnel - ', pid); -try { - process.kill(pid); - console.log('Finished killing TestingBot Tunnel'); -} catch (err) { - if (err.code === 'ESRCH') { - // The process is already gone; nothing left to do. - console.log('TestingBot Tunnel process was already gone.'); - } else { - throw err; - } -} diff --git a/tb-stop-tunnel/task.json b/tb-stop-tunnel/task.json index 76a16b3..e040be0 100644 --- a/tb-stop-tunnel/task.json +++ b/tb-stop-tunnel/task.json @@ -4,12 +4,16 @@ "friendlyName": "Stop TestingBot Tunnel", "description": "Stop TestingBot Tunnel", "author": "TestingBot", - "helpMarkDown": "", + "helpMarkDown": "Stops the TestingBot Tunnel started by the TestingBot Configuration task.", "category": "Test", "visibility": [ "Build", "Release" ], + "runsOn": [ + "Agent", + "DeploymentGroup" + ], "demands": [], "version": { "Major": "0", @@ -17,11 +21,19 @@ "Patch": "0" }, "minimumAgentVersion": "3.232.1", - "instanceNameFormat": "Stop TestingBot Tunnel $(message)", + "instanceNameFormat": "Stop TestingBot Tunnel", "inputs": [], + "restrictions": { + "commands": { + "mode": "restricted" + }, + "settableVariables": { + "allowed": [] + } + }, "execution": { "Node20_1": { - "target": "sample.js", + "target": "index.js", "argumentFormat": "" } } diff --git a/tb-stop-tunnel/tests/_suite.ts b/tb-stop-tunnel/tests/_suite.ts new file mode 100644 index 0000000..d779825 --- /dev/null +++ b/tb-stop-tunnel/tests/_suite.ts @@ -0,0 +1,63 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { spawnSync, spawn } from 'child_process'; + +// The stop task is dependency-free, so we exercise the compiled index.js +// directly as a child process rather than through task-lib's MockTestRunner. +const taskPath = path.join(__dirname, '..', 'index.js'); + +function runStop(tunnelPid: string | undefined): { stdout: string; status: number | null } { + const env = { ...process.env } as NodeJS.ProcessEnv; + if (tunnelPid === undefined) { + delete env.TB_TUNNEL_PID; + } else { + env.TB_TUNNEL_PID = tunnelPid; + } + const res = spawnSync(process.execPath, [taskPath], { env, encoding: 'utf8' }); + return { stdout: res.stdout || '', status: res.status }; +} + +describe('TBStopTunnel (Stop TestingBot Tunnel)', function () { + this.timeout(10000); + + it('is a graceful no-op when no pid is set', function () { + const { stdout, status } = runStop(undefined); + assert.strictEqual(status, 0); + assert.ok(stdout.indexOf('was probably not started') >= 0, stdout); + assert.ok(stdout.indexOf('##vso[task.complete result=Succeeded;]') >= 0, stdout); + }); + + for (const bad of ['pid_0', 'pid_-1', 'pid_12abc', 'not_a_pid']) { + it(`rejects the malformed pid ${bad} without killing anything`, function () { + const { stdout, status } = runStop(bad); + assert.strictEqual(status, 0); + assert.ok(stdout.indexOf('No valid') >= 0, stdout); + }); + } + + it('treats an already-gone pid as success (ESRCH)', function () { + // 2^31-1: valid shape, extremely unlikely to be a live process. + const { stdout, status } = runStop('pid_2147483647'); + assert.strictEqual(status, 0); + assert.ok(stdout.indexOf('already gone') >= 0, stdout); + }); + + it('actually kills a running process', function (done) { + const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { stdio: 'ignore' }); + child.on('spawn', () => { + const { status } = runStop(`pid_${child.pid}`); + assert.strictEqual(status, 0); + // Give the signal a moment, then confirm the process is gone. + setTimeout(() => { + let alive = true; + try { + process.kill(child.pid as number, 0); + } catch (err) { + alive = (err as NodeJS.ErrnoException).code !== 'ESRCH'; + } + assert.strictEqual(alive, false, 'child process should have been killed'); + done(); + }, 200); + }); + }); +}); diff --git a/tb-stop-tunnel/tsconfig.json b/tb-stop-tunnel/tsconfig.json new file mode 100644 index 0000000..d0b253f --- /dev/null +++ b/tb-stop-tunnel/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "moduleResolution": "node", + "lib": ["ES2020"], + "strict": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "skipLibCheck": true + }, + "include": ["index.ts", "tests/**/*.ts"] +} diff --git a/webpack.config.js b/webpack.config.js index 533e8bd..6abc8e6 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -18,6 +18,18 @@ const vssExternals = [ 'React' ]; +// The tasks are TypeScript: tsc emits index.js in-place, which this copy step +// picks up. The .ts sources, tests, tsconfig and dev node_modules are excluded; +// the production node_modules are installed into dist/ afterwards. +const taskIgnore = [ + '**/node_modules/**', + '**/.DS_Store', + '**/*.ts', + '**/tsconfig.json', + '**/tests/**', + '**/*.js.map' +]; + module.exports = { target: 'web', entry: { @@ -59,12 +71,12 @@ module.exports = { { from: 'tb-main', to: 'tb-main', - globOptions: { ignore: ['**/node_modules/**', '**/.DS_Store'] } + globOptions: { ignore: taskIgnore } }, { from: 'tb-stop-tunnel', to: 'tb-stop-tunnel', - globOptions: { ignore: ['**/.DS_Store'] } + globOptions: { ignore: taskIgnore } }, { // Everything in tb-build-info except the scripts webpack bundles above.