diff --git a/biome.json b/biome.json index ba81761c..c58a45ce 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.3/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.4/schema.json", "assist": { "actions": { "source": { "organizeImports": "on" } } }, "linter": { "enabled": true, diff --git a/package-lock.json b/package-lock.json index 0148ecea..e943b3ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,13 @@ "@cyclonedx/cdxgen": "^11.11.0", "@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.12", "@inquirer/prompts": "^8.0.1", + "@napi-rs/keyring": "^1.2.0", "@oclif/core": "^4.8.0", "@oclif/plugin-help": "^6.2.32", "@oclif/plugin-update": "^4.7.14", "@oclif/table": "^0.5.1", "date-fns": "^4.1.0", + "graphql": "^16.11.0", "node-machine-id": "^1.1.12", "ora": "^9.0.0", "packageurl-js": "^2.0.1", @@ -43,7 +45,8 @@ "sinon": "^21.0.0", "ts-node": "^10.9.2", "tsx": "^4.20.6", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^1.6.0" }, "engines": { "node": ">=20.0.0" @@ -3885,10 +3888,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "license": "MIT" + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", @@ -3912,6 +3914,212 @@ "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", "license": "MIT" }, + "node_modules/@napi-rs/keyring": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring/-/keyring-1.2.0.tgz", + "integrity": "sha512-d0d4Oyxm+v980PEq1ZH2PmS6cvpMIRc17eYpiU47KgW+lzxklMu6+HOEOPmxrpnF/XQZ0+Q78I2mgMhbIIo/dg==", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/keyring-darwin-arm64": "1.2.0", + "@napi-rs/keyring-darwin-x64": "1.2.0", + "@napi-rs/keyring-freebsd-x64": "1.2.0", + "@napi-rs/keyring-linux-arm-gnueabihf": "1.2.0", + "@napi-rs/keyring-linux-arm64-gnu": "1.2.0", + "@napi-rs/keyring-linux-arm64-musl": "1.2.0", + "@napi-rs/keyring-linux-riscv64-gnu": "1.2.0", + "@napi-rs/keyring-linux-x64-gnu": "1.2.0", + "@napi-rs/keyring-linux-x64-musl": "1.2.0", + "@napi-rs/keyring-win32-arm64-msvc": "1.2.0", + "@napi-rs/keyring-win32-ia32-msvc": "1.2.0", + "@napi-rs/keyring-win32-x64-msvc": "1.2.0" + } + }, + "node_modules/@napi-rs/keyring-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-arm64/-/keyring-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-CA83rDeyONDADO25JLZsh3eHY8yTEtm/RS6ecPsY+1v+dSawzT9GywBMu2r6uOp1IEhQs/xAfxgybGAFr17lSA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-x64/-/keyring-darwin-x64-1.2.0.tgz", + "integrity": "sha512-dBHjtKRCj4ByfnfqIKIJLo3wueQNJhLRyuxtX/rR4K/XtcS7VLlRD01XXizjpre54vpmObj63w+ZpHG+mGM8uA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-freebsd-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-freebsd-x64/-/keyring-freebsd-x64-1.2.0.tgz", + "integrity": "sha512-DPZFr11pNJSnaoh0dzSUNF+T6ORhy3CkzUT3uGixbA71cAOPJ24iG8e8QrLOkuC/StWrAku3gBnth2XMWOcR3Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm-gnueabihf": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm-gnueabihf/-/keyring-linux-arm-gnueabihf-1.2.0.tgz", + "integrity": "sha512-8xv6DyEMlvRdqJzp4F39RLUmmTQsLcGYYv/3eIfZNZN1O5257tHxTrFYqAsny659rJJK2EKeSa7PhrSibQqRWQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm64-gnu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-gnu/-/keyring-linux-arm64-gnu-1.2.0.tgz", + "integrity": "sha512-Pu2V6Py+PBt7inryEecirl+t+ti8bhZphjP+W68iVaXHUxLdWmkgL9KI1VkbRHbx5k8K5Tew9OP218YfmVguIA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm64-musl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-musl/-/keyring-linux-arm64-musl-1.2.0.tgz", + "integrity": "sha512-8TDymrpC4P1a9iDEaegT7RnrkmrJN5eNZh3Im3UEV5PPYGtrb82CRxsuFohthCWQW81O483u1bu+25+XA4nKUw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-riscv64-gnu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-riscv64-gnu/-/keyring-linux-riscv64-gnu-1.2.0.tgz", + "integrity": "sha512-awsB5XI1MYL7fwfjMDGmKOWvNgJEO7mM7iVEMS0fO39f0kVJnOSjlu7RHcXAF0LOx+0VfF3oxbWqJmZbvRCRHw==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-x64-gnu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-gnu/-/keyring-linux-x64-gnu-1.2.0.tgz", + "integrity": "sha512-8E+7z4tbxSJXxIBqA+vfB1CGajpCDRyTyqXkBig5NtASrv4YXcntSo96Iah2QDR5zD3dSTsmbqJudcj9rKKuHQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-x64-musl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-musl/-/keyring-linux-x64-musl-1.2.0.tgz", + "integrity": "sha512-8RZ8yVEnmWr/3BxKgBSzmgntI7lNEsY7xouNfOsQkuVAiCNmxzJwETspzK3PQ2FHtDxgz5vHQDEBVGMyM4hUHA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-arm64-msvc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-arm64-msvc/-/keyring-win32-arm64-msvc-1.2.0.tgz", + "integrity": "sha512-AoqaDZpQ6KPE19VBLpxyORcp+yWmHI9Xs9Oo0PJ4mfHma4nFSLVdhAubJCxdlNptHe5va7ghGCHj3L9Akiv4cQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-ia32-msvc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-ia32-msvc/-/keyring-win32-ia32-msvc-1.2.0.tgz", + "integrity": "sha512-EYL+EEI6bCsYi3LfwcQdnX3P/R76ENKNn+3PmpGheBsUFLuh0gQuP7aMVHM4rTw6UVe+L3vCLZSptq/oeacz0A==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-x64-msvc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-x64-msvc/-/keyring-win32-x64-msvc-1.2.0.tgz", + "integrity": "sha512-xFlx/TsmqmCwNU9v+AVnEJgoEAlBYgzFF5Ihz1rMpPAt4qQWWkMd4sCyM1gMJ1A/GnRqRegDiQpwaxGUHFtFbA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4119,7 +4327,6 @@ "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.8.0.tgz", "integrity": "sha512-jteNUQKgJHLHFbbz806aGZqf+RJJ7t4gwF4MYa8fCwCxQ8/klJNWc0MvaJiBebk7Mc+J39mdlsB4XraaCKznFw==", "license": "MIT", - "peer": true, "dependencies": { "ansi-escapes": "^4.3.2", "ansis": "^3.17.0", @@ -4901,86 +5108,372 @@ "node": ">=12" } }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", - "license": "MIT" - }, - "node_modules/@sindresorhus/is": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.0.tgz", - "integrity": "sha512-7F/yz2IphV39hiS2zB4QYVkivrptHHh0K8qJJd9HhuWSdvf8AN7NpebW3CcDZDBQsUPMoDKWsY2WWgW7bqOcfA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@sinonjs/samsam": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", - "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "lodash.get": "^4.4.2", - "type-detect": "^4.1.0" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@sinonjs/samsam/node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@smithy/abort-controller": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", - "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@smithy/chunked-blob-reader": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", - "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.0.tgz", + "integrity": "sha512-7F/yz2IphV39hiS2zB4QYVkivrptHHh0K8qJJd9HhuWSdvf8AN7NpebW3CcDZDBQsUPMoDKWsY2WWgW7bqOcfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", + "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", + "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -5807,6 +6300,12 @@ "@types/ms": "*" } }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -5867,7 +6366,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -5883,7 +6381,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5940,40 +6437,136 @@ "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", "license": "MIT" }, - "node_modules/@wry/caches": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz", - "integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==", - "license": "MIT", + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, "dependencies": { - "tslib": "^2.3.0" + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@wry/context": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz", - "integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==", - "license": "MIT", + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, "dependencies": { - "tslib": "^2.3.0" + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@wry/equality": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz", - "integrity": "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==", - "license": "MIT", + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, "dependencies": { - "tslib": "^2.3.0" + "yocto-queue": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@wry/caches": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz", + "integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/context": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz", + "integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/equality": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz", + "integrity": "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/@wry/trie": { @@ -6038,7 +6631,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6169,6 +6761,15 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/ast-types": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", @@ -6549,6 +7150,15 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/cacache": { "version": "19.0.1", "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", @@ -6730,6 +7340,33 @@ "upper-case-first": "^2.0.2" } }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chai/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -6754,6 +7391,18 @@ "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", "license": "MIT" }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, "node_modules/cheerio": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", @@ -7065,6 +7714,12 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true + }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -7331,6 +7986,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -7463,6 +8130,15 @@ "node": ">=0.3.1" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/discontinuous-range": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", @@ -7889,6 +8565,15 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -8367,6 +9052,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -8872,7 +9566,6 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", "license": "MIT", - "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -9112,6 +9805,15 @@ "node": ">= 14" } }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -9992,6 +10694,22 @@ "url": "https://github.com/sponsors/antonk52" } }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -10061,6 +10779,15 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -10090,6 +10817,15 @@ "devOptional": true, "license": "ISC" }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -10162,6 +10898,12 @@ "node": ">= 0.8" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -10435,6 +11177,24 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, "node_modules/mock-fs": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.5.0.tgz", @@ -10497,6 +11257,24 @@ "license": "MIT", "optional": true }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -11493,6 +12271,21 @@ "node": "20 || >=22" } }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/pg-connection-string": { "version": "2.9.1", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", @@ -11519,6 +12312,51 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postcss-selector-parser": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", @@ -11580,6 +12418,56 @@ "integrity": "sha512-kuoTbmC+QQUfx45PrdkVzJqrNEp2lhK++WGyiqBx6JrCvZUQDgeYjdV3h53n7p+37s1Iwx6GjAQ7fcIgD8kkLQ==", "license": "MIT" }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "node_modules/proc-log": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", @@ -11848,7 +12736,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -12127,6 +13014,47 @@ "node": ">=8.0" } }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12156,7 +13084,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -12492,6 +13419,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -12697,6 +13630,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -12797,6 +13739,12 @@ "node": ">=8" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -12807,6 +13755,12 @@ "node": ">= 0.6" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -12916,6 +13870,18 @@ "node": ">=0.10.0" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -12925,13 +13891,31 @@ "node": ">=0.10.0" } }, - "node_modules/strnum": { + "node_modules/strip-literal": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", - "funding": [ - { - "type": "github", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], @@ -13169,6 +14153,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -13204,7 +14194,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13212,6 +14201,24 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -13387,7 +14394,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13396,6 +14402,12 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true + }, "node_modules/undici": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", @@ -13729,6 +14741,659 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/vitest/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/walk-up-path": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", @@ -13780,6 +15445,22 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", diff --git a/package.json b/package.json index aaf21142..c48998ff 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "clean:files": "shx rm -f herodevs.**.csv herodevs.**.json herodevs.**.txt", "dev": "npm run build && ./bin/dev.js", "dev:debug": "npm run build && DEBUG=oclif:* ./bin/dev.js", + "dev:auth:local": "OAUTH_CONNECT_URL='http://localhost:6040/realms/herodevs_local/protocol/openid-connect' npm run dev auth login", + "dev:auth:logout": "npm run dev auth logout", "format": "biome format --write", "lint": "biome lint --write", "postpack": "shx rm -f oclif.manifest.json", @@ -27,7 +29,8 @@ "prepack": "oclif manifest", "pretest": "npm run lint && npm run typecheck", "readme": "npm run ci:fix && npm run build && oclif readme", - "test": "globstar -- node --import tsx --test --experimental-test-module-mocks \"test/**/*.test.ts\"", + "test": "vitest run", + "test:watch": "vitest watch", "test:e2e": "globstar -- node --import tsx --test \"e2e/**/*.test.ts\"", "typecheck": "tsc --noEmit", "version": "oclif manifest", @@ -43,12 +46,14 @@ "@apollo/client": "^4.0.9", "@cyclonedx/cdxgen": "^11.11.0", "@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.12", + "@napi-rs/keyring": "^1.2.0", "@inquirer/prompts": "^8.0.1", "@oclif/core": "^4.8.0", "@oclif/plugin-help": "^6.2.32", "@oclif/plugin-update": "^4.7.14", "@oclif/table": "^0.5.1", "date-fns": "^4.1.0", + "graphql": "^16.11.0", "node-machine-id": "^1.1.12", "ora": "^9.0.0", "packageurl-js": "^2.0.1", @@ -70,7 +75,8 @@ "sinon": "^21.0.0", "ts-node": "^10.9.2", "tsx": "^4.20.6", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^1.6.0" }, "engines": { "node": ">=20.0.0" @@ -115,4 +121,4 @@ } }, "types": "dist/index.d.ts" -} +} \ No newline at end of file diff --git a/src/api/nes.client.ts b/src/api/nes.client.ts index 6f0dabc5..6334329b 100644 --- a/src/api/nes.client.ts +++ b/src/api/nes.client.ts @@ -8,10 +8,23 @@ import type { } from '@herodevs/eol-shared'; import type { GraphQLFormattedError } from 'graphql'; import { config } from '../config/constants.ts'; +import { requireAccessToken } from '../service/auth.svc.ts'; import { debugLogger } from '../service/log.svc.ts'; import { stripTypename } from '../utils/strip-typename.ts'; import { createReportMutation, getEolReportQuery } from './gql-operations.ts'; +const createAuthorizedFetch = (): typeof fetch => async (input, init) => { + const headers = new Headers(init?.headers); + + if (config.enableAuth) { + // Temporary gate while legacy commands migrate to authenticated flow + const token = await requireAccessToken(); + headers.set('Authorization', `Bearer ${token}`); + } + + return fetch(input, { ...init, headers }); +}; + type GraphQLExecutionResult = { errors?: ReadonlyArray; }; @@ -25,6 +38,7 @@ export const createApollo = (uri: string) => }, link: new HttpLink({ uri, + fetch: createAuthorizedFetch(), headers: { 'User-Agent': `hdcli/${process.env.npm_package_version ?? 'unknown'}`, }, diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts new file mode 100644 index 00000000..ec76ce01 --- /dev/null +++ b/src/commands/auth/login.ts @@ -0,0 +1,159 @@ +import crypto from 'node:crypto'; +import http from 'node:http'; +import { createInterface } from 'node:readline'; +import { URL } from 'node:url'; +import { Command } from '@oclif/core'; +import { persistTokenResponse } from '../../service/auth.svc.ts'; +import { getClientId, getRealmUrl } from '../../service/auth-config.svc.ts'; +import type { TokenResponse } from '../../types/auth.ts'; +import { openInBrowser } from '../../utils/open-in-browser.ts'; + +export default class AuthLogin extends Command { + static description = 'OAuth CLI login'; + + private server?: http.Server; + private readonly port = parseInt(process.env.OAUTH_CALLBACK_PORT || '4000', 10); + private readonly redirectUri = process.env.OAUTH_CALLBACK_REDIRECT || `http://localhost:${this.port}/oauth2/callback`; + private readonly realmUrl = getRealmUrl(); + private readonly clientId = getClientId(); + + async run() { + if (typeof (this.config as { runHook?: unknown }).runHook === 'function') { + await this.parse(AuthLogin); + } + + const codeVerifier = crypto.randomBytes(32).toString('base64url'); + const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url'); + const state = crypto.randomBytes(16).toString('hex'); + + const authUrl = + `${this.realmUrl}/auth?` + + `client_id=${this.clientId}` + + `&response_type=code` + + `&redirect_uri=${encodeURIComponent(this.redirectUri)}` + + `&code_challenge=${codeChallenge}` + + `&code_challenge_method=S256` + + `&state=${state}`; + + const code = await this.startServerAndAwaitCode(authUrl, state); + const token = await this.exchangeCodeForToken(code, codeVerifier); + + try { + await persistTokenResponse(token); + } catch (error) { + this.warn(`Failed to store tokens securely: ${error instanceof Error ? error.message : error}`); + } + } + + private startServerAndAwaitCode(authUrl: string, expectedState: string): Promise { + return new Promise((resolve, reject) => { + this.server = http.createServer((req, res) => { + if (!req.url) { + res.writeHead(400); + res.end('Invalid request'); + return; + } + + let parsedUrl: URL; + try { + parsedUrl = new URL(req.url, `http://localhost:${this.port}`); + } catch { + res.writeHead(400, { 'Content-Type': 'text/plain' }); + res.end('Invalid callback URL'); + this.stopServer(); + reject(new Error('Invalid callback URL')); + return; + } + + if (parsedUrl.pathname === '/oauth2/callback') { + const code = parsedUrl.searchParams.get('code'); + const state = parsedUrl.searchParams.get('state'); + + if (!state) { + res.writeHead(400, { 'Content-Type': 'text/plain' }); + res.end('Missing state parameter.'); + this.stopServer(); + return reject(new Error('Missing state parameter in callback')); + } + + if (state !== expectedState) { + res.writeHead(400, { 'Content-Type': 'text/plain' }); + res.end('State verification failed. Please restart the login flow.'); + this.stopServer(); + return reject(new Error('State verification failed')); + } + + if (code) { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Login successful. You can close this window.'); + this.stopServer(); + resolve(code); + } else { + res.writeHead(400, { 'Content-Type': 'text/plain' }); + res.end('No authorization code returned. Please try again.'); + this.stopServer(); + reject(new Error('No code returned from Keycloak')); + } + } else { + res.writeHead(404); + res.end(); + } + }); + + this.server.listen(this.port, async () => { + await new Promise((resolve) => { + const rl = createInterface({ input: process.stdin, output: process.stdout }); + rl.question(`Press Enter to navigate to: ${authUrl}\n`, () => { + rl.close(); + resolve(); + }); + }); + + try { + await openInBrowser(authUrl); + } catch (err) { + this.warn( + `Failed to open browser automatically. Please open this URL manually:\n${authUrl}\n${err instanceof Error ? err.message : err}`, + ); + } + }); + + this.server.on('error', (err) => { + this.stopServer(); + reject(err); + }); + }); + } + + private async stopServer() { + if (this.server) { + await new Promise((resolve, reject) => this.server?.close((err) => (err ? reject(err) : resolve()))); + this.server = undefined; + } + } + + private async exchangeCodeForToken(code: string, codeVerifier: string): Promise { + const tokenUrl = `${this.realmUrl}/token`; + + const params = new URLSearchParams({ + grant_type: 'authorization_code', + client_id: this.clientId, + redirect_uri: this.redirectUri, + code_verifier: codeVerifier, + code, + }); + + const response = await fetch(tokenUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: params.toString(), + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Token exchange failed: ${response.status} ${response.statusText}\n${text}`); + } + + return response.json() as Promise; + } +} diff --git a/src/commands/auth/logout.ts b/src/commands/auth/logout.ts new file mode 100644 index 00000000..069abe55 --- /dev/null +++ b/src/commands/auth/logout.ts @@ -0,0 +1,29 @@ +import { Command } from '@oclif/core'; +import { logoutFromProvider } from '../../service/auth-refresh.svc.ts'; +import { clearStoredTokens, getStoredTokens } from '../../service/auth-token.svc.ts'; + +export default class AuthLogout extends Command { + static description = 'Logs out of HeroDevs OAuth and clears stored tokens'; + + async run() { + if (typeof (this.config as { runHook?: unknown }).runHook === 'function') { + await this.parse(AuthLogout); + } + + const tokens = await getStoredTokens(); + if (!tokens?.accessToken && !tokens?.refreshToken) { + this.log('No stored authentication tokens found.'); + return; + } + + try { + await logoutFromProvider(tokens?.refreshToken); + this.log('Logged out of HeroDevs OAuth provider.'); + } catch (error) { + this.warn(`Failed to revoke tokens remotely: ${error instanceof Error ? error.message : error}`); + } + + await clearStoredTokens(); + this.log('Local authentication tokens removed from your system.'); + } +} diff --git a/src/config/constants.ts b/src/config/constants.ts index 8f928e86..16628ad0 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -10,6 +10,13 @@ export const GIT_OUTPUT_FORMAT = `"${['%h', '%an', '%ad'].join('|')}"`; export const DEFAULT_DATE_FORMAT = 'yyyy-MM-dd'; export const DEFAULT_DATE_COMMIT_FORMAT = 'MM/dd/yyyy, h:mm:ss a'; export const DEFAULT_DATE_COMMIT_MONTH_FORMAT = 'MMMM yyyy'; +export const ENABLE_AUTH = false; + +const toBoolean = (value: string | undefined): boolean | undefined => { + if (value === 'true') return true; + if (value === 'false') return false; + return undefined; +}; let concurrentPageRequests = CONCURRENT_PAGE_REQUESTS; const parsed = Number.parseInt(process.env.CONCURRENT_PAGE_REQUESTS ?? '0', 10); @@ -28,6 +35,7 @@ export const config = { graphqlHost: process.env.GRAPHQL_HOST || GRAPHQL_HOST, graphqlPath: process.env.GRAPHQL_PATH || GRAPHQL_PATH, analyticsUrl: process.env.ANALYTICS_URL || ANALYTICS_URL, + enableAuth: toBoolean(process.env.ENABLE_AUTH) ?? ENABLE_AUTH, concurrentPageRequests, pageSize, }; diff --git a/src/service/auth-config.svc.ts b/src/service/auth-config.svc.ts new file mode 100644 index 00000000..4b85f115 --- /dev/null +++ b/src/service/auth-config.svc.ts @@ -0,0 +1,25 @@ +const DEFAULT_REALM_URL = 'https://idp.prod.apps.herodevs.io/realms/universe/protocol/openid-connect'; +const DEFAULT_CLIENT_ID = 'default-public'; +const DEFAULT_SERVICE_NAME = '@herodevs/cli'; +const DEFAULT_ACCESS_KEY = 'access-token'; +const DEFAULT_REFRESH_KEY = 'refresh-token'; + +export function getRealmUrl() { + return process.env.OAUTH_CONNECT_URL || DEFAULT_REALM_URL; +} + +export function getClientId() { + return process.env.OAUTH_CLIENT_ID || DEFAULT_CLIENT_ID; +} + +export function getTokenServiceName() { + return process.env.HD_AUTH_SERVICE_NAME || DEFAULT_SERVICE_NAME; +} + +export function getAccessTokenKey() { + return process.env.HD_AUTH_ACCESS_KEY || DEFAULT_ACCESS_KEY; +} + +export function getRefreshTokenKey() { + return process.env.HD_AUTH_REFRESH_KEY || DEFAULT_REFRESH_KEY; +} diff --git a/src/service/auth-refresh.svc.ts b/src/service/auth-refresh.svc.ts new file mode 100644 index 00000000..309812a6 --- /dev/null +++ b/src/service/auth-refresh.svc.ts @@ -0,0 +1,62 @@ +import type { TokenResponse } from '../types/auth.ts'; +import { getClientId, getRealmUrl } from './auth-config.svc.ts'; + +interface AuthOptions { + clientId?: string; + realmUrl?: string; +} + +export async function refreshTokens(refreshToken: string, options: AuthOptions = {}): Promise { + const clientId = options.clientId ?? getClientId(); + const realmUrl = options.realmUrl ?? getRealmUrl(); + const tokenUrl = `${realmUrl}/token`; + + const body = new URLSearchParams({ + grant_type: 'refresh_token', + client_id: clientId, + refresh_token: refreshToken, + }); + + const response = await fetch(tokenUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString(), + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Token refresh failed: ${response.status} ${response.statusText}\n${text}`); + } + + return response.json() as Promise; +} + +export async function logoutFromProvider(refreshToken: string | undefined, options: AuthOptions = {}) { + if (!refreshToken) { + return; + } + + const clientId = options.clientId ?? getClientId(); + const realmUrl = options.realmUrl ?? getRealmUrl(); + const logoutUrl = `${realmUrl}/logout`; + + const body = new URLSearchParams({ + client_id: clientId, + refresh_token: refreshToken, + }); + + const response = await fetch(logoutUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString(), + }); + + if (!response.ok) { + const text = await response.text(); + if (response.status === 400 && text.includes('invalid_grant')) { + return; + } + + throw new Error(`Logout failed: ${response.status} ${response.statusText}\n${text}`); + } +} diff --git a/src/service/auth-token.svc.ts b/src/service/auth-token.svc.ts new file mode 100644 index 00000000..10328802 --- /dev/null +++ b/src/service/auth-token.svc.ts @@ -0,0 +1,77 @@ +import { AsyncEntry } from '@napi-rs/keyring'; +import { getAccessTokenKey, getRefreshTokenKey, getTokenServiceName } from './auth-config.svc.ts'; + +export interface StoredTokens { + accessToken?: string; + refreshToken?: string; +} + +const TOKEN_SKEW_SECONDS = 30; + +export async function saveTokens(tokens: { accessToken: string; refreshToken?: string }) { + const service = getTokenServiceName(); + const accessKey = getAccessTokenKey(); + const refreshKey = getRefreshTokenKey(); + + const accessTokenSet = new AsyncEntry(service, accessKey).setPassword(tokens.accessToken); + const refreshTokenSet = tokens.refreshToken + ? new AsyncEntry(service, refreshKey).setPassword(tokens.refreshToken) + : new AsyncEntry(service, refreshKey).deletePassword(); + + return Promise.all([accessTokenSet, refreshTokenSet]); +} + +export async function getStoredTokens(): Promise { + const service = getTokenServiceName(); + const accessKey = getAccessTokenKey(); + const refreshKey = getRefreshTokenKey(); + + return Promise.all([ + new AsyncEntry(service, accessKey).getPassword(), + new AsyncEntry(service, refreshKey).getPassword(), + ]).then(([accessToken, refreshToken]) => { + if (!accessToken && !refreshToken) { + return; + } + + return { + accessToken, + refreshToken, + }; + }); +} + +export async function clearStoredTokens() { + const service = getTokenServiceName(); + const accessKey = getAccessTokenKey(); + const refreshKey = getRefreshTokenKey(); + + return Promise.all([ + new AsyncEntry(service, accessKey).deletePassword(), + new AsyncEntry(service, refreshKey).deletePassword(), + ]); +} + +export function isAccessTokenExpired(token: string | undefined): boolean { + if (!token) { + return true; + } + + try { + const [, payloadB64] = token.split('.'); + if (!payloadB64) { + return true; + } + + const payloadJson = Buffer.from(payloadB64, 'base64url').toString('utf8'); + const payload = JSON.parse(payloadJson) as { exp?: number }; + if (!payload.exp) { + return true; + } + + const now = Date.now() / 1000; + return now + TOKEN_SKEW_SECONDS >= payload.exp; + } catch { + return true; + } +} diff --git a/src/service/auth.svc.ts b/src/service/auth.svc.ts new file mode 100644 index 00000000..92b9ad34 --- /dev/null +++ b/src/service/auth.svc.ts @@ -0,0 +1,42 @@ +import type { TokenResponse } from '../types/auth.ts'; +import { refreshTokens } from './auth-refresh.svc.ts'; +import { clearStoredTokens, getStoredTokens, isAccessTokenExpired, saveTokens } from './auth-token.svc.ts'; + +export async function persistTokenResponse(token: TokenResponse) { + await saveTokens({ + accessToken: token.access_token, + refreshToken: token.refresh_token, + }); +} + +export async function getAccessToken(): Promise { + const tokens = await getStoredTokens(); + if (!tokens) { + return; + } + + if (tokens.accessToken && !isAccessTokenExpired(tokens.accessToken)) { + return tokens.accessToken; + } + + if (!tokens.refreshToken) { + return; + } + + const refreshed = await refreshTokens(tokens.refreshToken); + await persistTokenResponse(refreshed); + return refreshed.access_token; +} + +export async function requireAccessToken(): Promise { + const token = await getAccessToken(); + if (!token) { + throw new Error('You are not logged in. Run "hd auth login" to authenticate.'); + } + + return token; +} + +export async function logoutLocally() { + await clearStoredTokens(); +} diff --git a/src/types/auth.ts b/src/types/auth.ts new file mode 100644 index 00000000..85ddef6e --- /dev/null +++ b/src/types/auth.ts @@ -0,0 +1,9 @@ +export interface TokenResponse { + access_token: string; + refresh_token?: string; + expires_in?: number; + refresh_expires_in?: number; + token_type?: string; + scope?: string; + id_token?: string; +} diff --git a/src/utils/open-in-browser.ts b/src/utils/open-in-browser.ts new file mode 100644 index 00000000..dcb4da00 --- /dev/null +++ b/src/utils/open-in-browser.ts @@ -0,0 +1,21 @@ +import { exec } from 'node:child_process'; +import { platform } from 'node:os'; + +export function openInBrowser(url: string): Promise { + return new Promise((resolve, reject) => { + const escapedUrl = `"${url.replace(/"/g, '\\"')}"`; + + const command = (() => { + const plat = platform(); + + if (plat === 'darwin') return `open ${escapedUrl}`; // macOS + if (plat === 'win32') return `start "" ${escapedUrl}`; // Windows + return `xdg-open ${escapedUrl}`; // Linux + })(); + + exec(command, (err) => { + if (err) reject(new Error(`Failed to open browser: ${err.message}`)); + else resolve(); + }); + }); +} diff --git a/src/utils/strip-typename.ts b/src/utils/strip-typename.ts index d3f913cf..bfbc2d1b 100644 --- a/src/utils/strip-typename.ts +++ b/src/utils/strip-typename.ts @@ -1,17 +1,18 @@ export function stripTypename(obj: T): T { if (Array.isArray(obj)) { - return obj.map(stripTypename) as any; + return obj.map(stripTypename) as unknown as T; } if (obj !== null && typeof obj === 'object') { - const result: any = {}; + const result: Record = {}; for (const key of Object.keys(obj)) { if (key === '__typename') continue; - result[key] = stripTypename((obj as any)[key]); + const value = (obj as Record)[key]; + result[key] = stripTypename(value); } - return result; + return result as unknown as T; } return obj; diff --git a/test/api/nes.client.test.ts b/test/api/nes.client.test.ts index ab0cc263..dba71db0 100644 --- a/test/api/nes.client.test.ts +++ b/test/api/nes.client.test.ts @@ -1,5 +1,3 @@ -import assert from 'node:assert'; -import { afterEach, beforeEach, describe, it } from 'node:test'; import type { CreateEolReportInput } from '@herodevs/eol-shared'; import { submitScan } from '../../src/api/nes.client.ts'; import { FetchMock } from '../utils/mocks/fetch.mock.ts'; @@ -47,9 +45,9 @@ describe('nes.client', () => { }; const res = await submitScan(input); - assert.strictEqual(res.id, 'test-123'); - assert.strictEqual(Array.isArray(res.components), true); - assert.strictEqual(res.components.length, components.length); + expect(res.id).toBe('test-123'); + expect(Array.isArray(res.components)).toBe(true); + expect(res.components).toHaveLength(components.length); }); it('throws when mutation returns unsuccessful response or no report', async () => { @@ -60,7 +58,7 @@ describe('nes.client', () => { const input: CreateEolReportInput = { sbom: { bomFormat: 'CycloneDX', components: [], specVersion: '1.4', version: 1 }, }; - await assert.rejects(() => submitScan(input), /Failed to create EOL report/); + await expect(submitScan(input)).rejects.toThrow(/Failed to create EOL report/); }); it('throws when GraphQL errors are present in createReport mutation', async () => { @@ -71,7 +69,7 @@ describe('nes.client', () => { const input: CreateEolReportInput = { sbom: { bomFormat: 'CycloneDX', components: [], specVersion: '1.4', version: 1 }, }; - await assert.rejects(() => submitScan(input), /Failed to create EOL report/); + await expect(submitScan(input)).rejects.toThrow(/Failed to create EOL report/); }); it('throws when GraphQL errors are present in getReport query', async () => { @@ -86,7 +84,7 @@ describe('nes.client', () => { const input: CreateEolReportInput = { sbom: { bomFormat: 'CycloneDX', components: [], specVersion: '1.4', version: 1 }, }; - await assert.rejects(() => submitScan(input), /Failed to fetch EOL report/); + await expect(submitScan(input)).rejects.toThrow(/Failed to fetch EOL report/); }); it('throws when multiple GraphQL errors are present', async () => { @@ -98,7 +96,7 @@ describe('nes.client', () => { const input: CreateEolReportInput = { sbom: { bomFormat: 'CycloneDX', components: [], specVersion: '1.4', version: 1 }, }; - await assert.rejects(() => submitScan(input), /Failed to create EOL report/); + await expect(submitScan(input)).rejects.toThrow(/Failed to create EOL report/); }); it('throws with generic message when GraphQL errors have no message', async () => { @@ -107,6 +105,6 @@ describe('nes.client', () => { const input: CreateEolReportInput = { sbom: { bomFormat: 'CycloneDX', components: [], specVersion: '1.4', version: 1 }, }; - await assert.rejects(() => submitScan(input), /Failed to create EOL report/); + await expect(submitScan(input)).rejects.toThrow(/Failed to create EOL report/); }); }); diff --git a/test/api/strip-typename.test.ts b/test/api/strip-typename.test.ts index 3ee1344e..d1bb7f32 100644 --- a/test/api/strip-typename.test.ts +++ b/test/api/strip-typename.test.ts @@ -1,5 +1,3 @@ -import assert from 'node:assert'; -import { describe, it } from 'node:test'; import { stripTypename } from '../../src/utils/strip-typename'; describe('stripTypename', () => { @@ -33,20 +31,20 @@ describe('stripTypename', () => { }; const output = stripTypename(input); - assert.deepStrictEqual(output, expectedOutput); + expect(output).toEqual(expectedOutput); }); it('handles null and primitive values correctly', () => { - assert.strictEqual(stripTypename(null), null); - assert.strictEqual(stripTypename(42), 42); - assert.strictEqual(stripTypename('string'), 'string'); - assert.strictEqual(stripTypename(true), true); + expect(stripTypename(null)).toBeNull(); + expect(stripTypename(42)).toBe(42); + expect(stripTypename('string')).toBe('string'); + expect(stripTypename(true)).toBe(true); }); it('handles arrays of primitives correctly', () => { const input = [1, 2, 3, { __typename: 'Item', id: 'a' }]; const expectedOutput = [1, 2, 3, { id: 'a' }]; const output = stripTypename(input); - assert.deepStrictEqual(output, expectedOutput); + expect(output).toEqual(expectedOutput); }); }); diff --git a/test/commands/auth/login.test.ts b/test/commands/auth/login.test.ts new file mode 100644 index 00000000..8ea5b003 --- /dev/null +++ b/test/commands/auth/login.test.ts @@ -0,0 +1,370 @@ +import type { Config } from '@oclif/core'; +import { type Mock, type MockedFunction, vi } from 'vitest'; +import AuthLogin from '../../../src/commands/auth/login.ts'; +import { persistTokenResponse } from '../../../src/service/auth.svc.ts'; +import { openInBrowser } from '../../../src/utils/open-in-browser.ts'; + +type ServerRequest = { url?: string }; +type ServerResponse = { writeHead: Mock; end: Mock }; +type ServerHandler = (req: ServerRequest, res: ServerResponse) => void; + +interface ServerStub { + handler: ServerHandler; + listen: MockedFunction<(port: number, cb?: () => void) => ServerStub>; + close: MockedFunction<(cb?: (err?: Error) => void) => ServerStub>; + on: MockedFunction<(event: string, cb: (err: Error) => void) => ServerStub>; + triggerRequest: (url?: string) => { writeHead: Mock; end: Mock }; + emitError: (error: Error) => void; +} + +const serverInstances: ServerStub[] = []; + +const createServerStub = (handler: ServerHandler): ServerStub => { + let errorListener: ((error: Error) => void) | undefined; + const stub: ServerStub = { + handler, + listen: vi.fn((_port: number, cb?: () => void) => { + if (cb) { + setImmediate(cb); + } + + return stub; + }), + close: vi.fn((cb?: (err?: Error) => void) => { + cb?.(); + return stub; + }), + on: vi.fn((event: string, cb: (err: Error) => void) => { + if (event === 'error') { + errorListener = cb; + } + + return stub; + }), + triggerRequest: (url?: string) => { + const res = { + writeHead: vi.fn(), + end: vi.fn(), + }; + + stub.handler({ url } as ServerRequest, res as ServerResponse); + return res; + }, + emitError: (error: Error) => { + errorListener?.(error); + }, + }; + + return stub; +}; + +vi.mock('http', () => ({ + __esModule: true, + default: { + createServer: vi.fn((handler: ServerHandler) => { + const server = createServerStub(handler); + serverInstances.push(server); + return server; + }), + }, +})); + +vi.mock('../../../src/utils/open-in-browser.ts', () => ({ + __esModule: true, + openInBrowser: vi.fn(), +})); + +const questionMock = vi.fn<[string, (answer: string) => void], void>(); +const closeMock = vi.fn<[], void>(); + +vi.mock('node:readline', () => ({ + __esModule: true, + createInterface: vi.fn(() => ({ + question: questionMock, + close: closeMock, + })), +})); + +vi.mock('../../../src/service/auth.svc.ts', () => ({ + __esModule: true, + persistTokenResponse: vi.fn().mockResolvedValue(undefined), +})); + +const openMock = vi.mocked(openInBrowser) as MockedFunction; +const persistTokenResponseMock = vi.mocked(persistTokenResponse); + +const flushAsync = () => new Promise((resolve) => setImmediate(resolve)); + +const getLatestServer = () => { + const server = serverInstances.at(-1); + if (!server) { + throw new Error('HTTP server stub was not initialized'); + } + + return server; +}; + +const sendCallbackThroughStub = (params: Record) => { + const search = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value !== undefined) { + search.append(key, value); + } + } + + const query = search.toString(); + const path = `/oauth2/callback${query ? `?${query}` : ''}`; + return getLatestServer().triggerRequest(path); +}; + +const createCommand = (port: number) => { + process.env.OAUTH_CALLBACK_PORT = `${port}`; + const mockConfig = {} as Config; + return new AuthLogin([], mockConfig); +}; + +describe('AuthLogin', () => { + beforeEach(() => { + questionMock.mockImplementation((_q, cb) => cb('')); + closeMock.mockClear(); + openMock.mockResolvedValue(undefined); + }); + + afterEach(() => { + vi.clearAllMocks(); + delete process.env.OAUTH_CALLBACK_PORT; + serverInstances.length = 0; + persistTokenResponseMock.mockClear(); + }); + + describe('startServerAndAwaitCode', () => { + const authUrl = 'https://login.example/auth'; + const basePort = 4900; + + it('resolves with the authorization code when the callback is valid', async () => { + const command = createCommand(basePort); + const state = 'expected-state'; + const pendingCode = ( + command as unknown as { startServerAndAwaitCode: (url: string, state: string) => Promise } + ).startServerAndAwaitCode(authUrl, state); + const server = getLatestServer(); + + await flushAsync(); + sendCallbackThroughStub({ code: 'test-code', state }); + + await expect(pendingCode).resolves.toBe('test-code'); + expect(questionMock).toHaveBeenCalledWith(expect.stringContaining(authUrl), expect.any(Function)); + expect(closeMock).toHaveBeenCalledTimes(1); + expect(openMock).toHaveBeenCalledWith(authUrl); + expect(server.close).toHaveBeenCalledTimes(1); + }); + + it('rejects when the callback is missing the state parameter', async () => { + const command = createCommand(basePort + 1); + const pendingCode = ( + command as unknown as { startServerAndAwaitCode: (url: string, state: string) => Promise } + ).startServerAndAwaitCode(authUrl, 'expected-state'); + const server = getLatestServer(); + + await flushAsync(); + sendCallbackThroughStub({ code: 'test-code', state: undefined }); + + await expect(pendingCode).rejects.toThrow('Missing state parameter in callback'); + expect(server.close).toHaveBeenCalledTimes(1); + }); + + it('rejects when the callback state does not match', async () => { + const command = createCommand(basePort + 2); + const pendingCode = ( + command as unknown as { startServerAndAwaitCode: (url: string, state: string) => Promise } + ).startServerAndAwaitCode(authUrl, 'expected-state'); + const server = getLatestServer(); + + await flushAsync(); + sendCallbackThroughStub({ code: 'test-code', state: 'different' }); + + await expect(pendingCode).rejects.toThrow('State verification failed'); + expect(server.close).toHaveBeenCalledTimes(1); + }); + + it('rejects when the callback omits the authorization code', async () => { + const command = createCommand(basePort + 3); + const pendingCode = ( + command as unknown as { startServerAndAwaitCode: (url: string, state: string) => Promise } + ).startServerAndAwaitCode(authUrl, 'expected-state'); + const server = getLatestServer(); + + await flushAsync(); + sendCallbackThroughStub({ state: 'expected-state' }); + + await expect(pendingCode).rejects.toThrow('No code returned from Keycloak'); + expect(server.close).toHaveBeenCalledTimes(1); + }); + + it('rejects when the callback URL is invalid', async () => { + const command = createCommand(basePort + 4); + const pendingCode = ( + command as unknown as { startServerAndAwaitCode: (url: string, state: string) => Promise } + ).startServerAndAwaitCode(authUrl, 'expected-state'); + const server = getLatestServer(); + + await flushAsync(); + const response = server.triggerRequest('http://%'); + + expect(response.writeHead).toHaveBeenCalledWith(400, { 'Content-Type': 'text/plain' }); + expect(response.end).toHaveBeenCalledWith('Invalid callback URL'); + await expect(pendingCode).rejects.toThrow('Invalid callback URL'); + expect(server.close).toHaveBeenCalledTimes(1); + }); + + it('returns a 400 response when the incoming request is missing a URL', async () => { + const command = createCommand(basePort + 4); + const pendingCode = ( + command as unknown as { startServerAndAwaitCode: (url: string, state: string) => Promise } + ).startServerAndAwaitCode(authUrl, 'expected-state'); + const server = getLatestServer(); + + await flushAsync(); + const response = server.triggerRequest(undefined); + + expect(response.writeHead).toHaveBeenCalledWith(400); + expect(response.end).toHaveBeenCalledWith('Invalid request'); + + const shutdownError = new Error('test shutdown'); + server.emitError(shutdownError); + await expect(pendingCode).rejects.toBe(shutdownError); + expect(server.close).toHaveBeenCalledTimes(1); + }); + + it('responds with not found for unrelated paths', async () => { + const command = createCommand(basePort + 5); + const pendingCode = ( + command as unknown as { startServerAndAwaitCode: (url: string, state: string) => Promise } + ).startServerAndAwaitCode(authUrl, 'expected-state'); + const server = getLatestServer(); + + await flushAsync(); + const response = server.triggerRequest('/not-supported'); + + expect(response.writeHead).toHaveBeenCalledWith(404); + expect(response.end).toHaveBeenCalledWith(); + + const shutdownError = new Error('not found handled'); + server.emitError(shutdownError); + await expect(pendingCode).rejects.toBe(shutdownError); + expect(server.close).toHaveBeenCalledTimes(1); + }); + + it('rejects when the local HTTP server emits an error', async () => { + const command = createCommand(basePort + 6); + const pendingCode = ( + command as unknown as { startServerAndAwaitCode: (url: string, state: string) => Promise } + ).startServerAndAwaitCode(authUrl, 'expected-state'); + const server = getLatestServer(); + + await flushAsync(); + const error = new Error('listener failed'); + server.emitError(error); + + await expect(pendingCode).rejects.toBe(error); + expect(server.close).toHaveBeenCalledTimes(1); + }); + + it('warns and allows manual navigation when browser launch fails', async () => { + openMock.mockRejectedValueOnce(new Error('browser unavailable')); + const command = createCommand(basePort + 7); + const warnSpy = vi + .spyOn(command as unknown as { warn: (...args: unknown[]) => unknown }, 'warn') + .mockImplementation(() => {}); + const state = 'expected-state'; + + try { + const pendingCode = ( + command as unknown as { startServerAndAwaitCode: (url: string, state: string) => Promise } + ).startServerAndAwaitCode(authUrl, state); + const server = getLatestServer(); + + await flushAsync(); + + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to open browser automatically')); + + sendCallbackThroughStub({ code: 'manual-code', state }); + await expect(pendingCode).resolves.toBe('manual-code'); + expect(server.close).toHaveBeenCalledTimes(1); + } finally { + warnSpy.mockRestore(); + } + }); + }); + + describe('exchangeCodeForToken', () => { + it('posts the authorization code and returns the parsed token response', async () => { + const command = createCommand(5000); + const mockResponse = { access_token: 'abc123' }; + const jsonMock = vi.fn().mockResolvedValue(mockResponse); + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ + ok: true, + json: jsonMock, + } as unknown as Response); + + try { + const token = await ( + command as unknown as { exchangeCodeForToken: (code: string, verifier: string) => Promise } + ).exchangeCodeForToken('code-123', 'verifier-456'); + + expect(fetchSpy).toHaveBeenCalledTimes(1); + const [url, options] = fetchSpy.mock.calls[0]; + expect(url).toMatch(/\/token$/); + expect(options).toMatchObject({ + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }); + expect(options?.body).toContain('code=code-123'); + expect(options?.body).toContain('code_verifier=verifier-456'); + expect(options?.body).toContain('grant_type=authorization_code'); + expect(token).toEqual(mockResponse); + } finally { + fetchSpy.mockRestore(); + } + }); + + it('throws an error that includes the response body when the exchange fails', async () => { + const command = createCommand(5001); + const textMock = vi.fn().mockResolvedValue('error-details'); + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ + ok: false, + status: 500, + statusText: 'Server Error', + text: textMock, + } as unknown as Response); + + try { + await expect( + ( + command as unknown as { exchangeCodeForToken: (code: string, verifier: string) => Promise } + ).exchangeCodeForToken('code-123', 'verifier-456'), + ).rejects.toThrow('Token exchange failed: 500 Server Error'); + expect(textMock).toHaveBeenCalled(); + } finally { + fetchSpy.mockRestore(); + } + }); + }); + + describe('run', () => { + it('stores tokens after successful authentication', async () => { + const command = createCommand(6000); + const tokenResponse = { access_token: 'access', refresh_token: 'refresh' }; + const commandWithInternals = command as unknown as { + startServerAndAwaitCode: (...args: unknown[]) => Promise; + exchangeCodeForToken: (...args: unknown[]) => Promise; + }; + vi.spyOn(commandWithInternals, 'startServerAndAwaitCode').mockResolvedValue('code-123'); + vi.spyOn(commandWithInternals, 'exchangeCodeForToken').mockResolvedValue(tokenResponse); + + await command.run(); + + expect(persistTokenResponseMock).toHaveBeenCalledWith(tokenResponse); + }); + }); +}); diff --git a/test/commands/auth/logout.test.ts b/test/commands/auth/logout.test.ts new file mode 100644 index 00000000..15a8902c --- /dev/null +++ b/test/commands/auth/logout.test.ts @@ -0,0 +1,57 @@ +import { type Mock, vi } from 'vitest'; + +vi.mock('../../../src/service/auth-token.svc.ts', () => ({ + __esModule: true, + getStoredTokens: vi.fn(), + clearStoredTokens: vi.fn(), +})); + +vi.mock('../../../src/service/auth-refresh.svc.ts', () => ({ + __esModule: true, + logoutFromProvider: vi.fn().mockResolvedValue(undefined), +})); + +import AuthLogout from '../../../src/commands/auth/logout.ts'; +import { logoutFromProvider } from '../../../src/service/auth-refresh.svc.ts'; +import { clearStoredTokens, getStoredTokens } from '../../../src/service/auth-token.svc.ts'; + +describe('AuthLogout command', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('logs when there are no stored tokens', async () => { + (getStoredTokens as Mock).mockResolvedValue(undefined); + const command = new AuthLogout([], {} as Record); + const logSpy = vi.spyOn(command, 'log').mockImplementation(() => {}); + + await command.run(); + + expect(logSpy).toHaveBeenCalledWith('No stored authentication tokens found.'); + expect(clearStoredTokens).not.toHaveBeenCalled(); + }); + + it('revokes tokens and clears local storage', async () => { + (getStoredTokens as Mock).mockResolvedValue({ refreshToken: 'refresh', accessToken: 'access' }); + const command = new AuthLogout([], {} as Record); + const logSpy = vi.spyOn(command, 'log').mockImplementation(() => {}); + + await command.run(); + + expect(logoutFromProvider).toHaveBeenCalledWith('refresh'); + expect(clearStoredTokens).toHaveBeenCalled(); + expect(logSpy).toHaveBeenCalledWith('Local authentication tokens removed from your system.'); + }); + + it('warns when remote logout fails but still clears tokens', async () => { + (getStoredTokens as Mock).mockResolvedValue({ refreshToken: 'refresh' }); + (logoutFromProvider as Mock).mockRejectedValueOnce(new Error('network fail')); + const command = new AuthLogout([], {} as Record); + const warnSpy = vi.spyOn(command, 'warn').mockImplementation((msg) => msg); + + await command.run(); + + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('network fail')); + expect(clearStoredTokens).toHaveBeenCalled(); + }); +}); diff --git a/test/commands/scan/eol.count.test.ts b/test/commands/scan/eol.count.test.ts index 70ee530e..5f2dcf7a 100644 --- a/test/commands/scan/eol.count.test.ts +++ b/test/commands/scan/eol.count.test.ts @@ -1,5 +1,3 @@ -import assert from 'node:assert'; -import { describe, it } from 'node:test'; import type { EolReport } from '@herodevs/eol-shared'; import { countComponentsByStatus } from '../../../src/service/display.svc.ts'; @@ -22,7 +20,7 @@ describe('countComponentsByStatus', () => { const counts = countComponentsByStatus(report); const statusSum = counts.EOL + counts.EOL_UPCOMING + counts.OK + counts.UNKNOWN; - assert.strictEqual(statusSum, report.components.length); - assert.strictEqual(counts.NES_AVAILABLE, 1); + expect(statusSum).toBe(report.components.length); + expect(counts.NES_AVAILABLE).toBe(1); }); }); diff --git a/test/hooks/npm-update-notifier.test.ts b/test/hooks/npm-update-notifier.test.ts index c26fa044..1ec9de93 100644 --- a/test/hooks/npm-update-notifier.test.ts +++ b/test/hooks/npm-update-notifier.test.ts @@ -1,26 +1,24 @@ -import assert from 'node:assert'; -import { describe, it } from 'node:test'; import { getDistTag, handleUpdate } from '../../src/hooks/init/00_npm-update-notifier'; describe('getDistTag', () => { it('should return beta for beta versions', () => { - assert.strictEqual(getDistTag('1.0.0-beta.1'), 'beta'); - assert.strictEqual(getDistTag('2.3.4-beta.42'), 'beta'); + expect(getDistTag('1.0.0-beta.1')).toBe('beta'); + expect(getDistTag('2.3.4-beta.42')).toBe('beta'); }); it('should return alpha for alpha versions', () => { - assert.strictEqual(getDistTag('1.0.0-alpha.1'), 'alpha'); - assert.strictEqual(getDistTag('2.3.4-alpha.42'), 'alpha'); + expect(getDistTag('1.0.0-alpha.1')).toBe('alpha'); + expect(getDistTag('2.3.4-alpha.42')).toBe('alpha'); }); it('should return next for next versions', () => { - assert.strictEqual(getDistTag('1.0.0-next.1'), 'next'); - assert.strictEqual(getDistTag('2.3.4-next.42'), 'next'); + expect(getDistTag('1.0.0-next.1')).toBe('next'); + expect(getDistTag('2.3.4-next.42')).toBe('next'); }); it('should return latest for stable versions', () => { - assert.strictEqual(getDistTag('1.0.0'), 'latest'); - assert.strictEqual(getDistTag('2.3.4'), 'latest'); + expect(getDistTag('1.0.0')).toBe('latest'); + expect(getDistTag('2.3.4')).toBe('latest'); }); }); @@ -37,7 +35,7 @@ describe('handleUpdate', () => { '0.3.1', ); - assert.deepStrictEqual(result, { + expect(result).toEqual({ message: 'Update available! v0.3.1 → v0.3.2\nThis update may contain breaking changes.', defer: false, }); @@ -54,7 +52,7 @@ describe('handleUpdate', () => { '1.4.0-beta.1', ); - assert.deepStrictEqual(result, { + expect(result).toEqual({ message: 'Update available! v1.4.0-beta.1 → v1.4.0-beta.2\nThis update may contain breaking changes.', defer: false, }); @@ -71,7 +69,7 @@ describe('handleUpdate', () => { '1.4.0', ); - assert.deepStrictEqual(result, { + expect(result).toEqual({ message: 'Update available! v1.4.0 → v1.4.0-alpha.1\nThis update may contain breaking changes.', defer: false, }); @@ -88,7 +86,7 @@ describe('handleUpdate', () => { '1.4.0', ); - assert.deepStrictEqual(result, { + expect(result).toEqual({ message: 'Update available! v1.4.0 → v1.4.0-next.1\nThis update may contain breaking changes.', defer: false, }); @@ -105,7 +103,7 @@ describe('handleUpdate', () => { '1.0.0', ); - assert.deepStrictEqual(result, { + expect(result).toEqual({ message: 'Update available! v1.0.0 → v2.0.0\nThis update may contain breaking changes.', defer: false, }); @@ -122,7 +120,7 @@ describe('handleUpdate', () => { '1.3.0', ); - assert.deepStrictEqual(result, { + expect(result).toEqual({ message: 'Update available! v1.3.0 → v1.4.0-beta.1\nThis update may contain breaking changes.', defer: false, }); @@ -139,7 +137,7 @@ describe('handleUpdate', () => { '1.4.0-beta.1', ); - assert.deepStrictEqual(result, { + expect(result).toEqual({ message: 'Update available! v1.4.0-beta.1 → v1.4.0\nThis update may contain breaking changes.', defer: false, }); @@ -156,7 +154,7 @@ describe('handleUpdate', () => { '1.3.0-beta.1', ); - assert.deepStrictEqual(result, { + expect(result).toEqual({ message: 'Update available! v1.3.0-beta.1 → v1.4.0-beta.2\nThis update may contain breaking changes.', defer: false, }); @@ -175,7 +173,7 @@ describe('handleUpdate', () => { '1.0.0', ); - assert.deepStrictEqual(result, { + expect(result).toEqual({ message: 'Update available! v1.0.0 → v1.1.0', defer: false, }); @@ -192,7 +190,7 @@ describe('handleUpdate', () => { '1.0.0', ); - assert.deepStrictEqual(result, { + expect(result).toEqual({ message: 'Update available! v1.0.0 → v1.0.1', defer: false, }); @@ -209,7 +207,7 @@ describe('handleUpdate', () => { '1.3.0', ); - assert.deepStrictEqual(result, { + expect(result).toEqual({ message: 'Update available! v1.3.0 → v1.4.0', defer: false, }); diff --git a/test/service/analytics.svc.test.ts b/test/service/analytics.svc.test.ts index b074c302..3c1021c3 100644 --- a/test/service/analytics.svc.test.ts +++ b/test/service/analytics.svc.test.ts @@ -1,6 +1,5 @@ -import assert from 'node:assert'; -import { afterEach, beforeEach, describe, it, type TestContext } from 'node:test'; import sinon from 'sinon'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; describe('analytics.svc', () => { const mockAmplitude = { @@ -14,14 +13,27 @@ describe('analytics.svc', () => { const mockNodeMachineId = { machineIdSync: sinon.stub().returns('test-machine-id') }; let originalEnv: typeof process.env; - function setupModule(t: TestContext) { - t.mock.module('@amplitude/analytics-node', { namedExports: mockAmplitude }); - t.mock.module('node-machine-id', { defaultExport: mockNodeMachineId }); - t.mock.module('../../src/config/constants.ts', { - namedExports: { config: { analyticsUrl: 'https://test-analytics.com' } }, - }); - - return import(import.meta.resolve(`../../src/service/analytics.svc.ts?${Math.random().toFixed(4)}`)); + async function setupModule() { + vi.resetModules(); + vi.doMock('@amplitude/analytics-node', () => ({ + __esModule: true, + init: mockAmplitude.init, + setOptOut: mockAmplitude.setOptOut, + identify: mockAmplitude.identify, + track: mockAmplitude.track, + Identify: mockAmplitude.Identify, + Types: mockAmplitude.Types, + })); + vi.doMock('node-machine-id', () => ({ + __esModule: true, + default: mockNodeMachineId, + })); + vi.doMock('../../src/config/constants.ts', () => ({ + __esModule: true, + config: { analyticsUrl: 'https://test-analytics.com' }, + })); + + return import('../../src/service/analytics.svc.ts'); } beforeEach(() => { @@ -38,14 +50,14 @@ describe('analytics.svc', () => { }); describe('initializeAnalytics', () => { - it('should call amplitude init with correct parameters', async (t) => { - const mod = await setupModule(t); + it('should call amplitude init with correct parameters', async () => { + const mod = await setupModule(); mod.initializeAnalytics(); - assert(mockAmplitude.init.calledOnce); + expect(mockAmplitude.init.calledOnce).toBe(true); const initCall = mockAmplitude.init.getCall(0); - assert.strictEqual(initCall.args[0], '0'); - assert.deepStrictEqual(initCall.args[1], { + expect(initCall.args[0]).toBe('0'); + expect(initCall.args[1]).toEqual({ flushQueueSize: 2, flushIntervalMillis: 250, logLevel: 0, @@ -53,146 +65,146 @@ describe('analytics.svc', () => { }); }); - it('should call setOptOut with true when TRACKING_OPT_OUT is true', async (t) => { + it('should call setOptOut with true when TRACKING_OPT_OUT is true', async () => { process.env.TRACKING_OPT_OUT = 'true'; - const mod = await setupModule(t); + const mod = await setupModule(); mod.initializeAnalytics(); - assert(mockAmplitude.setOptOut.calledOnce); - assert.strictEqual(mockAmplitude.setOptOut.getCall(0).args[0], true); + expect(mockAmplitude.setOptOut.calledOnce).toBe(true); + expect(mockAmplitude.setOptOut.getCall(0).args[0]).toBe(true); }); - it('should call setOptOut with false when TRACKING_OPT_OUT is not true', async (t) => { + it('should call setOptOut with false when TRACKING_OPT_OUT is not true', async () => { process.env.TRACKING_OPT_OUT = 'false'; - const mod = await setupModule(t); + const mod = await setupModule(); mod.initializeAnalytics(); - assert(mockAmplitude.setOptOut.calledOnce); - assert.strictEqual(mockAmplitude.setOptOut.getCall(0).args[0], false); + expect(mockAmplitude.setOptOut.calledOnce).toBe(true); + expect(mockAmplitude.setOptOut.getCall(0).args[0]).toBe(false); }); - it('should call identify with correct user properties', async (t) => { - const mod = await setupModule(t); + it('should call identify with correct user properties', async () => { + const mod = await setupModule(); mod.initializeAnalytics(); - assert(mockAmplitude.identify.calledOnce); + expect(mockAmplitude.identify.calledOnce).toBe(true); const identifyCall = mockAmplitude.identify.getCall(0); const userProperties = identifyCall.args[1]; - assert.strictEqual(userProperties.device_id, 'test-machine-id'); - assert(typeof userProperties.session_id === 'number'); - assert(typeof userProperties.platform === 'string'); - assert(typeof userProperties.os_name === 'string'); - assert(typeof userProperties.os_version === 'string'); - assert(typeof userProperties.app_version === 'string'); + expect(userProperties.device_id).toBe('test-machine-id'); + expect(typeof userProperties.session_id).toBe('number'); + expect(typeof userProperties.platform).toBe('string'); + expect(typeof userProperties.os_name).toBe('string'); + expect(typeof userProperties.os_version).toBe('string'); + expect(typeof userProperties.app_version).toBe('string'); }); - it('should handle case when npm_package_version is undefined', async (t) => { + it('should handle case when npm_package_version is undefined', async () => { process.env.npm_package_version = undefined; - const mod = await setupModule(t); + const mod = await setupModule(); mod.initializeAnalytics(); const identifyCall = mockAmplitude.identify.getCall(0); const userProperties = identifyCall.args[1]; - assert.strictEqual(userProperties.app_version, 'unknown'); + expect(userProperties.app_version).toBe('unknown'); }); }); describe('track', () => { - it('should call amplitude track with event name and no properties when getProperties is undefined', async (t) => { - const mod = await setupModule(t); + it('should call amplitude track with event name and no properties when getProperties is undefined', async () => { + const mod = await setupModule(); mod.track('test-event'); - assert(mockAmplitude.track.calledOnce); + expect(mockAmplitude.track.calledOnce).toBe(true); const trackCall = mockAmplitude.track.getCall(0); - assert.strictEqual(trackCall.args[0], 'test-event'); - assert.strictEqual(trackCall.args[1], undefined); - assert(typeof trackCall.args[2].device_id === 'string'); - assert(typeof trackCall.args[2].session_id === 'number'); + expect(trackCall.args[0]).toBe('test-event'); + expect(trackCall.args[1]).toBeUndefined(); + expect(typeof trackCall.args[2].device_id).toBe('string'); + expect(typeof trackCall.args[2].session_id).toBe('number'); }); - it('should call amplitude track with event name and properties when getProperties returns data', async (t) => { - const mod = await setupModule(t); + it('should call amplitude track with event name and properties when getProperties returns data', async () => { + const mod = await setupModule(); const testProperties = { eol_true_count: 5 }; const getProperties = sinon.stub().returns(testProperties); mod.track('test-event', getProperties); - assert(mockAmplitude.track.calledOnce); + expect(mockAmplitude.track.calledOnce).toBe(true); const trackCall = mockAmplitude.track.getCall(0); - assert.strictEqual(trackCall.args[0], 'test-event'); - assert.deepStrictEqual(trackCall.args[1], testProperties); - assert(typeof trackCall.args[2].device_id === 'string'); - assert(typeof trackCall.args[2].session_id === 'number'); + expect(trackCall.args[0]).toBe('test-event'); + expect(trackCall.args[1]).toEqual(testProperties); + expect(typeof trackCall.args[2].device_id).toBe('string'); + expect(typeof trackCall.args[2].session_id).toBe('number'); }); - it('should merge properties into analyticsContext when getProperties returns data', async (t) => { - const mod = await setupModule(t); + it('should merge properties into analyticsContext when getProperties returns data', async () => { + const mod = await setupModule(); const firstProperties = { eol_true_count: 3 }; const secondProperties = { eol_unknown_count: 2 }; mod.track('test-event-1', () => firstProperties); const getSecondProperties = sinon.stub().callsFake((context) => { - assert.strictEqual(context.eol_true_count, 3); + expect(context.eol_true_count).toBe(3); return secondProperties; }); mod.track('test-event-2', getSecondProperties); - assert(getSecondProperties.calledOnce); - assert(mockAmplitude.track.calledTwice); + expect(getSecondProperties.calledOnce).toBe(true); + expect(mockAmplitude.track.calledTwice).toBe(true); }); - it('should preserve existing analyticsContext when getProperties returns undefined', async (t) => { - const mod = await setupModule(t); + it('should preserve existing analyticsContext when getProperties returns undefined', async () => { + const mod = await setupModule(); const initialProperties = { eol_true_count: 5 }; mod.track('test-event-1', () => initialProperties); const getUndefinedProperties = sinon.stub().callsFake((context) => { - assert.strictEqual(context.eol_true_count, 5); + expect(context.eol_true_count).toBe(5); return undefined; }); mod.track('test-event-2', getUndefinedProperties); - assert(getUndefinedProperties.calledOnce); - assert(mockAmplitude.track.calledTwice); + expect(getUndefinedProperties.calledOnce).toBe(true); + expect(mockAmplitude.track.calledTwice).toBe(true); // Second track call should have undefined properties const secondTrackCall = mockAmplitude.track.getCall(1); - assert.strictEqual(secondTrackCall.args[1], undefined); + expect(secondTrackCall.args[1]).toBeUndefined(); }); - it('should pass correct device_id and session_id to amplitude track', async (t) => { - const mod = await setupModule(t); + it('should pass correct device_id and session_id to amplitude track', async () => { + const mod = await setupModule(); mod.track('test-event'); - assert(mockAmplitude.track.calledOnce); + expect(mockAmplitude.track.calledOnce).toBe(true); const trackCall = mockAmplitude.track.getCall(0); const eventOptions = trackCall.args[2]; - assert.strictEqual(eventOptions.device_id, 'test-machine-id'); - assert(typeof eventOptions.session_id === 'number'); - assert(eventOptions.session_id > 0); + expect(eventOptions.device_id).toBe('test-machine-id'); + expect(typeof eventOptions.session_id).toBe('number'); + expect(eventOptions.session_id).toBeGreaterThan(0); }); }); describe('Module Initialization', () => { - it('should initialize device_id using NodeMachineId.machineIdSync', async (t) => { - await setupModule(t); + it('should initialize device_id using NodeMachineId.machineIdSync', async () => { + await setupModule(); - assert(mockNodeMachineId.machineIdSync.calledOnce); - assert.strictEqual(mockNodeMachineId.machineIdSync.getCall(0).args[0], true); + expect(mockNodeMachineId.machineIdSync.calledOnce).toBe(true); + expect(mockNodeMachineId.machineIdSync.getCall(0).args[0]).toBe(true); }); - it('should initialize started_at as a Date object', async (t) => { + it('should initialize started_at as a Date object', async () => { const beforeImport = Date.now(); - const mod = await setupModule(t); + const mod = await setupModule(); const afterImport = Date.now(); mod.initializeAnalytics(); @@ -200,78 +212,78 @@ describe('analytics.svc', () => { const identifyCall = mockAmplitude.identify.getCall(0); const sessionId = identifyCall.args[1].session_id; - assert(sessionId >= beforeImport); - assert(sessionId <= afterImport); + expect(sessionId).toBeGreaterThanOrEqual(beforeImport); + expect(sessionId).toBeLessThanOrEqual(afterImport); }); - it('should initialize session_id as timestamp from started_at', async (t) => { - const mod = await setupModule(t); + it('should initialize session_id as timestamp from started_at', async () => { + const mod = await setupModule(); mod.initializeAnalytics(); const identifyCall = mockAmplitude.identify.getCall(0); const sessionId = identifyCall.args[1].session_id; // Session ID should be a valid timestamp - assert(typeof sessionId === 'number'); - assert(sessionId > 0); - assert(sessionId <= Date.now()); + expect(typeof sessionId).toBe('number'); + expect(sessionId).toBeGreaterThan(0); + expect(sessionId).toBeLessThanOrEqual(Date.now()); }); - it('should initialize defaultAnalyticsContext with correct locale', async (t) => { - const mod = await setupModule(t); + it('should initialize defaultAnalyticsContext with correct locale', async () => { + const mod = await setupModule(); const getProperties = sinon.stub().callsFake((context) => { - assert(typeof context.locale === 'string'); - assert(context.locale.length > 0); + expect(typeof context.locale).toBe('string'); + expect(context.locale.length).toBeGreaterThan(0); return {}; }); mod.track('test-event', getProperties); - assert(getProperties.calledOnce); + expect(getProperties.calledOnce).toBe(true); }); - it('should initialize defaultAnalyticsContext with correct OS platform', async (t) => { - const mod = await setupModule(t); + it('should initialize defaultAnalyticsContext with correct OS platform', async () => { + const mod = await setupModule(); const getProperties = sinon.stub().callsFake((context) => { - assert(typeof context.os_platform === 'string'); - assert( + expect(typeof context.os_platform).toBe('string'); + expect( ['darwin', 'linux', 'win32', 'freebsd', 'openbsd', 'android', 'aix', 'sunos'].includes(context.os_platform), - ); + ).toBe(true); return {}; }); mod.track('test-event', getProperties); - assert(getProperties.calledOnce); + expect(getProperties.calledOnce).toBe(true); }); - it('should initialize defaultAnalyticsContext with CLI version from npm_package_version or unknown', async (t) => { - const mod = await setupModule(t); + it('should initialize defaultAnalyticsContext with CLI version from npm_package_version or unknown', async () => { + const mod = await setupModule(); const getProperties = sinon.stub().callsFake((context) => { - assert(typeof context.cli_version === 'string'); - assert(context.cli_version.length > 0); + expect(typeof context.cli_version).toBe('string'); + expect(context.cli_version.length).toBeGreaterThan(0); return {}; }); mod.track('test-event', getProperties); - assert(getProperties.calledOnce); + expect(getProperties.calledOnce).toBe(true); }); - it('should initialize analyticsContext equal to defaultAnalyticsContext', async (t) => { - const mod = await setupModule(t); + it('should initialize analyticsContext equal to defaultAnalyticsContext', async () => { + const mod = await setupModule(); const getProperties = sinon.stub().callsFake((context) => { - assert(typeof context.locale === 'string'); - assert(typeof context.os_platform === 'string'); - assert(typeof context.os_release === 'string'); - assert(typeof context.cli_version === 'string'); - assert(context.started_at instanceof Date); + expect(typeof context.locale).toBe('string'); + expect(typeof context.os_platform).toBe('string'); + expect(typeof context.os_release).toBe('string'); + expect(typeof context.cli_version).toBe('string'); + expect(context.started_at instanceof Date).toBe(true); return {}; }); mod.track('test-event', getProperties); - assert(getProperties.calledOnce); + expect(getProperties.calledOnce).toBe(true); }); }); }); diff --git a/test/service/auth-token.svc.test.ts b/test/service/auth-token.svc.test.ts new file mode 100644 index 00000000..5635f74b --- /dev/null +++ b/test/service/auth-token.svc.test.ts @@ -0,0 +1,97 @@ +import { vi } from 'vitest'; + +vi.mock('@napi-rs/keyring', () => { + const store = new Map(); + const setPasswordMock = vi.fn(async (service: string, account: string, password: string) => { + store.set(`${service}:${account}`, password); + }); + const getPasswordMock = vi.fn(async (service: string, account: string) => store.get(`${service}:${account}`)); + const deletePasswordMock = vi.fn(async (service: string, account: string) => { + store.delete(`${service}:${account}`); + return true; + }); + + class AsyncEntry { + service: string; + username: string; + constructor(service: string, username: string) { + this.service = service; + this.username = username; + } + + async setPassword(password: string) { + return setPasswordMock(this.service, this.username, password); + } + + async getPassword() { + return getPasswordMock(this.service, this.username); + } + + async deletePassword() { + return deletePasswordMock(this.service, this.username); + } + } + + return { + __esModule: true, + AsyncEntry, + __store: store, + __mocks: { setPasswordMock, getPasswordMock, deletePasswordMock }, + }; +}); + +// @ts-expect-error - __mocks is exposed only via the test double above +import { __mocks as keyringMocks } from '@napi-rs/keyring'; +import { + clearStoredTokens, + getStoredTokens, + isAccessTokenExpired, + saveTokens, +} from '../../src/service/auth-token.svc.ts'; +import { createTokenWithExp } from '../utils/token.ts'; + +describe('auth-token.svc', () => { + beforeEach(async () => { + await clearStoredTokens(); + }); + + it('saves and retrieves access and refresh tokens', async () => { + await saveTokens({ accessToken: 'access-1', refreshToken: 'refresh-1' }); + + const tokens = await getStoredTokens(); + expect(tokens?.accessToken).toBe('access-1'); + expect(tokens?.refreshToken).toBe('refresh-1'); + }); + + it('clears stored tokens', async () => { + await saveTokens({ accessToken: 'access-2', refreshToken: 'refresh-2' }); + await clearStoredTokens(); + + const tokens = await getStoredTokens(); + expect(tokens).toBeUndefined(); + }); + + it('returns undefined when nothing is stored', async () => { + const tokens = await getStoredTokens(); + expect(tokens).toBeUndefined(); + }); + + it('removes stored refresh token when not provided', async () => { + await saveTokens({ accessToken: 'access-3', refreshToken: 'refresh-3' }); + await saveTokens({ accessToken: 'access-4' }); + + const tokens = await getStoredTokens(); + expect(tokens?.accessToken).toBe('access-4'); + expect(tokens?.refreshToken).toBeUndefined(); + expect(keyringMocks.deletePasswordMock).toHaveBeenCalled(); + }); + + it('computes access token expiry from JWT payload', () => { + const futureToken = createTokenWithExp(120); + const pastToken = createTokenWithExp(-120); + + expect(isAccessTokenExpired(futureToken)).toBe(false); + expect(isAccessTokenExpired(pastToken)).toBe(true); + expect(isAccessTokenExpired('invalid-token')).toBe(true); + }); +}); diff --git a/test/service/auth.svc.test.ts b/test/service/auth.svc.test.ts new file mode 100644 index 00000000..55ba805d --- /dev/null +++ b/test/service/auth.svc.test.ts @@ -0,0 +1,80 @@ +import { type Mock, vi } from 'vitest'; + +vi.mock('../../src/service/auth-token.svc.ts', () => ({ + __esModule: true, + getStoredTokens: vi.fn(), + saveTokens: vi.fn(), + clearStoredTokens: vi.fn(), + isAccessTokenExpired: vi.fn(), +})); + +vi.mock('../../src/service/auth-refresh.svc.ts', () => ({ + __esModule: true, + refreshTokens: vi.fn(), +})); + +import { getAccessToken, logoutLocally, persistTokenResponse, requireAccessToken } from '../../src/service/auth.svc.ts'; +import { refreshTokens } from '../../src/service/auth-refresh.svc.ts'; +import { + clearStoredTokens, + getStoredTokens, + isAccessTokenExpired, + saveTokens, +} from '../../src/service/auth-token.svc.ts'; + +describe('auth.svc', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('persists token responses via keyring service', async () => { + await persistTokenResponse({ access_token: 'access', refresh_token: 'refresh' }); + expect(saveTokens).toHaveBeenCalledWith({ accessToken: 'access', refreshToken: 'refresh' }); + }); + + it('returns stored access token when it is valid', async () => { + (getStoredTokens as Mock).mockResolvedValue({ accessToken: 'valid-token' }); + (isAccessTokenExpired as Mock).mockReturnValue(false); + + const token = await getAccessToken(); + expect(token).toBe('valid-token'); + expect(refreshTokens).not.toHaveBeenCalled(); + }); + + it('refreshes access token when expired and refresh token exists', async () => { + (getStoredTokens as Mock).mockResolvedValue({ accessToken: 'expired', refreshToken: 'refresh-1' }); + (isAccessTokenExpired as Mock).mockReturnValue(true); + (refreshTokens as Mock).mockResolvedValue({ access_token: 'new-token', refresh_token: 'refresh-2' }); + + const token = await getAccessToken(); + expect(token).toBe('new-token'); + expect(refreshTokens).toHaveBeenCalledWith('refresh-1'); + expect(saveTokens).toHaveBeenCalledWith({ accessToken: 'new-token', refreshToken: 'refresh-2' }); + }); + + it('propagates refresh errors when renewal fails', async () => { + (getStoredTokens as Mock).mockResolvedValue({ accessToken: 'expired', refreshToken: 'refresh-1' }); + (isAccessTokenExpired as Mock).mockReturnValue(true); + (refreshTokens as Mock).mockRejectedValue(new Error('refresh failed')); + + await expect(getAccessToken()).rejects.toThrow('refresh failed'); + }); + + it('returns undefined when no tokens are stored', async () => { + (getStoredTokens as Mock).mockResolvedValue(undefined); + + const token = await getAccessToken(); + expect(token).toBeUndefined(); + }); + + it('throws when requireAccessToken cannot obtain a token', async () => { + (getStoredTokens as Mock).mockResolvedValue(undefined); + + await expect(requireAccessToken()).rejects.toThrow(/not logged in/i); + }); + + it('clears local tokens when logging out locally', async () => { + await logoutLocally(); + expect(clearStoredTokens).toHaveBeenCalled(); + }); +}); diff --git a/test/service/cdx.svc.test.ts b/test/service/cdx.svc.test.ts index c4fbe29e..da731579 100644 --- a/test/service/cdx.svc.test.ts +++ b/test/service/cdx.svc.test.ts @@ -1,25 +1,45 @@ -import assert from 'node:assert'; -import { describe, it, mock } from 'node:test'; +import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; -import { createSbomFactory } from '../../src/service/cdx.svc.ts'; +beforeAll(() => { + if (typeof (globalThis as { File?: unknown }).File === 'undefined') { + (globalThis as { File?: unknown }).File = class File {}; + } +}); + +type SbomOptions = typeof import('../../src/service/cdx.svc.ts').SBOM_DEFAULT__OPTIONS; +type BomResult = { bomJson: unknown }; +type CreateBomFn = (path: string, options: SbomOptions) => Promise; +type PostProcessFn = (sbom: BomResult, options: SbomOptions) => BomResult | null; + +const createBomMock = vi.fn(); +const postProcessMock = vi.fn(); describe('cdx.svc createSbom', () => { + beforeEach(() => { + vi.resetModules(); + createBomMock.mockReset(); + }); + it('returns bomJson when cdxgen returns an object', async () => { const bomJson = { bomFormat: 'CycloneDX', specVersion: '1.6', components: [] }; - const createBom = mock.fn(async () => ({ bomJson })); - const postProcess = mock.fn((sbom) => sbom); - - const createSbom = createSbomFactory({ createBom, postProcess }); + createBomMock.mockResolvedValue({ bomJson }); + postProcessMock.mockReturnValue({ bomJson }); + const mod = await import('../../src/service/cdx.svc.ts'); + const createSbom = mod.createSbomFactory({ + createBom: createBomMock, + postProcess: postProcessMock, + }); const res = await createSbom('/tmp/project'); - - assert.deepStrictEqual(res, bomJson); + expect(res).toEqual(bomJson); }); it('throws when cdxgen returns a falsy value', async () => { - const createBom = mock.fn(async () => null); - - const createSbom = createSbomFactory({ createBom }); - - await assert.rejects(() => createSbom('/tmp/project'), /SBOM not generated/); + createBomMock.mockResolvedValue(null); + const mod = await import('../../src/service/cdx.svc.ts'); + const createSbom = mod.createSbomFactory({ + createBom: createBomMock, + postProcess: postProcessMock, + }); + await expect(createSbom('/tmp/project')).rejects.toThrow(/SBOM not generated/); }); }); diff --git a/test/service/committers.svc.test.ts b/test/service/committers.svc.test.ts index 7a7f5eaf..f3fe2ce2 100644 --- a/test/service/committers.svc.test.ts +++ b/test/service/committers.svc.test.ts @@ -1,6 +1,5 @@ -import assert from 'node:assert'; -import { describe, it } from 'node:test'; import { formatDate, parse } from 'date-fns'; +import { describe, expect, it } from 'vitest'; import { DEFAULT_DATE_FORMAT } from '../../src/config/constants.ts'; import { type AuthorReportRow, @@ -142,13 +141,13 @@ describe('committers', () => { it('should parse git log output into commit entries', () => { const result = parseGitLogOutput(sampleGitLog); - assert.deepStrictEqual(result, sampleEntries); + expect(result).toEqual(sampleEntries); }); it('should handle empty input', () => { const result = parseGitLogOutput(''); - assert.deepStrictEqual(result, []); + expect(result).toEqual([]); }); it('should handle quoted input', () => { @@ -172,7 +171,7 @@ describe('committers', () => { const result = parseGitLogOutput(quotedLog); - assert.deepStrictEqual(result, expected); + expect(result).toEqual(expected); }); }); @@ -180,13 +179,13 @@ describe('committers', () => { it('should generate the committers report from a git log input', () => { const result = generateCommittersReport(sampleEntries); - assert.deepStrictEqual(result, sampleAuthorReport); + expect(result).toEqual(sampleAuthorReport); }); it('should not fail if the git log input is empty', () => { const result = generateCommittersReport([]); - assert.deepStrictEqual(result, []); + expect(result).toEqual([]); }); }); @@ -194,13 +193,13 @@ describe('committers', () => { it('should generate the monthly report from a git log input', () => { const result = generateMonthlyReport(sampleEntries); - assert.deepStrictEqual(result, sampleMonthlyReport); + expect(result).toEqual(sampleMonthlyReport); }); it('should not fail if the git log input is empty', () => { const result = generateMonthlyReport([]); - assert.deepStrictEqual(result, []); + expect(result).toEqual([]); }); }); }); diff --git a/test/service/display.svc.test.ts b/test/service/display.svc.test.ts index 90e65de8..5313827d 100644 --- a/test/service/display.svc.test.ts +++ b/test/service/display.svc.test.ts @@ -1,6 +1,4 @@ -import assert from 'node:assert'; -import { describe, it } from 'node:test'; -import type { EolReport } from '@herodevs/eol-shared'; +import type { EolReport, EolScanComponentMetadata } from '@herodevs/eol-shared'; import { countComponentsByStatus, formatDataPrivacyLink, @@ -19,8 +17,8 @@ describe('display.svc', () => { isEol: true, eolAt: '2023-01-01T00:00:00.000Z', eolReasons: ['End of life'], - cve: [], - }, + cveStats: [], + } as unknown as EolScanComponentMetadata, nesRemediation: { remediations: [ { @@ -36,8 +34,8 @@ describe('display.svc', () => { isEol: false, eolAt: null, eolReasons: [], - cve: [], - }, + cveStats: [], + } as unknown as EolScanComponentMetadata, }, { purl: 'pkg:npm/test3@3.0.0', @@ -49,8 +47,8 @@ describe('display.svc', () => { isEol: false, eolAt: null, eolReasons: [], - cve: [], - }, + cveStats: [], + } as unknown as EolScanComponentMetadata, }, { purl: 'pkg:maven/org.springframework/spring-core@5.3.21', @@ -58,35 +56,38 @@ describe('display.svc', () => { isEol: true, eolAt: '2023-01-01T00:00:00.000Z', eolReasons: ['End of life'], - cve: [], - }, + cveStats: [], + } as unknown as EolScanComponentMetadata, }, ], createdOn: new Date().toISOString(), metadata: { totalComponentsCount: 5, unknownComponentsCount: 1, + totalUniqueComponentsCount: 5, }, + page: 1, + totalRecords: 5, }; describe('countComponentsByStatus', () => { it('should count components by status correctly', () => { const counts = countComponentsByStatus(mockReport); - assert.strictEqual(counts.EOL, 2); - assert.strictEqual(counts.OK, 2); - assert.strictEqual(counts.UNKNOWN, 1); - assert.strictEqual(counts.EOL_UPCOMING, 0); - assert.strictEqual(counts.NES_AVAILABLE, 1); + expect(counts.EOL).toBe(2); + expect(counts.OK).toBe(2); + expect(counts.UNKNOWN).toBe(1); + expect(counts.EOL_UPCOMING).toBe(0); + expect(counts.NES_AVAILABLE).toBe(1); }); it('should extract ecosystems correctly from various PURL formats', () => { const counts = countComponentsByStatus(mockReport); // Should extract both npm and maven ecosystems - assert.ok(counts.ECOSYSTEMS.includes('npm')); - assert.ok(counts.ECOSYSTEMS.includes('maven')); - assert.strictEqual(counts.ECOSYSTEMS.length, 2); + expect(counts.ECOSYSTEMS).toContain('npm'); + expect(counts.ECOSYSTEMS).toContain('maven'); + expect(counts.ECOSYSTEMS).toHaveLength(2); }); it('should handle empty report', () => { @@ -97,16 +98,19 @@ describe('display.svc', () => { metadata: { totalComponentsCount: 0, unknownComponentsCount: 0, + totalUniqueComponentsCount: 0, }, + page: 1, + totalRecords: 0, }; const counts = countComponentsByStatus(emptyReport); - assert.strictEqual(counts.EOL, 0); - assert.strictEqual(counts.OK, 0); - assert.strictEqual(counts.UNKNOWN, 0); - assert.strictEqual(counts.EOL_UPCOMING, 0); - assert.strictEqual(counts.NES_AVAILABLE, 0); + expect(counts.EOL).toBe(0); + expect(counts.OK).toBe(0); + expect(counts.UNKNOWN).toBe(0); + expect(counts.EOL_UPCOMING).toBe(0); + expect(counts.NES_AVAILABLE).toBe(0); }); }); @@ -114,9 +118,9 @@ describe('display.svc', () => { it('should format scan results with components', () => { const lines = formatScanResults(mockReport); - assert.ok(lines.length > 0); - assert.ok(lines.some((line) => line.includes('Scan results:'))); - assert.ok(lines.some((line) => line.includes('5 total packages scanned'))); + expect(lines.length).toBeGreaterThan(0); + expect(lines.some((line) => line.includes('Scan results:'))).toBe(true); + expect(lines.some((line) => line.includes('5 total packages scanned'))).toBe(true); }); it('should handle empty scan results', () => { @@ -127,13 +131,16 @@ describe('display.svc', () => { metadata: { totalComponentsCount: 0, unknownComponentsCount: 0, + totalUniqueComponentsCount: 0, }, + page: 1, + totalRecords: 0, }; const lines = formatScanResults(emptyReport); - assert.strictEqual(lines.length, 1); - assert.ok(lines[0].includes('No components found')); + expect(lines).toHaveLength(1); + expect(lines[0]).toContain('No components found'); }); }); @@ -141,17 +148,17 @@ describe('display.svc', () => { it('should format web report URL correctly', () => { const lines = formatWebReportUrl('test-id', 'https://example.com'); - assert.strictEqual(lines.length, 2); - assert.ok(lines[0].includes('-'.repeat(40))); - assert.ok(lines[1].includes('View your full EOL report')); - assert.ok(lines[1].includes('test-id')); + expect(lines).toHaveLength(2); + expect(lines[0]).toContain('-'.repeat(40)); + expect(lines[1]).toContain('View your full EOL report'); + expect(lines[1]).toContain('test-id'); }); it('should handle different URLs', () => { const lines = formatWebReportUrl('another-id', 'https://reports.herodevs.com'); - assert.ok(lines[1].includes('reports.herodevs.com')); - assert.ok(lines[1].includes('another-id')); + expect(lines[1]).toContain('reports.herodevs.com'); + expect(lines[1]).toContain('another-id'); }); }); @@ -159,15 +166,15 @@ describe('display.svc', () => { it('should return array with privacy link', () => { const lines = formatDataPrivacyLink(); - assert.strictEqual(lines.length, 1); - assert.ok(lines[0].includes('🔒')); - assert.ok(lines[0].includes('Learn more about data privacy')); + expect(lines).toHaveLength(1); + expect(lines[0]).toContain('🔒'); + expect(lines[0]).toContain('Learn more about data privacy'); }); it('should include HeroDevs documentation URL', () => { const lines = formatDataPrivacyLink(); - assert.ok(lines[0].includes('docs.herodevs.com/eol-ds/data-privacy-and-security')); + expect(lines[0]).toContain('docs.herodevs.com/eol-ds/data-privacy-and-security'); }); }); @@ -175,9 +182,9 @@ describe('display.svc', () => { it('should provide a save hint message', () => { const lines = formatReportSaveHint(); - assert.strictEqual(lines.length, 2); - assert.ok(lines[0].includes('-'.repeat(40))); - assert.ok(lines[1].includes('--save')); + expect(lines).toHaveLength(2); + expect(lines[0]).toContain('-'.repeat(40)); + expect(lines[1]).toContain('--save'); }); }); }); diff --git a/test/service/file.svc.test.ts b/test/service/file.svc.test.ts index eaf2aa5b..5e60da30 100644 --- a/test/service/file.svc.test.ts +++ b/test/service/file.svc.test.ts @@ -1,9 +1,7 @@ -import assert from 'node:assert'; import fs from 'node:fs'; -import { mkdir, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { after, describe, it } from 'node:test'; import type { CdxBom, EolReport, SPDX23 } from '@herodevs/eol-shared'; import { readSbomFromFile, saveArtifactToFile, validateDirectory } from '../../src/service/file.svc.ts'; @@ -48,10 +46,13 @@ describe('file.svc', () => { metadata: { totalComponentsCount: 0, unknownComponentsCount: 0, + totalUniqueComponentsCount: 0, }, + page: 1, + totalRecords: 0, }; - after(() => { + afterAll(() => { if (tempDir && fs.existsSync(tempDir)) { fs.rmSync(tempDir, { recursive: true, force: true }); } @@ -64,7 +65,7 @@ describe('file.svc', () => { await writeFile(filePath, JSON.stringify(mockSbom)); const result = readSbomFromFile(filePath); - assert.deepStrictEqual(result, mockSbom); + expect(result).toEqual(mockSbom); }); it('should read and convert a valid SPDX SBOM file to CycloneDX', async () => { @@ -74,13 +75,13 @@ describe('file.svc', () => { const result = readSbomFromFile(filePath); - assert.strictEqual(result.bomFormat, 'CycloneDX'); - assert.ok(result.specVersion); - assert.ok(Array.isArray(result.components)); + expect(result.bomFormat).toBe('CycloneDX'); + expect(result.specVersion).toBeTruthy(); + expect(Array.isArray(result.components)).toBe(true); }); it('should throw error for non-existent file', () => { - assert.throws(() => readSbomFromFile('/non/existent/path'), /SBOM file not found/); + expect(() => readSbomFromFile('/non/existent/path')).toThrow(/SBOM file not found/); }); it('should throw error for invalid JSON', async () => { @@ -88,7 +89,7 @@ describe('file.svc', () => { const filePath = join(tempDir, 'invalid.json'); await writeFile(filePath, 'invalid json'); - assert.throws(() => readSbomFromFile(filePath), /Failed to read SBOM file/); + expect(() => readSbomFromFile(filePath)).toThrow(/Failed to read SBOM file/); }); it('should throw error for invalid SBOM format (neither SPDX nor CycloneDX)', async () => { @@ -96,8 +97,7 @@ describe('file.svc', () => { const filePath = join(tempDir, 'invalid-format.json'); await writeFile(filePath, JSON.stringify({ invalid: 'format' })); - assert.throws( - () => readSbomFromFile(filePath), + expect(() => readSbomFromFile(filePath)).toThrow( /Invalid SBOM file format\. Expected SPDX 2\.3 or CycloneDX format/, ); }); @@ -105,12 +105,12 @@ describe('file.svc', () => { describe('validateDirectory', () => { it('should not throw for valid directory', async () => { - tempDir = createTempDir(); - assert.doesNotThrow(() => validateDirectory(tempDir)); + tempDir = await mkdtemp(join(tmpdir(), 'file-svc-test-')); + expect(() => validateDirectory(tempDir)).not.toThrow(); }); it('should throw error for non-existent directory', () => { - assert.throws(() => validateDirectory('/non/existent/directory'), /Directory not found/); + expect(() => validateDirectory('/non/existent/directory')).toThrow(/Directory not found/); }); it('should throw error for file instead of directory', async () => { @@ -118,7 +118,7 @@ describe('file.svc', () => { const filePath = join(tempDir, 'file.txt'); await writeFile(filePath, 'content'); - assert.throws(() => validateDirectory(filePath), /Path is not a directory/); + expect(() => validateDirectory(filePath)).toThrow(/Path is not a directory/); }); }); @@ -128,10 +128,10 @@ describe('file.svc', () => { const outputPath = saveArtifactToFile(tempDir, { kind: 'sbom', payload: mockSbom }); - assert.ok(fs.existsSync(outputPath)); + expect(fs.existsSync(outputPath)).toBe(true); const content = fs.readFileSync(outputPath, 'utf8'); const parsed = JSON.parse(content); - assert.deepStrictEqual(parsed, mockSbom); + expect(parsed).toEqual(mockSbom); }); it('should return the correct SBOM output path', async () => { @@ -139,8 +139,8 @@ describe('file.svc', () => { const outputPath = saveArtifactToFile(tempDir, { kind: 'sbom', payload: mockSbom }); - assert.ok(outputPath.endsWith('herodevs.sbom.json')); - assert.ok(outputPath.includes(tempDir)); + expect(outputPath.endsWith('herodevs.sbom.json')).toBe(true); + expect(outputPath.includes(tempDir)).toBe(true); }); it('should save SBOM to a custom path', async () => { @@ -190,10 +190,10 @@ describe('file.svc', () => { const outputPath = saveArtifactToFile(tempDir, { kind: 'sbomTrimmed', payload: mockSbom }); - assert.ok(fs.existsSync(outputPath)); + expect(fs.existsSync(outputPath)).toBe(true); const content = fs.readFileSync(outputPath, 'utf8'); const parsed = JSON.parse(content); - assert.deepStrictEqual(parsed, mockSbom); + expect(parsed).toEqual(mockSbom); }); it('should return the correct trimmed SBOM output path', async () => { @@ -201,8 +201,8 @@ describe('file.svc', () => { const outputPath = saveArtifactToFile(tempDir, { kind: 'sbomTrimmed', payload: mockSbom }); - assert.ok(outputPath.endsWith('herodevs.sbom-trimmed.json')); - assert.ok(outputPath.includes(tempDir)); + expect(outputPath.endsWith('herodevs.sbom-trimmed.json')).toBe(true); + expect(outputPath.includes(tempDir)).toBe(true); }); it('should save report to file successfully', async () => { @@ -210,10 +210,10 @@ describe('file.svc', () => { const outputPath = saveArtifactToFile(tempDir, { kind: 'report', payload: mockReport }); - assert.ok(fs.existsSync(outputPath)); + expect(fs.existsSync(outputPath)).toBe(true); const content = fs.readFileSync(outputPath, 'utf8'); const parsed = JSON.parse(content); - assert.deepStrictEqual(parsed, mockReport); + expect(parsed).toEqual(mockReport); }); it('should return the correct report output path', async () => { @@ -221,8 +221,8 @@ describe('file.svc', () => { const outputPath = saveArtifactToFile(tempDir, { kind: 'report', payload: mockReport }); - assert.ok(outputPath.endsWith('herodevs.report.json')); - assert.ok(outputPath.includes(tempDir)); + expect(outputPath.endsWith('herodevs.report.json')).toBe(true); + expect(outputPath.includes(tempDir)).toBe(true); }); it('should save report to a custom path', async () => { diff --git a/test/service/log.svc.test.ts b/test/service/log.svc.test.ts index b76a57e0..ec0486d0 100644 --- a/test/service/log.svc.test.ts +++ b/test/service/log.svc.test.ts @@ -1,22 +1,20 @@ -import assert from 'node:assert'; -import { describe, it } from 'node:test'; import { debugLogger, getErrorMessage } from '../../src/service/log.svc.ts'; describe('log.svc', () => { it('getErrorMessage returns error.message for Error', () => { const msg = getErrorMessage(new Error('boom')); - assert.strictEqual(msg, 'boom'); + expect(msg).toBe('boom'); }); it('getErrorMessage stringifies non-Error objects', () => { - assert.strictEqual(getErrorMessage({ bad: 'x' }), '{"bad":"x"}'); + expect(getErrorMessage({ bad: 'x' })).toBe('{"bad":"x"}'); }); it('getErrorMessage stringifies non-Error', () => { - assert.strictEqual(getErrorMessage('x'), 'x'); + expect(getErrorMessage('x')).toBe('x'); }); it('debugLogger is a function', () => { - assert.strictEqual(typeof debugLogger, 'function'); + expect(typeof debugLogger).toBe('function'); }); }); diff --git a/test/service/tracker.svc.test.ts b/test/service/tracker.svc.test.ts index 5887fb83..95f316b7 100644 --- a/test/service/tracker.svc.test.ts +++ b/test/service/tracker.svc.test.ts @@ -1,8 +1,7 @@ -import assert from 'node:assert'; import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; -import { afterEach, beforeEach, describe, it } from 'node:test'; import mock from 'mock-fs'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { TRACKER_DEFAULT_CONFIG, TRACKER_ROOT_FILE } from '../../src/config/tracker.config.ts'; import { createTrackerConfig, getRootDir } from '../../src/service/tracker.svc.ts'; @@ -36,7 +35,7 @@ describe('tracker.svc', () => { const result = getRootDir(dirPath); - assert.strictEqual(result, join(process.cwd(), 'path/to/package')); + expect(result).toBe(join(process.cwd(), 'path/to/package')); }); it(`should throw an error if ${TRACKER_ROOT_FILE} is not found`, () => { @@ -46,7 +45,7 @@ describe('tracker.svc', () => { getRootDir(dirPath); } catch (err) { if (err instanceof Error) { - assert.strictEqual(err.message.includes(`Couldn't find root directory for the project`), true); + expect(err.message.includes(`Couldn't find root directory for the project`)).toBe(true); } } }); @@ -71,7 +70,7 @@ describe('tracker.svc', () => { const outputDir = 'path/to/tracker/new-folder'; await createTrackerConfig(outputDir, TRACKER_DEFAULT_CONFIG); - assert.strictEqual(existsSync(outputDir), true); + expect(existsSync(outputDir)).toBe(true); } catch (_err) {} }); @@ -80,7 +79,7 @@ describe('tracker.svc', () => { const outputDir = 'path/to/tracker/new-folder'; await createTrackerConfig(outputDir, TRACKER_DEFAULT_CONFIG); - assert.strictEqual(existsSync(`${outputDir}/${TRACKER_DEFAULT_CONFIG.configFile}`), true); + expect(existsSync(`${outputDir}/${TRACKER_DEFAULT_CONFIG.configFile}`)).toBe(true); } catch (_err) {} }); @@ -91,7 +90,7 @@ describe('tracker.svc', () => { const fileOutput = readFileSync(`${outputDir}/${TRACKER_DEFAULT_CONFIG.configFile}`).toString('utf-8'); - assert.strictEqual(fileOutput, TRACKER_DEFAULT_CONFIG); + expect(fileOutput).toBe(TRACKER_DEFAULT_CONFIG); } catch (_err) {} }); @@ -101,12 +100,11 @@ describe('tracker.svc', () => { await createTrackerConfig(outputDir, TRACKER_DEFAULT_CONFIG); } catch (err) { if (err instanceof Error) { - assert.strictEqual( + expect( err.message.includes( `Configuration file already exists for this repo. If you want to overwrite it, run the command again with the --overwrite flag`, ), - true, - ); + ).toBe(true); } } }); @@ -125,10 +123,12 @@ describe('tracker.svc', () => { const fileOutput = readFileSync(`${outputDir}/${TRACKER_DEFAULT_CONFIG.configFile}`).toString('utf-8'); - assert.strictEqual(fileOutput, { - ...TRACKER_DEFAULT_CONFIG, - ignorePatterns: [], - }); + expect(fileOutput).toBe( + JSON.stringify({ + ...TRACKER_DEFAULT_CONFIG, + ignorePatterns: [], + }), + ); } catch (_err) {} }); }); diff --git a/test/tsconfig.json b/test/tsconfig.json index cc70e12d..16a759bb 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,6 +1,10 @@ { "extends": "../tsconfig", "compilerOptions": { - "noEmit": true - } + "noEmit": true, + "types": ["vitest/globals", "node"], + "rootDir": ".." + }, + "include": ["./**/*.ts"], + "exclude": ["../node_modules", "../dist"] } diff --git a/test/utils/mocks/base.mock.ts b/test/utils/mocks/base.mock.ts index c5e5a43f..b1a7aaf0 100644 --- a/test/utils/mocks/base.mock.ts +++ b/test/utils/mocks/base.mock.ts @@ -5,20 +5,25 @@ import { default as sinon } from 'sinon'; * from a FIFO */ export class BaseStackMock { - private stack: unknown[] = []; + protected queue: unknown[] = []; private stub: sinon.SinonStub | null = null; + private calls: unknown[][] = []; constructor(target: Record, prop: string) { - // Create a new sandbox for each instance - this.stub = sinon.stub(target, prop).callsFake(() => this.next()); + this.stub = sinon.stub(target, prop).callsFake((...args: unknown[]) => this.invoke(args)); } protected next() { - return Promise.resolve(this.stack.shift()); + return Promise.resolve(this.queue.shift()); + } + + private invoke(args: unknown[]) { + this.calls.push(args); + return this.next(); } public push(value: unknown) { - this.stack.push(value); + this.queue.push(value); return this; } @@ -27,6 +32,11 @@ export class BaseStackMock { this.stub.restore(); this.stub = null; } - this.stack = []; + this.queue = []; + this.calls = []; + } + + public getCalls() { + return [...this.calls]; } } diff --git a/test/utils/mocks/fetch.mock.ts b/test/utils/mocks/fetch.mock.ts index 280fb3b1..890b6c8e 100644 --- a/test/utils/mocks/fetch.mock.ts +++ b/test/utils/mocks/fetch.mock.ts @@ -7,11 +7,13 @@ import { BaseStackMock } from './base.mock.ts'; export class FetchMock extends BaseStackMock { constructor(stack: unknown[] = []) { super(globalThis, 'fetch'); - this.stack = stack; + for (const value of stack) { + this.push(value); + } } addGraphQL(data?: D, errors: unknown[] = []) { - this.stack.push({ + this.push({ headers: { get: () => 'application/json; charset=utf-8', }, @@ -25,4 +27,11 @@ export class FetchMock extends BaseStackMock { } as unknown as Response); return this; } + + getCalls() { + return super.getCalls().map(([input, init]) => ({ + input, + init: init as RequestInit | undefined, + })); + } } diff --git a/test/utils/open-in-browser.test.ts b/test/utils/open-in-browser.test.ts new file mode 100644 index 00000000..530bbf3d --- /dev/null +++ b/test/utils/open-in-browser.test.ts @@ -0,0 +1,59 @@ +import type { ExecException } from 'node:child_process'; + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const execMock = vi.fn<[string, ((err: ExecException | null) => void)?], void>(); +const platformMock = vi.fn<[], NodeJS.Platform>(); + +vi.mock('node:child_process', () => ({ + exec: execMock, +})); + +vi.mock('node:os', () => ({ + platform: platformMock, +})); + +describe('openInBrowser', () => { + let openInBrowser: (url: string) => Promise; + + beforeEach(async () => { + execMock.mockReset(); + platformMock.mockReset(); + vi.resetModules(); + ({ openInBrowser } = await import('../../src/utils/open-in-browser.ts')); + }); + + it('uses macOS open command when platform is darwin', async () => { + platformMock.mockReturnValue('darwin'); + execMock.mockImplementation((_cmd, cb) => cb?.(null)); + + await expect(openInBrowser('https://example.com')).resolves.toBeUndefined(); + + expect(execMock).toHaveBeenCalledWith('open "https://example.com"', expect.any(Function)); + }); + + it('uses Windows start command when platform is win32', async () => { + platformMock.mockReturnValue('win32'); + execMock.mockImplementation((_cmd, cb) => cb?.(null)); + + await expect(openInBrowser('https://example.com')).resolves.toBeUndefined(); + + expect(execMock).toHaveBeenCalledWith('start "" "https://example.com"', expect.any(Function)); + }); + + it('falls back to xdg-open on other platforms', async () => { + platformMock.mockReturnValue('linux'); + execMock.mockImplementation((_cmd, cb) => cb?.(null)); + + await expect(openInBrowser('https://example.com')).resolves.toBeUndefined(); + + expect(execMock).toHaveBeenCalledWith('xdg-open "https://example.com"', expect.any(Function)); + }); + + it('rejects when the underlying command fails', async () => { + platformMock.mockReturnValue('darwin'); + execMock.mockImplementation((_cmd, cb) => cb?.(new Error('boom'))); + + await expect(openInBrowser('https://example.com')).rejects.toThrow('Failed to open browser: boom'); + }); +}); diff --git a/test/utils/token.ts b/test/utils/token.ts new file mode 100644 index 00000000..a1120644 --- /dev/null +++ b/test/utils/token.ts @@ -0,0 +1,7 @@ +export function createTokenWithExp(offsetSeconds: number) { + const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url'); + const payload = Buffer.from(JSON.stringify({ exp: Math.floor(Date.now() / 1000) + offsetSeconds })).toString( + 'base64url', + ); + return `${header}.${payload}.signature`; +} diff --git a/test/vitest.setup.ts b/test/vitest.setup.ts new file mode 100644 index 00000000..decc1a81 --- /dev/null +++ b/test/vitest.setup.ts @@ -0,0 +1,20 @@ +import { webcrypto } from 'node:crypto'; +import { vi } from 'vitest'; + +if (typeof globalThis.crypto === 'undefined') { + Object.defineProperty(globalThis, 'crypto', { + value: webcrypto, + configurable: true, + writable: true, + }); +} + +vi.mock('terminal-link', () => ({ + __esModule: true, + default: (text: string | undefined, url: string | undefined) => `${text ?? ''} (${url ?? ''})`, +})); + +vi.mock('update-notifier', () => ({ + __esModule: true, + default: () => ({ notify: vi.fn(), update: undefined }), +})); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..fc549811 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vitest/config'; + +const inlineDeps = ['@herodevs/eol-shared', 'update-notifier', '@apollo/client']; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['test/**/*.test.ts', 'test/**/*.jest.ts'], + setupFiles: 'test/vitest.setup.ts', + server: { + deps: { + inline: inlineDeps, + }, + }, + }, +});